karafka 2.6.0.beta1 → 2.6.0.rc1

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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -27
  3. data/certs/expired.txt +2 -0
  4. data/config/locales/errors.yml +5 -0
  5. data/karafka.gemspec +1 -1
  6. data/lib/active_job/queue_adapters/karafka_adapter.rb +1 -1
  7. data/lib/karafka/active_job/current_attributes/loading.rb +1 -1
  8. data/lib/karafka/active_job/current_attributes.rb +4 -2
  9. data/lib/karafka/active_job/dispatcher.rb +1 -2
  10. data/lib/karafka/admin/consumer_groups.rb +8 -2
  11. data/lib/karafka/admin/replication.rb +6 -3
  12. data/lib/karafka/admin/topics.rb +32 -12
  13. data/lib/karafka/admin.rb +155 -59
  14. data/lib/karafka/app.rb +1 -1
  15. data/lib/karafka/base_consumer.rb +33 -5
  16. data/lib/karafka/cli/console.rb +0 -1
  17. data/lib/karafka/cli/help.rb +0 -1
  18. data/lib/karafka/cli/info.rb +0 -1
  19. data/lib/karafka/cli/install.rb +0 -1
  20. data/lib/karafka/cli/server.rb +0 -1
  21. data/lib/karafka/cli/swarm.rb +0 -1
  22. data/lib/karafka/cli.rb +2 -2
  23. data/lib/karafka/connection/client.rb +44 -7
  24. data/lib/karafka/connection/listener.rb +3 -3
  25. data/lib/karafka/connection/manager.rb +0 -1
  26. data/lib/karafka/connection/pauses_manager.rb +17 -0
  27. data/lib/karafka/connection/proxy.rb +45 -0
  28. data/lib/karafka/connection/status.rb +0 -1
  29. data/lib/karafka/instrumentation/assignments_tracker.rb +34 -0
  30. data/lib/karafka/instrumentation/callbacks/consumer_groups/decorator.rb +19 -0
  31. data/lib/karafka/instrumentation/callbacks/consumer_groups/error.rb +0 -1
  32. data/lib/karafka/instrumentation/callbacks/consumer_groups/statistics.rb +3 -2
  33. data/lib/karafka/instrumentation/critical_errors_listener.rb +85 -0
  34. data/lib/karafka/instrumentation/logger_listener.rb +2 -0
  35. data/lib/karafka/instrumentation/monitor.rb +2 -3
  36. data/lib/karafka/instrumentation/vendors/appsignal/errors_listener.rb +0 -1
  37. data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +0 -1
  38. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +4 -3
  39. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +0 -2
  40. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +181 -18
  41. data/lib/karafka/pro/admin/recovery/errors.rb +0 -1
  42. data/lib/karafka/pro/admin/recovery.rb +6 -4
  43. data/lib/karafka/pro/base_consumer.rb +5 -5
  44. data/lib/karafka/pro/cleaner/messages/metadata.rb +0 -1
  45. data/lib/karafka/pro/cli/parallel_segments/base.rb +1 -2
  46. data/lib/karafka/pro/connection/manager.rb +7 -4
  47. data/lib/karafka/pro/encryption/messages/middleware.rb +5 -0
  48. data/lib/karafka/pro/encryption/messages/parser.rb +4 -0
  49. data/lib/karafka/pro/instrumentation/callbacks/consumer_groups/decorator.rb +63 -0
  50. data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/compensator.rb +120 -0
  51. data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/fetcher.rb +88 -0
  52. data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/refresher.rb +185 -0
  53. data/lib/karafka/pro/instrumentation/consumer_groups/lag_compensation/registry.rb +102 -0
  54. data/lib/karafka/pro/instrumentation/performance_tracker.rb +40 -6
  55. data/lib/karafka/pro/iterator/tpl_builder.rb +27 -7
  56. data/lib/karafka/pro/iterator.rb +22 -7
  57. data/lib/karafka/pro/loader.rb +12 -0
  58. data/lib/karafka/pro/processing/consumer_groups/adaptive_iterator/consumer.rb +0 -3
  59. data/lib/karafka/pro/processing/consumer_groups/adaptive_iterator/tracker.rb +2 -4
  60. data/lib/karafka/pro/processing/consumer_groups/collapser.rb +6 -9
  61. data/lib/karafka/pro/processing/consumer_groups/coordinator.rb +1 -4
  62. data/lib/karafka/pro/processing/consumer_groups/coordinators/errors_tracker.rb +0 -3
  63. data/lib/karafka/pro/processing/consumer_groups/coordinators/filters_applier.rb +21 -10
  64. data/lib/karafka/pro/processing/consumer_groups/coordinators/virtual_offset_manager.rb +4 -4
  65. data/lib/karafka/pro/processing/consumer_groups/expansions_selector.rb +0 -3
  66. data/lib/karafka/pro/processing/consumer_groups/filters/actions.rb +84 -0
  67. data/lib/karafka/pro/processing/consumer_groups/filters/base.rb +5 -6
  68. data/lib/karafka/pro/processing/consumer_groups/filters/delayer.rb +2 -5
  69. data/lib/karafka/pro/processing/consumer_groups/filters/expirer.rb +1 -4
  70. data/lib/karafka/pro/processing/consumer_groups/filters/inline_insights_delayer.rb +1 -4
  71. data/lib/karafka/pro/processing/consumer_groups/filters/throttler.rb +2 -5
  72. data/lib/karafka/pro/processing/consumer_groups/filters/virtual_limiter.rb +0 -3
  73. data/lib/karafka/pro/processing/consumer_groups/jobs/consume_non_blocking.rb +2 -3
  74. data/lib/karafka/pro/processing/consumer_groups/jobs/eofed_non_blocking.rb +0 -2
  75. data/lib/karafka/pro/processing/consumer_groups/jobs/periodic.rb +2 -3
  76. data/lib/karafka/pro/processing/consumer_groups/jobs/periodic_non_blocking.rb +0 -2
  77. data/lib/karafka/pro/processing/consumer_groups/jobs/revoked_non_blocking.rb +0 -3
  78. data/lib/karafka/pro/processing/consumer_groups/jobs_builder.rb +5 -4
  79. data/lib/karafka/pro/processing/consumer_groups/offset_metadata/consumer.rb +3 -7
  80. data/lib/karafka/pro/processing/consumer_groups/offset_metadata/fetcher.rb +8 -10
  81. data/lib/karafka/pro/processing/consumer_groups/offset_metadata/listener.rb +0 -3
  82. data/lib/karafka/pro/processing/consumer_groups/parallel_segments/filters/base.rb +3 -5
  83. data/lib/karafka/pro/processing/consumer_groups/parallel_segments/filters/default.rb +0 -3
  84. data/lib/karafka/pro/processing/consumer_groups/parallel_segments/filters/mom.rb +0 -3
  85. data/lib/karafka/pro/processing/consumer_groups/partitioner.rb +2 -5
  86. data/lib/karafka/pro/processing/consumer_groups/periodic_job/consumer.rb +2 -5
  87. data/lib/karafka/pro/processing/consumer_groups/piping/consumer.rb +2 -5
  88. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_lrj_mom.rb +10 -9
  89. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_lrj_mom_vp.rb +11 -10
  90. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_mom.rb +0 -3
  91. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_ftr_mom_vp.rb +0 -3
  92. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_lrj_mom.rb +8 -7
  93. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_lrj_mom_vp.rb +9 -8
  94. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_mom.rb +5 -8
  95. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/dlq_mom_vp.rb +3 -6
  96. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_lrj_mom.rb +4 -7
  97. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_lrj_mom_vp.rb +6 -9
  98. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_mom.rb +3 -7
  99. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/ftr_mom_vp.rb +4 -8
  100. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/lrj_mom.rb +3 -6
  101. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/lrj_mom_vp.rb +4 -7
  102. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/mom.rb +2 -5
  103. data/lib/karafka/pro/processing/consumer_groups/strategies/aj/mom_vp.rb +3 -6
  104. data/lib/karafka/pro/processing/consumer_groups/strategies/base.rb +0 -3
  105. data/lib/karafka/pro/processing/consumer_groups/strategies/default.rb +56 -33
  106. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/default.rb +27 -14
  107. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr.rb +0 -3
  108. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj.rb +4 -7
  109. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj_mom.rb +5 -8
  110. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj_mom_vp.rb +5 -8
  111. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_lrj_vp.rb +3 -7
  112. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_mom.rb +0 -3
  113. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_mom_vp.rb +0 -3
  114. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/ftr_vp.rb +0 -3
  115. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj.rb +2 -5
  116. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj_mom.rb +0 -3
  117. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj_mom_vp.rb +0 -3
  118. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/lrj_vp.rb +3 -6
  119. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/mom.rb +0 -3
  120. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/mom_vp.rb +3 -6
  121. data/lib/karafka/pro/processing/consumer_groups/strategies/dlq/vp.rb +65 -8
  122. data/lib/karafka/pro/processing/consumer_groups/strategies/ftr/default.rb +0 -4
  123. data/lib/karafka/pro/processing/consumer_groups/strategies/ftr/vp.rb +2 -5
  124. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/default.rb +0 -3
  125. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr.rb +3 -6
  126. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr_mom.rb +4 -7
  127. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr_mom_vp.rb +4 -8
  128. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/ftr_vp.rb +3 -6
  129. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/mom.rb +2 -5
  130. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/mom_vp.rb +3 -7
  131. data/lib/karafka/pro/processing/consumer_groups/strategies/lrj/vp.rb +2 -5
  132. data/lib/karafka/pro/processing/consumer_groups/strategies/mom/default.rb +0 -3
  133. data/lib/karafka/pro/processing/consumer_groups/strategies/mom/ftr.rb +0 -3
  134. data/lib/karafka/pro/processing/consumer_groups/strategies/mom/ftr_vp.rb +0 -3
  135. data/lib/karafka/pro/processing/consumer_groups/strategies/mom/vp.rb +0 -3
  136. data/lib/karafka/pro/processing/consumer_groups/strategies/vp/default.rb +11 -6
  137. data/lib/karafka/pro/processing/consumer_groups/strategy_selector.rb +0 -3
  138. data/lib/karafka/pro/processing/consumer_groups/subscription_groups_coordinator.rb +0 -3
  139. data/lib/karafka/pro/processing/consumer_groups/virtual_partitions/distributors/balanced.rb +0 -3
  140. data/lib/karafka/pro/processing/consumer_groups/virtual_partitions/distributors/base.rb +2 -4
  141. data/lib/karafka/pro/processing/consumer_groups/virtual_partitions/distributors/consistent.rb +0 -3
  142. data/lib/karafka/pro/processing/jobs_queue.rb +4 -1
  143. data/lib/karafka/pro/processing/schedulers/base.rb +4 -2
  144. data/lib/karafka/pro/processing/schedulers/default.rb +2 -2
  145. data/lib/karafka/pro/recurring_tasks/consumer.rb +9 -2
  146. data/lib/karafka/pro/recurring_tasks/contracts/config.rb +0 -1
  147. data/lib/karafka/pro/recurring_tasks/executor.rb +25 -13
  148. data/lib/karafka/pro/routing/features/consumer_groups/direct_assignments/subscription_group.rb +0 -2
  149. data/lib/karafka/pro/routing/features/consumer_groups/long_running_job.rb +0 -2
  150. data/lib/karafka/pro/routing/features/consumer_groups/multiplexing/proxy.rb +2 -2
  151. data/lib/karafka/pro/routing/features/consumer_groups/non_blocking_job.rb +0 -2
  152. data/lib/karafka/pro/routing/features/consumer_groups/offset_metadata/topic.rb +0 -6
  153. data/lib/karafka/pro/routing/features/consumer_groups/parallel_segments/consumer_group.rb +5 -4
  154. data/lib/karafka/pro/routing/features/consumer_groups/patterns/config.rb +0 -6
  155. data/lib/karafka/pro/routing/features/consumer_groups/patterns/consumer_group.rb +2 -1
  156. data/lib/karafka/pro/routing/features/consumer_groups/patterns/contracts/pattern.rb +0 -1
  157. data/lib/karafka/pro/routing/features/consumer_groups/patterns/detector.rb +2 -1
  158. data/lib/karafka/pro/routing/features/consumer_groups/patterns/pattern.rb +2 -1
  159. data/lib/karafka/pro/routing/features/consumer_groups/patterns/topics.rb +8 -8
  160. data/lib/karafka/pro/routing/features/consumer_groups/periodic_job/topic.rb +5 -5
  161. data/lib/karafka/pro/routing/features/consumer_groups/recurring_tasks/builder.rb +33 -13
  162. data/lib/karafka/pro/routing/features/consumer_groups/scheduled_messages/builder.rb +52 -28
  163. data/lib/karafka/pro/routing/features/consumer_groups/scheduled_messages/topic.rb +2 -1
  164. data/lib/karafka/pro/routing/features/consumer_groups/virtual_partitions/topic.rb +6 -5
  165. data/lib/karafka/pro/scheduled_messages/consumer.rb +7 -1
  166. data/lib/karafka/pro/scheduled_messages/schema_validator.rb +6 -1
  167. data/lib/karafka/pro/swarm/liveness_listener.rb +118 -13
  168. data/lib/karafka/pro.rb +0 -6
  169. data/lib/karafka/processing/consumer_groups/coordinator.rb +4 -5
  170. data/lib/karafka/processing/consumer_groups/coordinators_buffer.rb +6 -2
  171. data/lib/karafka/processing/consumer_groups/executor.rb +4 -4
  172. data/lib/karafka/processing/consumer_groups/executors_buffer.rb +2 -4
  173. data/lib/karafka/processing/consumer_groups/expansions_selector.rb +0 -2
  174. data/lib/karafka/processing/consumer_groups/inline_insights/consumer.rb +0 -2
  175. data/lib/karafka/processing/consumer_groups/inline_insights/listener.rb +0 -2
  176. data/lib/karafka/processing/consumer_groups/inline_insights/tracker.rb +14 -16
  177. data/lib/karafka/processing/consumer_groups/jobs/consume.rb +2 -5
  178. data/lib/karafka/processing/consumer_groups/jobs/eofed.rb +2 -3
  179. data/lib/karafka/processing/consumer_groups/jobs/idle.rb +2 -2
  180. data/lib/karafka/processing/consumer_groups/jobs/revoked.rb +2 -4
  181. data/lib/karafka/processing/consumer_groups/jobs/shutdown.rb +2 -2
  182. data/lib/karafka/processing/consumer_groups/partitioner.rb +3 -6
  183. data/lib/karafka/processing/consumer_groups/strategies/aj_dlq_mom.rb +5 -2
  184. data/lib/karafka/processing/consumer_groups/strategies/aj_mom.rb +2 -4
  185. data/lib/karafka/processing/consumer_groups/strategies/base.rb +3 -5
  186. data/lib/karafka/processing/consumer_groups/strategies/default.rb +43 -13
  187. data/lib/karafka/processing/consumer_groups/strategies/dlq.rb +5 -2
  188. data/lib/karafka/processing/consumer_groups/strategies/dlq_mom.rb +5 -2
  189. data/lib/karafka/processing/consumer_groups/strategies/mom.rb +2 -3
  190. data/lib/karafka/processing/consumer_groups/strategy_selector.rb +0 -2
  191. data/lib/karafka/processing/coordinator.rb +0 -2
  192. data/lib/karafka/processing/coordinators_buffer.rb +0 -2
  193. data/lib/karafka/processing/partitioner.rb +0 -2
  194. data/lib/karafka/processing/strategy_selector.rb +0 -2
  195. data/lib/karafka/processing/worker.rb +14 -6
  196. data/lib/karafka/processing/workers_pool.rb +21 -4
  197. data/lib/karafka/railtie.rb +0 -1
  198. data/lib/karafka/routing/contracts/routing.rb +2 -2
  199. data/lib/karafka/routing/features/consumer_groups/active_job/topic.rb +6 -5
  200. data/lib/karafka/routing/features/consumer_groups/manual_offset_management/topic.rb +2 -2
  201. data/lib/karafka/routing/features/deserializers/topic.rb +0 -1
  202. data/lib/karafka/routing/subscription_group.rb +1 -1
  203. data/lib/karafka/routing/topic.rb +4 -4
  204. data/lib/karafka/routing/topics.rb +21 -1
  205. data/lib/karafka/server.rb +9 -0
  206. data/lib/karafka/setup/config.rb +54 -10
  207. data/lib/karafka/setup/config_proxy.rb +2 -1
  208. data/lib/karafka/setup/contracts/config.rb +14 -0
  209. data/lib/karafka/swarm/node.rb +2 -2
  210. data/lib/karafka/version.rb +1 -2
  211. data/lib/karafka.rb +2 -2
  212. metadata +12 -4
@@ -45,19 +45,26 @@ module Karafka
45
45
  private_constant :SAMPLES_COUNT
46
46
 
47
47
  # Builds up nested concurrent hash for data tracking
48
+ #
49
+ # @note Samples are scoped by subscription group id so that two groups consuming the same
50
+ # topic name keep independent measurements - and so revoking a partition in one group
51
+ # never drops samples another group is still using.
48
52
  def initialize
49
- @processing_times = Hash.new do |topics_hash, topic|
50
- topics_hash[topic] = Hash.new do |partitions_hash, partition|
51
- partitions_hash[partition] = []
53
+ @processing_times = Hash.new do |groups_hash, group_id|
54
+ groups_hash[group_id] = Hash.new do |topics_hash, topic|
55
+ topics_hash[topic] = Hash.new do |partitions_hash, partition|
56
+ partitions_hash[partition] = []
57
+ end
52
58
  end
53
59
  end
54
60
  end
55
61
 
62
+ # @param group_id [String] subscription group id the topic partition belongs to
56
63
  # @param topic [String]
57
64
  # @param partition [Integer]
58
65
  # @return [Float] p95 processing time of a single message from a single topic partition
59
- def processing_time_p95(topic, partition)
60
- values = @processing_times[topic][partition]
66
+ def processing_time_p95(group_id, topic, partition)
67
+ values = @processing_times[group_id][topic][partition]
61
68
 
62
69
  return 0 if values.empty?
63
70
  return values.first if values.size == 1
@@ -71,10 +78,11 @@ module Karafka
71
78
  def on_consumer_consumed(event)
72
79
  consumer = event[:caller]
73
80
  messages = consumer.messages
81
+ group_id = consumer.topic.subscription_group.id
74
82
  topic = messages.metadata.topic
75
83
  partition = messages.metadata.partition
76
84
 
77
- samples = @processing_times[topic][partition]
85
+ samples = @processing_times[group_id][topic][partition]
78
86
  samples << (event[:time] / messages.size)
79
87
 
80
88
  return unless samples.size > SAMPLES_COUNT
@@ -82,6 +90,32 @@ module Karafka
82
90
  samples.shift
83
91
  end
84
92
 
93
+ # @param event [Karafka::Core::Monitoring::Event] rebalance revoked event details
94
+ # Evicts the processing time samples of revoked partitions so the tracker does not retain
95
+ # them for the whole process lifetime. Without this every (topic, partition) ever consumed
96
+ # keeps its samples array forever - unbounded under regex pattern subscriptions that keep
97
+ # discovering new topic names. Mirrors the offset metadata fetcher, which also clears on
98
+ # revoke. Samples are scoped by subscription group, so we only ever evict the revoked
99
+ # group's data and never another group consuming the same topic.
100
+ def on_rebalance_partitions_revoked(event)
101
+ group_id = event[:subscription_group_id]
102
+
103
+ # Do not auto-vivify a branch for a group we have never tracked
104
+ return unless @processing_times.key?(group_id)
105
+
106
+ group_times = @processing_times[group_id]
107
+
108
+ event[:tpl].to_h.each do |topic, partitions|
109
+ next unless group_times.key?(topic)
110
+
111
+ topic_times = group_times[topic]
112
+ partitions.each { |partition| topic_times.delete(partition.partition) }
113
+ group_times.delete(topic) if topic_times.empty?
114
+ end
115
+
116
+ @processing_times.delete(group_id) if group_times.empty?
117
+ end
118
+
85
119
  private
86
120
 
87
121
  # Computers the requested percentile out of provided values
@@ -129,7 +129,7 @@ module Karafka
129
129
  # librdkafka's per-partition metadata cache for all integer-offset partitions in
130
130
  # one roundtrip. Without this pre-fetch, librdkafka triggers a much broader
131
131
  # metadata refresh when assign is called later, adding at least a second to
132
- # iterator startup noticeable in latency-sensitive contexts like Puma.
132
+ # iterator startup - noticeable in latency-sensitive contexts like Puma.
133
133
  # The epoch timestamp ensures every partition returns its LWM; the result is
134
134
  # intentionally discarded since we only need the warm-up side effect here.
135
135
  warm_up_tpl = Rdkafka::Consumer::TopicPartitionList.new
@@ -142,16 +142,36 @@ module Karafka
142
142
 
143
143
  return if negative_partitions.empty?
144
144
 
145
- # Batch-fetch LWM and HWM for all negative-offset partitions in two admin calls
146
- # (one for :earliest, one for :latest) instead of N per-partition consumer calls
147
- watermarks = Karafka::Admin::Topics.read_watermark_offsets(
148
- negative_partitions.transform_values(&:keys)
149
- )
145
+ # Batch-fetch LWM and HWM for all negative-offset partitions in two calls (one for
146
+ # :earliest, one for :latest) instead of N per-partition watermark queries. Both run
147
+ # on the consumer own connection, so no dedicated admin client is ever created. The
148
+ # proxy passes no isolation level, so :latest resolves with the librdkafka default
149
+ # (read_uncommitted), i.e. the true high watermark - same as the previous
150
+ # read_watermark_offsets path this replaced
151
+ low_specs = {}
152
+ high_specs = {}
153
+
154
+ negative_partitions.each do |name, partitions|
155
+ low_specs[name] = partitions.keys.map { |p| { partition: p, offset: :earliest } }
156
+ high_specs[name] = partitions.keys.map { |p| { partition: p, offset: :latest } }
157
+ end
158
+
159
+ lows = {}
160
+ highs = {}
161
+
162
+ @consumer.read_partition_offsets(low_specs).each do |result|
163
+ (lows[result[:topic]] ||= {})[result[:partition]] = result[:offset]
164
+ end
165
+
166
+ @consumer.read_partition_offsets(high_specs).each do |result|
167
+ (highs[result[:topic]] ||= {})[result[:partition]] = result[:offset]
168
+ end
150
169
 
151
170
  negative_partitions.each do |name, partitions|
152
171
  partitions.each do |partition, offset|
172
+ low = lows.dig(name, partition) || 0
173
+ high = highs.dig(name, partition) || 0
153
174
  # We add because this offset is negative
154
- low, high = watermarks.dig(name, partition) || [0, 0]
155
175
  @mapped_topics[name][partition] = [high + offset, low].max
156
176
  end
157
177
  end
@@ -74,7 +74,12 @@ module Karafka
74
74
 
75
75
  @total_partitions = @topics_with_partitions.map(&:last).sum(&:count)
76
76
 
77
- @stopped_partitions = 0
77
+ # Set of [topic, partition] that have reached EOF or were explicitly stopped. We track
78
+ # identities instead of counting EOF events because librdkafka re-emits EOF for the same
79
+ # partition every time it reaches the end again after new data arrived. Counting events
80
+ # would let a single live partition exhaust the whole EOF budget and terminate iteration
81
+ # while other partitions still had a backlog.
82
+ @stopped_partitions = Set.new
78
83
 
79
84
  @settings = settings
80
85
  @yield_nil = yield_nil
@@ -115,9 +120,14 @@ module Karafka
115
120
  @current_message = nil
116
121
  @current_consumer = nil
117
122
  end
118
-
119
- # Reset so we can use the same iterator again if needed
120
- @stopped_partitions = 0
123
+ ensure
124
+ # Reset so we can use the same iterator again if needed, regardless of how the previous run
125
+ # ended: normal completion, `#stop`, or a `break` out of the yielded block. A `break` is a
126
+ # non-local return out of `#each`; placed after the loop this reset would be skipped on a
127
+ # break, leaving `@stopped_partitions` full so the next run's `done?` is immediately true
128
+ # and the whole iteration becomes a silent no-op.
129
+ @stopped_partitions = Set.new
130
+ @stopped = false
121
131
  end
122
132
 
123
133
  # Stops the partition we're currently yielded into
@@ -138,7 +148,7 @@ module Karafka
138
148
  # @param name [String] topic name of which partition we want to stop
139
149
  # @param partition [Integer] partition we want to stop processing
140
150
  def stop_partition(name, partition)
141
- @stopped_partitions += 1
151
+ @stopped_partitions << [name, partition]
142
152
 
143
153
  @current_consumer.pause(
144
154
  Rdkafka::Consumer::TopicPartitionList.new(
@@ -178,7 +188,12 @@ module Karafka
178
188
  rescue Rdkafka::RdkafkaError => e
179
189
  # End of partition
180
190
  if e.code == :partition_eof
181
- @stopped_partitions += 1
191
+ # The error details carry the topic and partition of the EOF-ed partition. We track
192
+ # them instead of counting events: librdkafka re-emits EOF for the same partition
193
+ # whenever it reaches the end again after new data arrived, and an explicitly stopped
194
+ # partition can also EOF - either would inflate a plain counter and end the iteration
195
+ # before other partitions were fully consumed
196
+ @stopped_partitions << [e.details[:topic], e.details[:partition]]
182
197
 
183
198
  retry
184
199
  end
@@ -201,7 +216,7 @@ module Karafka
201
216
  # Do we have all the data we wanted or did every topic partition has reached eof.
202
217
  # @return [Boolean]
203
218
  def done?
204
- (@stopped_partitions >= @total_partitions) || @stopped
219
+ (@stopped_partitions.size >= @total_partitions) || @stopped
205
220
  end
206
221
  end
207
222
  end
@@ -84,6 +84,14 @@ module Karafka
84
84
 
85
85
  # We initialize it here so we don't initialize it during multi-threading work
86
86
  Processing::ConsumerGroups::SubscriptionGroupsCoordinator.instance
87
+
88
+ # Subscribe the paused partitions lags refresher only when the feature is enabled.
89
+ # It is checked post setup as the interval is user-configurable during setup
90
+ unless config.internal.statistics.consumer_groups.lag_compensation.interval.zero?
91
+ config.monitor.subscribe(
92
+ Instrumentation::ConsumerGroups::LagCompensation::Refresher.new
93
+ )
94
+ end
87
95
  end
88
96
 
89
97
  # Runs operations needed after fork in swarm for features that need it
@@ -135,6 +143,10 @@ module Karafka
135
143
  icfg.active_job.dispatcher = ActiveJob::Dispatcher.new
136
144
  icfg.active_job.job_options_contract = ActiveJob::JobOptionsContract.new
137
145
 
146
+ # The decorator is swapped unconditionally as it is a pass-through when the paused
147
+ # partitions lags refreshing is disabled or when there is no refreshed data
148
+ icfg.statistics.consumer_groups.decorator_class = Instrumentation::Callbacks::ConsumerGroups::Decorator
149
+
138
150
  config.monitor.subscribe(Instrumentation::PerformanceTracker.instance)
139
151
  end
140
152
 
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  # Namespace for adaptive iterator consumer components
39
36
  module AdaptiveIterator
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module AdaptiveIterator
39
36
  # Tracker is responsible for monitoring the processing of messages within the poll
@@ -45,7 +42,8 @@ module Karafka
45
42
 
46
43
  # Initializes a new Tracker instance.
47
44
  #
48
- # @param safety_margin [Float] The safety margin percentage (0-100) to leave as a buffer.
45
+ # @param safety_margin [Float] The safety margin percentage (0-100) to leave as a
46
+ # buffer.
49
47
  # @param last_polled_at [Float] The timestamp of the last polling in milliseconds.
50
48
  # @param max_poll_interval_ms [Integer] The maximum poll interval time in milliseconds.
51
49
  def initialize(
@@ -31,16 +31,13 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  # Manages the collapse of virtual partitions
39
36
  # Since any non-virtual partition is actually a virtual partition of size one, we can use
40
37
  # it in a generic manner without having to distinguish between those cases.
41
38
  #
42
- # We need to have notion of the offset until we want to collapse because upon pause and retry
43
- # rdkafka may purge the buffer. This means, that we may end up with smaller or bigger
39
+ # We need to have notion of the offset until we want to collapse because upon pause and
40
+ # retry rdkafka may purge the buffer. This means, that we may end up with smaller or bigger
44
41
  # (different) dataset and without tracking the end of collapse, there would be a chance for
45
42
  # things to flicker. Tracking allows us to ensure, that collapse is happening until all the
46
43
  # messages from the corrupted batch are processed.
@@ -57,8 +54,8 @@ module Karafka
57
54
  @collapsed
58
55
  end
59
56
 
60
- # Collapse until given offset. Until given offset is encountered or offset bigger than that
61
- # we keep collapsing.
57
+ # Collapse until given offset. Until given offset is encountered or offset bigger than
58
+ # that we keep collapsing.
62
59
  # @param offset [Integer] offset until which we keep the collapse
63
60
  def collapse_until!(offset)
64
61
  @mutex.synchronize do
@@ -69,8 +66,8 @@ module Karafka
69
66
  end
70
67
  end
71
68
 
72
- # Sets the collapse state based on the first collective offset that we are going to process
73
- # and makes the decision whether or not we need to still keep the collapse.
69
+ # Sets the collapse state based on the first collective offset that we are going to
70
+ # process and makes the decision whether or not we need to still keep the collapse.
74
71
  # @param first_offset [Integer] first offset from a collective batch
75
72
  def refresh!(first_offset)
76
73
  @mutex.synchronize do
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  # Pro coordinator that provides extra orchestration methods useful for parallel processing
39
36
  # within the same partition
@@ -95,7 +92,7 @@ module Karafka
95
92
  # need to make sure we collect them all. Under collapse when we reference a given
96
93
  # consumer we should be able to get all the errors and not just first/last.
97
94
  #
98
- # @note We use zero as the attempt mark because we are not "yet" in the attempt 1
95
+ # We use zero as the attempt mark because we are not "yet" in the attempt 1
99
96
  @errors_tracker.clear if attempt.zero?
100
97
  @executed.clear
101
98
 
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  # Namespace for Pro coordinator related sub-components
39
36
  module Coordinators
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Coordinators
39
36
  # Applier for all filters we want to have. Whether related to limiting messages based
@@ -43,6 +40,10 @@ module Karafka
43
40
  # This means that this is the API we expose as a single filter, allowing us to control
44
41
  # the filtering via many filters easily.
45
42
  class FiltersApplier
43
+ # Provides `#skip?`, `#pause?` and `#seek?` predicates built on top of `#action`, the
44
+ # same API individual filters expose
45
+ include Filters::Actions
46
+
46
47
  # @return [Array] registered filters array. Useful if we want to inject internal context
47
48
  # aware filters.
48
49
  attr_reader :filters
@@ -79,25 +80,35 @@ module Karafka
79
80
  !applied.empty?
80
81
  end
81
82
 
82
- # @return [Symbol] consumer post-filtering action that should be taken
83
+ # @return [Symbol] consumer post-filtering action that should be taken. One of
84
+ # {Filters::Actions::ALL}.
83
85
  def action
84
- return :skip unless applied?
86
+ return Filters::Actions.skip unless applied?
85
87
 
86
88
  # The highest priority is on a potential backoff from any of the filters because it is
87
89
  # the less risky (delay and continue later)
88
- return :pause if applied.any? { |filter| filter.action == :pause }
90
+ return Filters::Actions.pause if applied.any?(&:pause?)
89
91
 
90
92
  # If none of the filters wanted to pause, we can check for any that would want to seek
91
93
  # and if there is any, we can go with this strategy
92
- return :seek if applied.any? { |filter| filter.action == :seek }
94
+ return Filters::Actions.seek if applied.any?(&:seek?)
93
95
 
94
- :skip
96
+ Filters::Actions.skip
95
97
  end
96
98
 
97
99
  # @return [Integer] minimum timeout we need to pause. This is the minimum for all the
98
- # filters to satisfy all of them.
100
+ # pausing filters to satisfy all of them: we pause for the shortest requested backoff,
101
+ # re-poll and re-apply, and any filter that still needs more time pauses again. We
102
+ # consider only filters that actually resolved to `:pause`. Per the filter contract
103
+ # non-pausing filters should report a `nil` timeout, but the built-in
104
+ # throttler/delayer report `0` when seeking; including such a filter let that `0`
105
+ # collapse the pause to `0`, which expires immediately and busy-spins instead of
106
+ # backing off.
99
107
  def timeout
100
- applied.filter_map(&:timeout).min || 0
108
+ applied
109
+ .select(&:pause?)
110
+ .filter_map(&:timeout)
111
+ .min || 0
101
112
  end
102
113
 
103
114
  # The first message we do need to get next time we poll. We use the minimum not to jump
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Coordinators
39
36
  # Manager that keeps track of our offsets with the virtualization layer that are local
@@ -173,7 +170,10 @@ module Karafka
173
170
 
174
171
  offset_metadata = case @offset_metadata_strategy
175
172
  when :exact
176
- @offsets_metadata.fetch(@real_offset)
173
+ # When the lowest-offset group is unmarked, the materialized real offset falls back
174
+ # to `min - 1` (see #materialize_real_offset), an offset that was never registered
175
+ # and thus has no metadata. Fall back to the current metadata instead of raising.
176
+ @offsets_metadata.fetch(@real_offset, @current_offset_metadata)
177
177
  when :current
178
178
  @current_offset_metadata
179
179
  else
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  # Pro selector of appropriate topic setup based features enhancements.
39
36
  class ExpansionsSelector < Karafka::Processing::ConsumerGroups::ExpansionsSelector
@@ -0,0 +1,84 @@
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
+ # The author retains all right, title, and interest in this software,
10
+ # including all copyrights, patents, and other intellectual property rights.
11
+ # No patent rights are granted under this license.
12
+ #
13
+ # PROHIBITED:
14
+ # - Use without a valid commercial license
15
+ # - Redistribution, modification, or derivative works without authorization
16
+ # - Reverse engineering, decompilation, or disassembly of this software
17
+ # - Use as training data for AI/ML models or inclusion in datasets
18
+ # - Scraping, crawling, or automated collection for any purpose
19
+ #
20
+ # PERMITTED:
21
+ # - Reading, referencing, and linking for personal or commercial use
22
+ # - Runtime retrieval by AI assistants, coding agents, and RAG systems
23
+ # for the purpose of providing contextual help to Karafka users
24
+ #
25
+ # Receipt, viewing, or possession of this software does not convey or
26
+ # imply any license or right beyond those expressly stated above.
27
+ #
28
+ # License: https://karafka.io/docs/Pro-License-Comm/
29
+ # Contact: contact@karafka.io
30
+
31
+ module Karafka
32
+ module Pro
33
+ module Processing
34
+ module ConsumerGroups
35
+ module Filters
36
+ # Single source of truth for the post-filtering actions. Exposes each action as a method
37
+ # (so filters and internal code can return `Actions.pause` instead of the raw `:pause`
38
+ # symbol), the full `ALL` list, and the `#skip?` / `#pause?` / `#seek?` predicate helpers
39
+ # built on top of `#action`. The predicates are mixed into both individual filters
40
+ # (`Filters::Base`) and the aggregating `Coordinators::FiltersApplier`, so both expose the
41
+ # same API instead of comparing the `#action` symbol directly. The returned values are the
42
+ # plain symbols, so filters that still return `:skip`/`:pause`/`:seek` directly keep
43
+ # working.
44
+ module Actions
45
+ class << self
46
+ # @return [Symbol] the filter did not alter the consumption flow
47
+ def skip
48
+ :skip
49
+ end
50
+
51
+ # @return [Symbol] back off the partition and continue later
52
+ def pause
53
+ :pause
54
+ end
55
+
56
+ # @return [Symbol] move the partition offset without pausing
57
+ def seek
58
+ :seek
59
+ end
60
+ end
61
+
62
+ # All the actions a filter's `#action` (or the aggregated applier `#action`) can return
63
+ ALL = [skip, pause, seek].freeze
64
+
65
+ # @return [Boolean] should we skip without pausing or seeking
66
+ def skip?
67
+ action == Actions.skip
68
+ end
69
+
70
+ # @return [Boolean] should we pause the partition
71
+ def pause?
72
+ action == Actions.pause
73
+ end
74
+
75
+ # @return [Boolean] should we seek the partition
76
+ def seek?
77
+ action == Actions.seek
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Filters
39
36
  # Base for all the filters. All filters (including custom) need to use this API.
@@ -47,6 +44,8 @@ module Karafka
47
44
  attr_reader :cursor
48
45
 
49
46
  include Karafka::Core::Helpers::Time
47
+ # Provides `#skip?`, `#pause?` and `#seek?` predicates built on top of `#action`
48
+ include Actions
50
49
 
51
50
  # Initializes the filter as not yet applied
52
51
  def initialize
@@ -60,10 +59,10 @@ module Karafka
60
59
  raise NotImplementedError, "Implement in a subclass"
61
60
  end
62
61
 
63
- # @return [Symbol] filter post-execution action on consumer. Either `:skip`, `:pause` or
64
- # `:seek`.
62
+ # @return [Symbol] filter post-execution action on consumer. One of {Actions::ALL}
63
+ # (`Actions.skip`, `Actions.pause` or `Actions.seek`).
65
64
  def action
66
- :skip
65
+ Actions.skip
67
66
  end
68
67
 
69
68
  # @return [Boolean] did this filter change messages in any way
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Filters
39
36
  # A filter that allows us to delay processing by pausing until time is right.
@@ -80,9 +77,9 @@ module Karafka
80
77
 
81
78
  # @return [Symbol] action to take on post-filtering
82
79
  def action
83
- return :skip unless applied?
80
+ return Actions.skip unless applied?
84
81
 
85
- (timeout <= 0) ? :seek : :pause
82
+ (timeout <= 0) ? Actions.seek : Actions.pause
86
83
  end
87
84
  end
88
85
  end
@@ -31,14 +31,11 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Filters
39
36
  # Expirer for removing too old messages.
40
37
  # It never moves offsets in any way and does not impact the processing flow. It always
41
- # runs `:skip` action.
38
+ # runs the `Actions.skip` action.
42
39
  class Expirer < Base
43
40
  # @param ttl [Integer] maximum age of a message (in ms)
44
41
  def initialize(ttl)
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Filters
39
36
  # Delayer that checks if we have appropriate insights available. If not, pauses for
@@ -89,7 +86,7 @@ module Karafka
89
86
 
90
87
  # Pause when we had to back-off or skip if delay is not needed
91
88
  def action
92
- applied? ? :pause : :skip
89
+ applied? ? Actions.pause : Actions.skip
93
90
  end
94
91
  end
95
92
  end
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  # Namespace containing Pro out of the box filters used by various strategies
39
36
  module Filters
@@ -86,9 +83,9 @@ module Karafka
86
83
  # @return [Symbol] action to take upon throttler reaching certain state
87
84
  def action
88
85
  if applied?
89
- timeout.zero? ? :seek : :pause
86
+ timeout.zero? ? Actions.seek : Actions.pause
90
87
  else
91
- :skip
88
+ Actions.skip
92
89
  end
93
90
  end
94
91
 
@@ -31,9 +31,6 @@
31
31
  module Karafka
32
32
  module Pro
33
33
  module Processing
34
- # Consumer-group-specific Pro processing components (driven by rebalance callbacks and
35
- # partition ticks). Parallel `ShareGroups` will live next to this namespace once KIP-932
36
- # lands.
37
34
  module ConsumerGroups
38
35
  module Filters
39
36
  # Removes messages that are already marked as consumed in the virtual offset manager
@@ -32,9 +32,7 @@ module Karafka
32
32
  module Pro
33
33
  # Pro components related to processing part of Karafka
34
34
  module Processing
35
- # Pro consumer-group-specific processing components
36
35
  module ConsumerGroups
37
- # Pro jobs
38
36
  module Jobs
39
37
  # The main job type in a non-blocking variant.
40
38
  # This variant works "like" the regular consumption but does not block the queue.
@@ -43,7 +41,8 @@ module Karafka
43
41
  # if would block.
44
42
  #
45
43
  # @note It needs to be working with a proper consumer that will handle the partition
46
- # management. This layer of the framework knows nothing about Kafka messages consumption.
44
+ # management. This layer of the framework knows nothing about Kafka messages
45
+ # consumption.
47
46
  class ConsumeNonBlocking < Karafka::Processing::ConsumerGroups::Jobs::Consume
48
47
  self.action = :consume
49
48