karafka 2.5.5 → 2.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE-COMM +4 -0
  4. data/README.md +2 -2
  5. data/certs/expired.txt +2 -0
  6. data/karafka.gemspec +23 -23
  7. data/lib/active_job/karafka.rb +2 -2
  8. data/lib/active_job/queue_adapters/karafka_adapter.rb +5 -5
  9. data/lib/karafka/active_job/consumer.rb +3 -3
  10. data/lib/karafka/active_job/current_attributes.rb +4 -4
  11. data/lib/karafka/active_job/job_options_contract.rb +2 -2
  12. data/lib/karafka/admin/acl.rb +3 -3
  13. data/lib/karafka/admin/configs/resource.rb +1 -1
  14. data/lib/karafka/admin/configs.rb +1 -1
  15. data/lib/karafka/admin/consumer_groups.rb +8 -8
  16. data/lib/karafka/admin/contracts/replication.rb +2 -2
  17. data/lib/karafka/admin/replication.rb +21 -21
  18. data/lib/karafka/admin/topics.rb +6 -6
  19. data/lib/karafka/admin.rb +4 -5
  20. data/lib/karafka/app.rb +3 -3
  21. data/lib/karafka/base_consumer.rb +34 -30
  22. data/lib/karafka/cli/base.rb +8 -8
  23. data/lib/karafka/cli/console.rb +1 -1
  24. data/lib/karafka/cli/contracts/server.rb +12 -12
  25. data/lib/karafka/cli/help.rb +2 -2
  26. data/lib/karafka/cli/info.rb +4 -4
  27. data/lib/karafka/cli/install.rb +11 -11
  28. data/lib/karafka/cli/server.rb +6 -6
  29. data/lib/karafka/cli/swarm.rb +1 -1
  30. data/lib/karafka/cli/topics/align.rb +4 -4
  31. data/lib/karafka/cli/topics/base.rb +5 -5
  32. data/lib/karafka/cli/topics/create.rb +2 -2
  33. data/lib/karafka/cli/topics/delete.rb +2 -2
  34. data/lib/karafka/cli/topics/help.rb +5 -1
  35. data/lib/karafka/cli/topics/plan.rb +16 -16
  36. data/lib/karafka/cli/topics/repartition.rb +3 -3
  37. data/lib/karafka/cli/topics.rb +22 -22
  38. data/lib/karafka/cli.rb +2 -2
  39. data/lib/karafka/connection/client.rb +17 -17
  40. data/lib/karafka/connection/listener.rb +6 -6
  41. data/lib/karafka/connection/mode.rb +1 -1
  42. data/lib/karafka/connection/proxy.rb +1 -1
  43. data/lib/karafka/connection/status.rb +2 -2
  44. data/lib/karafka/constraints.rb +3 -3
  45. data/lib/karafka/embedded.rb +3 -3
  46. data/lib/karafka/env.rb +4 -4
  47. data/lib/karafka/errors.rb +6 -1
  48. data/lib/karafka/execution_mode.rb +1 -1
  49. data/lib/karafka/helpers/config_importer.rb +2 -2
  50. data/lib/karafka/helpers/interval_runner.rb +4 -2
  51. data/lib/karafka/helpers/multi_delegator.rb +1 -1
  52. data/lib/karafka/instrumentation/assignments_tracker.rb +9 -9
  53. data/lib/karafka/instrumentation/callbacks/error.rb +5 -5
  54. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +4 -4
  55. data/lib/karafka/instrumentation/callbacks/rebalance.rb +6 -6
  56. data/lib/karafka/instrumentation/callbacks/statistics.rb +5 -5
  57. data/lib/karafka/instrumentation/logger.rb +7 -7
  58. data/lib/karafka/instrumentation/logger_listener.rb +76 -63
  59. data/lib/karafka/instrumentation/vendors/appsignal/base.rb +1 -1
  60. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +1 -1
  61. data/lib/karafka/instrumentation/vendors/appsignal/errors_listener.rb +1 -1
  62. data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +36 -36
  63. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +33 -28
  64. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +38 -38
  65. data/lib/karafka/instrumentation/vendors/kubernetes/base_listener.rb +5 -5
  66. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +1 -1
  67. data/lib/karafka/instrumentation/vendors/kubernetes/swarm_liveness_listener.rb +1 -1
  68. data/lib/karafka/licenser.rb +115 -8
  69. data/lib/karafka/messages/builders/batch_metadata.rb +4 -2
  70. data/lib/karafka/messages/messages.rb +1 -1
  71. data/lib/karafka/patches/rdkafka/bindings.rb +2 -2
  72. data/lib/karafka/pro/active_job/job_options_contract.rb +2 -2
  73. data/lib/karafka/pro/cleaner/messages/messages.rb +10 -0
  74. data/lib/karafka/pro/cli/contracts/server.rb +12 -12
  75. data/lib/karafka/pro/cli/parallel_segments/base.rb +4 -4
  76. data/lib/karafka/pro/cli/parallel_segments/collapse.rb +5 -5
  77. data/lib/karafka/pro/cli/parallel_segments/distribute.rb +3 -3
  78. data/lib/karafka/pro/cli/parallel_segments.rb +7 -7
  79. data/lib/karafka/pro/cli/topics/health.rb +162 -0
  80. data/lib/karafka/pro/cli/topics.rb +52 -0
  81. data/lib/karafka/pro/connection/manager.rb +14 -14
  82. data/lib/karafka/pro/encryption/contracts/config.rb +2 -2
  83. data/lib/karafka/pro/encryption/messages/middleware.rb +2 -2
  84. data/lib/karafka/pro/encryption/messages/parser.rb +2 -2
  85. data/lib/karafka/pro/encryption/setup/config.rb +2 -2
  86. data/lib/karafka/pro/iterator/tpl_builder.rb +2 -2
  87. data/lib/karafka/pro/iterator.rb +1 -1
  88. data/lib/karafka/pro/loader.rb +2 -1
  89. data/lib/karafka/pro/processing/adaptive_iterator/consumer.rb +1 -1
  90. data/lib/karafka/pro/processing/coordinators/virtual_offset_manager.rb +24 -14
  91. data/lib/karafka/pro/processing/filters/base.rb +1 -1
  92. data/lib/karafka/pro/processing/filters/delayer.rb +2 -2
  93. data/lib/karafka/pro/processing/filters/inline_insights_delayer.rb +1 -1
  94. data/lib/karafka/pro/processing/offset_metadata/consumer.rb +1 -1
  95. data/lib/karafka/pro/processing/parallel_segments/filters/base.rb +6 -6
  96. data/lib/karafka/pro/processing/partitioner.rb +3 -3
  97. data/lib/karafka/pro/processing/periodic_job/consumer.rb +6 -5
  98. data/lib/karafka/pro/processing/piping/consumer.rb +7 -7
  99. data/lib/karafka/pro/processing/schedulers/base.rb +5 -5
  100. data/lib/karafka/pro/processing/schedulers/default.rb +5 -5
  101. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +6 -3
  102. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +6 -3
  103. data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom_vp.rb +6 -3
  104. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -2
  105. data/lib/karafka/pro/processing/strategies/default.rb +22 -22
  106. data/lib/karafka/pro/processing/strategies/dlq/default.rb +7 -7
  107. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +6 -3
  108. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +6 -3
  109. data/lib/karafka/pro/processing/strategies/ftr/default.rb +2 -2
  110. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -2
  111. data/lib/karafka/pro/processing/strategies/lrj/ftr.rb +6 -3
  112. data/lib/karafka/pro/processing/strategies/lrj/ftr_mom.rb +6 -3
  113. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -2
  114. data/lib/karafka/pro/recurring_tasks/consumer.rb +2 -2
  115. data/lib/karafka/pro/recurring_tasks/contracts/config.rb +2 -2
  116. data/lib/karafka/pro/recurring_tasks/contracts/task.rb +2 -2
  117. data/lib/karafka/pro/recurring_tasks/dispatcher.rb +2 -2
  118. data/lib/karafka/pro/recurring_tasks/listener.rb +1 -1
  119. data/lib/karafka/pro/recurring_tasks/matcher.rb +2 -2
  120. data/lib/karafka/pro/recurring_tasks/serializer.rb +5 -5
  121. data/lib/karafka/pro/recurring_tasks/setup/config.rb +3 -3
  122. data/lib/karafka/pro/recurring_tasks/task.rb +4 -4
  123. data/lib/karafka/pro/recurring_tasks.rb +4 -4
  124. data/lib/karafka/pro/routing/features/adaptive_iterator/contracts/topic.rb +2 -2
  125. data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +2 -2
  126. data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +1 -1
  127. data/lib/karafka/pro/routing/features/delaying/contracts/topic.rb +2 -2
  128. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +2 -2
  129. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +2 -2
  130. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +1 -1
  131. data/lib/karafka/pro/routing/features/expiring/contracts/topic.rb +2 -2
  132. data/lib/karafka/pro/routing/features/filtering/contracts/topic.rb +2 -2
  133. data/lib/karafka/pro/routing/features/inline_insights/contracts/topic.rb +2 -2
  134. data/lib/karafka/pro/routing/features/long_running_job/contracts/topic.rb +2 -2
  135. data/lib/karafka/pro/routing/features/long_running_job/topic.rb +1 -1
  136. data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +2 -2
  137. data/lib/karafka/pro/routing/features/multiplexing.rb +5 -5
  138. data/lib/karafka/pro/routing/features/non_blocking_job/topic.rb +1 -1
  139. data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +2 -2
  140. data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +1 -1
  141. data/lib/karafka/pro/routing/features/offset_metadata.rb +1 -1
  142. data/lib/karafka/pro/routing/features/parallel_segments/consumer_group.rb +5 -5
  143. data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +2 -2
  144. data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +2 -2
  145. data/lib/karafka/pro/routing/features/patterns/contracts/pattern.rb +3 -3
  146. data/lib/karafka/pro/routing/features/patterns/contracts/topic.rb +2 -2
  147. data/lib/karafka/pro/routing/features/patterns/topic.rb +1 -1
  148. data/lib/karafka/pro/routing/features/pausing/contracts/topic.rb +2 -2
  149. data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +2 -2
  150. data/lib/karafka/pro/routing/features/periodic_job/topic.rb +1 -1
  151. data/lib/karafka/pro/routing/features/recurring_tasks/builder.rb +7 -7
  152. data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +2 -2
  153. data/lib/karafka/pro/routing/features/scheduled_messages/builder.rb +13 -13
  154. data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +2 -2
  155. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +2 -2
  156. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +2 -2
  157. data/lib/karafka/pro/routing/features/swarm.rb +1 -1
  158. data/lib/karafka/pro/routing/features/throttling/contracts/topic.rb +2 -2
  159. data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +7 -7
  160. data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +2 -2
  161. data/lib/karafka/pro/scheduled_messages/consumer.rb +4 -4
  162. data/lib/karafka/pro/scheduled_messages/contracts/config.rb +2 -2
  163. data/lib/karafka/pro/scheduled_messages/contracts/message.rb +10 -10
  164. data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +2 -2
  165. data/lib/karafka/pro/scheduled_messages/deserializers/headers.rb +4 -4
  166. data/lib/karafka/pro/scheduled_messages/dispatcher.rb +5 -5
  167. data/lib/karafka/pro/scheduled_messages/proxy.rb +8 -8
  168. data/lib/karafka/pro/scheduled_messages/schema_validator.rb +1 -1
  169. data/lib/karafka/pro/scheduled_messages/setup/config.rb +2 -2
  170. data/lib/karafka/pro/scheduled_messages/state.rb +1 -1
  171. data/lib/karafka/pro/scheduled_messages/tracker.rb +2 -2
  172. data/lib/karafka/pro/scheduled_messages.rb +2 -2
  173. data/lib/karafka/pro/swarm/liveness_listener.rb +2 -2
  174. data/lib/karafka/process.rb +1 -1
  175. data/lib/karafka/processing/coordinator.rb +1 -1
  176. data/lib/karafka/processing/inline_insights/consumer.rb +4 -4
  177. data/lib/karafka/processing/inline_insights/tracker.rb +6 -6
  178. data/lib/karafka/processing/jobs/base.rb +6 -4
  179. data/lib/karafka/processing/jobs_queue.rb +10 -0
  180. data/lib/karafka/processing/schedulers/default.rb +4 -4
  181. data/lib/karafka/processing/strategies/base.rb +6 -6
  182. data/lib/karafka/processing/strategies/default.rb +13 -13
  183. data/lib/karafka/processing/strategies/dlq.rb +1 -1
  184. data/lib/karafka/processing/worker.rb +5 -5
  185. data/lib/karafka/railtie.rb +11 -11
  186. data/lib/karafka/routing/builder.rb +3 -3
  187. data/lib/karafka/routing/contracts/consumer_group.rb +6 -6
  188. data/lib/karafka/routing/contracts/routing.rb +2 -2
  189. data/lib/karafka/routing/contracts/topic.rb +4 -4
  190. data/lib/karafka/routing/features/active_job/contracts/topic.rb +3 -3
  191. data/lib/karafka/routing/features/base/expander.rb +4 -4
  192. data/lib/karafka/routing/features/base.rb +8 -8
  193. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +2 -2
  194. data/lib/karafka/routing/features/declaratives/contracts/topic.rb +2 -2
  195. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +2 -2
  196. data/lib/karafka/routing/features/eofed/contracts/topic.rb +3 -3
  197. data/lib/karafka/routing/features/inline_insights/contracts/topic.rb +2 -2
  198. data/lib/karafka/routing/features/inline_insights.rb +7 -7
  199. data/lib/karafka/routing/features/manual_offset_management/contracts/topic.rb +2 -2
  200. data/lib/karafka/routing/subscription_group.rb +9 -9
  201. data/lib/karafka/runner.rb +3 -3
  202. data/lib/karafka/server.rb +14 -5
  203. data/lib/karafka/setup/attributes_map.rb +7 -7
  204. data/lib/karafka/setup/config.rb +11 -11
  205. data/lib/karafka/setup/contracts/config.rb +2 -2
  206. data/lib/karafka/setup/defaults_injector.rb +11 -11
  207. data/lib/karafka/swarm/manager.rb +6 -6
  208. data/lib/karafka/swarm/node.rb +8 -37
  209. data/lib/karafka/swarm/producer_replacer.rb +110 -0
  210. data/lib/karafka/swarm/supervisor.rb +9 -6
  211. data/lib/karafka/swarm.rb +1 -1
  212. data/lib/karafka/time_trackers/pause.rb +1 -1
  213. data/lib/karafka/version.rb +1 -1
  214. data/lib/karafka.rb +36 -36
  215. metadata +7 -3
@@ -37,12 +37,12 @@ module Karafka
37
37
  class Collapse < Base
38
38
  # Runs the collapse operation
39
39
  def call
40
- puts 'Starting parallel segments collapse...'
40
+ puts "Starting parallel segments collapse..."
41
41
 
42
42
  segments_count = applicable_groups.size
43
43
 
44
44
  if segments_count.zero?
45
- puts "#{red('No')} consumer groups with parallel segments configuration found"
45
+ puts "#{red("No")} consumer groups with parallel segments configuration found"
46
46
 
47
47
  return
48
48
  end
@@ -74,7 +74,7 @@ module Karafka
74
74
  end
75
75
 
76
76
  puts
77
- puts "Collapse completed #{green('successfully')}!"
77
+ puts "Collapse completed #{green("successfully")}!"
78
78
  end
79
79
 
80
80
  private
@@ -141,7 +141,7 @@ module Karafka
141
141
 
142
142
  puts(
143
143
  " Inconclusive offsets for #{red(topic_name)}##{red(partition_id)}: " \
144
- "#{parallel_offsets.to_a.join(', ')}"
144
+ "#{parallel_offsets.to_a.join(", ")}"
145
145
  )
146
146
  end
147
147
  end
@@ -150,7 +150,7 @@ module Karafka
150
150
 
151
151
  raise(
152
152
  Karafka::Errors::CommandValidationError,
153
- "Parallel segments for #{red(segment_origin)} have #{red('inconclusive')} offsets"
153
+ "Parallel segments for #{red(segment_origin)} have #{red("inconclusive")} offsets"
154
154
  )
155
155
  end
156
156
 
@@ -44,12 +44,12 @@ module Karafka
44
44
  class Distribute < Base
45
45
  # Runs the distribution process
46
46
  def call
47
- puts 'Starting parallel segments distribution...'
47
+ puts "Starting parallel segments distribution..."
48
48
 
49
49
  segments_count = applicable_groups.size
50
50
 
51
51
  if segments_count.zero?
52
- puts "#{red('No')} consumer groups with parallel segments configuration found"
52
+ puts "#{red("No")} consumer groups with parallel segments configuration found"
53
53
 
54
54
  return
55
55
  end
@@ -79,7 +79,7 @@ module Karafka
79
79
  end
80
80
 
81
81
  puts
82
- puts "Distribution completed #{green('successfully')}!"
82
+ puts "Distribution completed #{green("successfully")}!"
83
83
  end
84
84
 
85
85
  private
@@ -31,11 +31,11 @@ module Karafka
31
31
  kafka_config: %i[kafka]
32
32
  )
33
33
 
34
- desc 'Allows for parallel segments management'
34
+ desc "Allows for parallel segments management"
35
35
 
36
36
  option(
37
37
  :groups,
38
- 'Names of consumer groups on which we want to run the command. All if not provided',
38
+ "Names of consumer groups on which we want to run the command. All if not provided",
39
39
  Array,
40
40
  %w[
41
41
  --groups
@@ -50,7 +50,7 @@ module Karafka
50
50
  # their existing distributed offsets to be reset.
51
51
  option(
52
52
  :force,
53
- 'Should an operation on the parallel segments consumer group be forced',
53
+ "Should an operation on the parallel segments consumer group be forced",
54
54
  TrueClass,
55
55
  %w[
56
56
  --force
@@ -58,13 +58,13 @@ module Karafka
58
58
  )
59
59
 
60
60
  # @param action [String] action we want to take
61
- def call(action = 'distribute')
61
+ def call(action = "distribute")
62
62
  case action
63
- when 'distribute'
63
+ when "distribute"
64
64
  Distribute.new(options).call
65
- when 'collapse'
65
+ when "collapse"
66
66
  Collapse.new(options).call
67
- when 'reset'
67
+ when "reset"
68
68
  Collapse.new(options).call
69
69
  Distribute.new(options).call
70
70
  else
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Karafka Pro - Source Available Commercial Software
4
+ # Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
5
+ #
6
+ # This software is NOT open source. It is source-available commercial software
7
+ # requiring a paid license for use. It is NOT covered by LGPL.
8
+ #
9
+ # PROHIBITED:
10
+ # - Use without a valid commercial license
11
+ # - Redistribution, modification, or derivative works without authorization
12
+ # - Use as training data for AI/ML models or inclusion in datasets
13
+ # - Scraping, crawling, or automated collection for any purpose
14
+ #
15
+ # PERMITTED:
16
+ # - Reading, referencing, and linking for personal or commercial use
17
+ # - Runtime retrieval by AI assistants, coding agents, and RAG systems
18
+ # for the purpose of providing contextual help to Karafka users
19
+ #
20
+ # License: https://karafka.io/docs/Pro-License-Comm/
21
+ # Contact: contact@karafka.io
22
+
23
+ module Karafka
24
+ module Pro
25
+ module Cli
26
+ module Topics
27
+ # Checks health of Kafka topics by analyzing replication and durability settings
28
+ # Identifies topics with:
29
+ # - No redundancy (RF=1)
30
+ # - Zero fault tolerance (RF <= min.insync.replicas)
31
+ # - Low durability (min.insync.replicas=1)
32
+ class Health < Karafka::Cli::Topics::Base
33
+ # Executes the health check across all topics in the cluster
34
+ # @return [Boolean] true if issues were found, false if all topics are healthy
35
+ def call
36
+ issues_found = false
37
+
38
+ supervised("Checking topics health") do
39
+ topics = existing_topics
40
+ puts "Found #{topics.count} topics to check..."
41
+ puts
42
+
43
+ topics.each do |topic|
44
+ # Skip internal Kafka topics
45
+ next if topic[:topic_name].start_with?("__")
46
+
47
+ issue = analyze_topic(topic)
48
+
49
+ if issue
50
+ display_issue(issue)
51
+ issues_found = true
52
+ else
53
+ puts "#{green("✓")} #{topic[:topic_name]}"
54
+ end
55
+ end
56
+ end
57
+
58
+ puts
59
+ display_summary(issues_found)
60
+ puts
61
+
62
+ issues_found
63
+ end
64
+
65
+ private
66
+
67
+ # Analyzes a single topic for replication and durability issues
68
+ # @param topic [Hash] topic metadata from cluster info
69
+ # @return [Hash, nil] issue hash if problems found, nil if healthy
70
+ def analyze_topic(topic)
71
+ topic_name = topic[:topic_name]
72
+ rf = topic[:partitions].first&.fetch(:replica_count) || 0
73
+ min_isr = fetch_min_insync_replicas(topic_name)
74
+
75
+ check_replication_issue(topic_name, rf, min_isr)
76
+ end
77
+
78
+ # Fetches min.insync.replicas configuration for a topic
79
+ # @param topic_name [String] name of the topic
80
+ # @return [Integer] min.insync.replicas value
81
+ def fetch_min_insync_replicas(topic_name)
82
+ configs = Admin::Configs.describe(
83
+ Admin::Configs::Resource.new(type: :topic, name: topic_name)
84
+ ).first.configs
85
+
86
+ configs.find { |c| c.name == "min.insync.replicas" }.value.to_i
87
+ end
88
+
89
+ # Checks for replication and durability issues
90
+ # @param topic_name [String] name of the topic
91
+ # @param rf [Integer] replication factor
92
+ # @param min_isr [Integer] min.insync.replicas setting
93
+ # @return [Hash, nil] issue hash if problems found, nil if healthy
94
+ def check_replication_issue(topic_name, rf, min_isr)
95
+ return build_issue(topic_name, rf, min_isr, :critical, "RF=#{rf} (no redundancy)") if rf == 1
96
+
97
+ if rf <= min_isr
98
+ return build_issue(
99
+ topic_name,
100
+ rf,
101
+ min_isr,
102
+ :critical,
103
+ "RF=#{rf}, min.insync=#{min_isr} (zero fault tolerance)"
104
+ )
105
+ end
106
+
107
+ if min_isr == 1
108
+ return build_issue(
109
+ topic_name,
110
+ rf,
111
+ min_isr,
112
+ :warning,
113
+ "RF=#{rf}, min.insync=#{min_isr} (low durability)"
114
+ )
115
+ end
116
+
117
+ nil
118
+ end
119
+
120
+ # Builds an issue hash with consistent structure
121
+ # @param topic [String] topic name
122
+ # @param rf [Integer] replication factor
123
+ # @param min_isr [Integer] min.insync.replicas
124
+ # @param severity [Symbol] :critical or :warning
125
+ # @param message [String] human-readable issue description
126
+ # @return [Hash] issue details
127
+ def build_issue(topic, rf, min_isr, severity, message)
128
+ {
129
+ topic: topic,
130
+ rf: rf,
131
+ min_isr: min_isr,
132
+ severity: severity,
133
+ message: message
134
+ }
135
+ end
136
+
137
+ # Displays a single issue immediately as it's found
138
+ # @param issue [Hash] issue details
139
+ def display_issue(issue)
140
+ color_method = (issue[:severity] == :critical) ? :red : :yellow
141
+ symbol = (issue[:severity] == :critical) ? "\u2717" : "\u26A0"
142
+ puts "#{send(color_method, symbol)} #{issue[:topic]}: #{issue[:message]}"
143
+ end
144
+
145
+ # Displays final summary and recommendations
146
+ # @param issues_found [Boolean] whether any issues were found
147
+ def display_summary(issues_found)
148
+ if issues_found
149
+ puts
150
+ puts "#{grey("Recommendations")}:"
151
+ puts " #{grey("\u2022")} Ensure RF >= 3 for production topics"
152
+ puts " #{grey("\u2022")} Set min.insync.replicas to at least 2"
153
+ puts " #{grey("\u2022")} Maintain RF > min.insync.replicas for fault tolerance"
154
+ else
155
+ puts "#{green("\u2713")} All topics are healthy"
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Karafka Pro - Source Available Commercial Software
4
+ # Copyright (c) 2017-present Maciej Mensfeld. All rights reserved.
5
+ #
6
+ # This software is NOT open source. It is source-available commercial software
7
+ # requiring a paid license for use. It is NOT covered by LGPL.
8
+ #
9
+ # PROHIBITED:
10
+ # - Use without a valid commercial license
11
+ # - Redistribution, modification, or derivative works without authorization
12
+ # - Use as training data for AI/ML models or inclusion in datasets
13
+ # - Scraping, crawling, or automated collection for any purpose
14
+ #
15
+ # PERMITTED:
16
+ # - Reading, referencing, and linking for personal or commercial use
17
+ # - Runtime retrieval by AI assistants, coding agents, and RAG systems
18
+ # for the purpose of providing contextual help to Karafka users
19
+ #
20
+ # License: https://karafka.io/docs/Pro-License-Comm/
21
+ # Contact: contact@karafka.io
22
+
23
+ module Karafka
24
+ module Pro
25
+ module Cli
26
+ # Pro extension for the Topics CLI command
27
+ # Adds health checking functionality to the base topics command
28
+ module Topics
29
+ # Extends the base topics command to add health action support
30
+ module Extension
31
+ # @param action [String] action we want to take
32
+ def call(action = "help")
33
+ if action == "health"
34
+ detailed_exit_code = options.fetch(:detailed_exitcode, false)
35
+ issues_found = Pro::Cli::Topics::Health.new.call
36
+
37
+ return unless detailed_exit_code
38
+
39
+ # Exit with code 2 if issues found, code 0 if all healthy
40
+ issues_found ? exit(2) : exit(0)
41
+ else
42
+ super
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # Extend the OSS Topics command with Pro functionality
52
+ Karafka::Cli::Topics.prepend(Karafka::Pro::Cli::Topics::Extension)
@@ -42,8 +42,8 @@ module Karafka
42
42
  @mutex = Mutex.new
43
43
  @changes = Hash.new do |h, k|
44
44
  h[k] = {
45
- state: '',
46
- join_state: '',
45
+ state: "",
46
+ join_state: "",
47
47
  state_age: 0,
48
48
  changed_at: monotonic_now
49
49
  }
@@ -86,9 +86,9 @@ module Karafka
86
86
  times = []
87
87
  # stateage is in microseconds
88
88
  # We monitor broker changes to make sure we do not introduce extra friction
89
- times << (statistics['brokers'].values.map { |stats| stats['stateage'] }.min / 1_000)
90
- times << statistics['cgrp']['rebalance_age']
91
- times << statistics['cgrp']['stateage']
89
+ times << (statistics["brokers"].values.map { |stats| stats["stateage"] }.min / 1_000)
90
+ times << statistics["cgrp"]["rebalance_age"]
91
+ times << statistics["cgrp"]["stateage"]
92
92
 
93
93
  # Keep the previous change age for changes that were triggered by us
94
94
  previous_changed_at = @changes[subscription_group_id][:changed_at]
@@ -96,8 +96,8 @@ module Karafka
96
96
  @changes[subscription_group_id].merge!(
97
97
  state_age: times.min,
98
98
  changed_at: previous_changed_at,
99
- join_state: statistics['cgrp']['join_state'],
100
- state: statistics['cgrp']['state']
99
+ join_state: statistics["cgrp"]["join_state"],
100
+ state: statistics["cgrp"]["state"]
101
101
  )
102
102
  end
103
103
 
@@ -198,11 +198,11 @@ module Karafka
198
198
  # partitions assigned in sgs that can be scaled. If that is the case, we scale up.
199
199
  def scale_up
200
200
  multi_part_sgs_families = Karafka::App
201
- .assignments
202
- .select { |_, partitions| partitions.size > 1 }
203
- .keys
204
- .map { |sg| sg.subscription_group.name }
205
- .uniq
201
+ .assignments
202
+ .select { |_, partitions| partitions.size > 1 }
203
+ .keys
204
+ .map { |sg| sg.subscription_group.name }
205
+ .uniq
206
206
 
207
207
  # Select connections for scaling up
208
208
  in_sg_families do |first_subscription_group, sg_listeners|
@@ -256,8 +256,8 @@ module Karafka
256
256
 
257
257
  state[:state_age] >= @scale_delay &&
258
258
  (monotonic_now - state[:changed_at]) >= @scale_delay &&
259
- state[:state] == 'up' &&
260
- state[:join_state] == 'steady'
259
+ state[:state] == "up" &&
260
+ state[:join_state] == "steady"
261
261
  end
262
262
  end
263
263
 
@@ -29,8 +29,8 @@ module Karafka
29
29
  class Config < Karafka::Contracts::Base
30
30
  configure do |config|
31
31
  config.error_messages = YAML.safe_load_file(
32
- File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
33
- ).fetch('en').fetch('validations').fetch('setup').fetch('config')
32
+ File.join(Karafka.gem_root, "config", "locales", "pro_errors.yml")
33
+ ).fetch("en").fetch("validations").fetch("setup").fetch("config")
34
34
  end
35
35
 
36
36
  nested(:encryption) do
@@ -41,12 +41,12 @@ module Karafka
41
41
  payload = message[:payload]
42
42
 
43
43
  message[:headers] ||= {}
44
- message[:headers]['encryption'] = version
44
+ message[:headers]["encryption"] = version
45
45
  message[:payload] = cipher.encrypt(payload)
46
46
 
47
47
  return message unless fingerprinter
48
48
 
49
- message[:headers]['encryption_fingerprint'] = fingerprinter.hexdigest(payload)
49
+ message[:headers]["encryption_fingerprint"] = fingerprinter.hexdigest(payload)
50
50
 
51
51
  message
52
52
  end
@@ -39,8 +39,8 @@ module Karafka
39
39
  # @return [Object] deserialized payload
40
40
  def call(message)
41
41
  headers = message.headers
42
- encryption = headers['encryption']
43
- fingerprint = headers['encryption_fingerprint']
42
+ encryption = headers["encryption"]
43
+ fingerprint = headers["encryption_fingerprint"]
44
44
 
45
45
  return super unless active && encryption
46
46
 
@@ -35,12 +35,12 @@ module Karafka
35
35
  # Supporting versions allows us to be able to rotate private and public keys in case
36
36
  # we would need this. We can increase the version, rotate and Karafka when decrypting
37
37
  # will figure out proper private key based on the version
38
- setting(:version, default: '1')
38
+ setting(:version, default: "1")
39
39
 
40
40
  # We always support one public key for producing messages
41
41
  # Public key needs to be always present even if we do not plan to produce messages from
42
42
  # a Karafka process. This is because of the web-ui and potentially other cases like this
43
- setting(:public_key, default: '')
43
+ setting(:public_key, default: "")
44
44
 
45
45
  # Private keys in pem format, where the key is the version and value is the key.
46
46
  # This allows us to support key rotation
@@ -177,8 +177,8 @@ module Karafka
177
177
 
178
178
  next unless SUPPORTED_NAMED_POSITIONS.include?(named_offset)
179
179
 
180
- @mapped_topics[name][partition] = -1 if named_offset == 'latest'
181
- @mapped_topics[name][partition] = -2 if named_offset == 'earliest'
180
+ @mapped_topics[name][partition] = -1 if named_offset == "latest"
181
+ @mapped_topics[name][partition] = -2 if named_offset == "earliest"
182
182
  end
183
183
  end
184
184
  end
@@ -54,7 +54,7 @@ module Karafka
54
54
  # so we don't stop polling data even when reaching the end (end on a given moment)
55
55
  def initialize(
56
56
  topics,
57
- settings: { 'auto.offset.reset': 'beginning' },
57
+ settings: { "auto.offset.reset": "beginning" },
58
58
  yield_nil: false,
59
59
  max_wait_time: 200
60
60
  )
@@ -37,6 +37,7 @@ module Karafka
37
37
  encryption/setup/config
38
38
  encryption/contracts/config
39
39
  encryption/messages/parser
40
+ cli/topics
40
41
  ].freeze
41
42
 
42
43
  # Zeitwerk pro loader
@@ -50,7 +51,7 @@ module Karafka
50
51
  def require_all
51
52
  FORCE_LOADED.each { |file| require_relative(file) }
52
53
 
53
- PRO_LOADER.push_dir(Karafka.core_root.join('pro'), namespace: Karafka::Pro)
54
+ PRO_LOADER.push_dir(Karafka.core_root.join("pro"), namespace: Karafka::Pro)
54
55
  PRO_LOADER.setup
55
56
  PRO_LOADER.eager_load
56
57
  end
@@ -36,7 +36,7 @@ module Karafka
36
36
  tracker = Tracker.new(
37
37
  adi_config.safety_margin,
38
38
  coordinator.last_polled_at,
39
- topic.subscription_group.kafka.fetch(:'max.poll.interval.ms')
39
+ topic.subscription_group.kafka.fetch(:"max.poll.interval.ms")
40
40
  )
41
41
 
42
42
  messages.each(*args) do |message|
@@ -91,17 +91,27 @@ module Karafka
91
91
  @offsets_metadata[offset] = offset_metadata
92
92
  @current_offset_metadata = offset_metadata
93
93
 
94
- group = @groups.find { |reg_group| reg_group.include?(offset) }
94
+ group = nil
95
+ position = nil
96
+
97
+ @groups.each do |reg_group|
98
+ pos = reg_group.index(offset)
99
+
100
+ if pos
101
+ group = reg_group
102
+ position = pos
103
+ break
104
+ end
105
+ end
95
106
 
96
107
  # This case can happen when someone uses MoM and wants to mark message from a previous
97
108
  # batch as consumed. We can add it, since the real offset refresh will point to it
98
109
  unless group
99
110
  group = [offset]
111
+ position = 0
100
112
  @groups << group
101
113
  end
102
114
 
103
- position = group.index(offset)
104
-
105
115
  # Mark all previous messages from the same group also as virtually consumed
106
116
  group[0..position].each do |markable_offset|
107
117
  # Set previous messages metadata offset as the offset of higher one for overwrites
@@ -135,7 +145,7 @@ module Karafka
135
145
 
136
146
  # @return [Array<Integer>] Offsets of messages already marked as consumed virtually
137
147
  def marked
138
- @marked.select { |_, status| status }.map(&:first).sort
148
+ @marked.select { |_, status| status }.map { |offset, _| offset }.sort
139
149
  end
140
150
 
141
151
  # Is there a real offset we can mark as consumed
@@ -150,13 +160,13 @@ module Karafka
150
160
  raise Errors::InvalidRealOffsetUsageError unless markable?
151
161
 
152
162
  offset_metadata = case @offset_metadata_strategy
153
- when :exact
154
- @offsets_metadata.fetch(@real_offset)
155
- when :current
156
- @current_offset_metadata
157
- else
158
- raise Errors::UnsupportedCaseError, @offset_metadata_strategy
159
- end
163
+ when :exact
164
+ @offsets_metadata.fetch(@real_offset)
165
+ when :current
166
+ @current_offset_metadata
167
+ else
168
+ raise Errors::UnsupportedCaseError, @offset_metadata_strategy
169
+ end
160
170
 
161
171
  [
162
172
  Messages::Seek.new(
@@ -171,11 +181,11 @@ module Karafka
171
181
  private
172
182
 
173
183
  # Recomputes the biggest possible real offset we can have.
174
- # It picks the the biggest offset that has uninterrupted stream of virtually marked as
184
+ # It picks the biggest offset that has uninterrupted stream of virtually marked as
175
185
  # consumed because this will be the collective offset.
176
186
  def materialize_real_offset
177
- @marked.to_a.sort_by(&:first).each do |offset, marked|
178
- break unless marked
187
+ @marked.keys.sort.each do |offset|
188
+ break unless @marked[offset]
179
189
 
180
190
  @real_offset = offset
181
191
  end
@@ -46,7 +46,7 @@ module Karafka
46
46
  # @param messages [Array<Karafka::Messages::Message>] array with messages. Please keep
47
47
  # in mind, this may already be partial due to execution of previous filters.
48
48
  def apply!(messages)
49
- raise NotImplementedError, 'Implement in a subclass'
49
+ raise NotImplementedError, "Implement in a subclass"
50
50
  end
51
51
 
52
52
  # @return [Symbol] filter post-execution action on consumer. Either `:skip`, `:pause` or
@@ -63,14 +63,14 @@ module Karafka
63
63
 
64
64
  timeout = (@delay / 1_000.0) - (::Time.now.utc - @cursor.timestamp)
65
65
 
66
- timeout <= 0 ? 0 : timeout * 1_000
66
+ (timeout <= 0) ? 0 : timeout * 1_000
67
67
  end
68
68
 
69
69
  # @return [Symbol] action to take on post-filtering
70
70
  def action
71
71
  return :skip unless applied?
72
72
 
73
- timeout <= 0 ? :seek : :pause
73
+ (timeout <= 0) ? :seek : :pause
74
74
  end
75
75
  end
76
76
  end
@@ -73,7 +73,7 @@ module Karafka
73
73
 
74
74
  # @return [Integer, nil] ms timeout in case of pause or nil if not delaying
75
75
  def timeout
76
- @cursor && applied? ? PAUSE_TIMEOUT : nil
76
+ (@cursor && applied?) ? PAUSE_TIMEOUT : nil
77
77
  end
78
78
 
79
79
  # Pause when we had to back-off or skip if delay is not needed
@@ -45,7 +45,7 @@ module Karafka
45
45
  Fetcher.find(topic, partition, cache: cache)
46
46
  end
47
47
 
48
- alias committed_offset_metadata offset_metadata
48
+ alias_method :committed_offset_metadata, :offset_metadata
49
49
  end
50
50
  end
51
51
  end
@@ -47,16 +47,16 @@ module Karafka
47
47
  # @return [String, Numeric] segment assignment key
48
48
  def partition(message)
49
49
  @partitioner.call(message)
50
- rescue StandardError => e
50
+ rescue => e
51
51
  # This should not happen. If you are seeing this it means your partitioner code
52
52
  # failed and raised an error. We highly recommend mitigating partitioner level errors
53
53
  # on the user side because this type of collapse should be considered a last resort
54
54
  Karafka.monitor.instrument(
55
- 'error.occurred',
55
+ "error.occurred",
56
56
  caller: self,
57
57
  error: e,
58
58
  message: message,
59
- type: 'parallel_segments.partitioner.error'
59
+ type: "parallel_segments.partitioner.error"
60
60
  )
61
61
 
62
62
  :failure
@@ -70,14 +70,14 @@ module Karafka
70
70
  return 0 if message_segment_key == :failure
71
71
 
72
72
  @reducer.call(message_segment_key)
73
- rescue StandardError => e
73
+ rescue => e
74
74
  # @see `#partition` method error handling doc
75
75
  Karafka.monitor.instrument(
76
- 'error.occurred',
76
+ "error.occurred",
77
77
  caller: self,
78
78
  error: e,
79
79
  message_segment_key: message_segment_key,
80
- type: 'parallel_segments.reducer.error'
80
+ type: "parallel_segments.reducer.error"
81
81
  )
82
82
 
83
83
  0