karafka 2.0.0 → 2.1.0

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 (246) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/workflows/ci.yml +40 -16
  5. data/.rspec +4 -0
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +459 -796
  8. data/Gemfile +3 -2
  9. data/Gemfile.lock +44 -32
  10. data/LICENSE +1 -1
  11. data/README.md +22 -17
  12. data/bin/integrations +86 -26
  13. data/bin/karafka +1 -13
  14. data/bin/rspecs +6 -0
  15. data/bin/verify_license_integrity +37 -0
  16. data/certs/cert_chain.pem +26 -0
  17. data/config/{errors.yml → locales/errors.yml} +34 -6
  18. data/config/locales/pro_errors.yml +34 -0
  19. data/docker-compose.yml +4 -30
  20. data/karafka.gemspec +17 -10
  21. data/lib/active_job/karafka.rb +3 -7
  22. data/lib/active_job/queue_adapters/karafka_adapter.rb +7 -1
  23. data/lib/karafka/active_job/consumer.rb +29 -6
  24. data/lib/karafka/active_job/current_attributes/loading.rb +36 -0
  25. data/lib/karafka/active_job/current_attributes/persistence.rb +28 -0
  26. data/lib/karafka/active_job/current_attributes.rb +42 -0
  27. data/lib/karafka/active_job/dispatcher.rb +34 -3
  28. data/lib/karafka/active_job/job_options_contract.rb +13 -2
  29. data/lib/karafka/admin.rb +246 -0
  30. data/lib/karafka/app.rb +30 -7
  31. data/lib/karafka/base_consumer.rb +124 -82
  32. data/lib/karafka/cli/base.rb +20 -0
  33. data/lib/karafka/cli/console.rb +13 -8
  34. data/lib/karafka/cli/info.rb +3 -2
  35. data/lib/karafka/cli/server.rb +55 -8
  36. data/lib/karafka/cli/topics.rb +146 -0
  37. data/lib/karafka/connection/client.rb +118 -61
  38. data/lib/karafka/connection/consumer_group_coordinator.rb +48 -0
  39. data/lib/karafka/connection/listener.rb +78 -30
  40. data/lib/karafka/connection/listeners_batch.rb +18 -2
  41. data/lib/karafka/connection/rebalance_manager.rb +3 -0
  42. data/lib/karafka/contracts/config.rb +1 -1
  43. data/lib/karafka/contracts/consumer_group.rb +22 -9
  44. data/lib/karafka/contracts/server_cli_options.rb +71 -11
  45. data/lib/karafka/contracts/topic.rb +65 -0
  46. data/lib/karafka/contracts.rb +1 -1
  47. data/lib/karafka/embedded.rb +36 -0
  48. data/lib/karafka/errors.rb +6 -4
  49. data/lib/karafka/helpers/colorize.rb +6 -0
  50. data/lib/karafka/instrumentation/callbacks/error.rb +3 -4
  51. data/lib/karafka/instrumentation/callbacks/statistics.rb +14 -4
  52. data/lib/karafka/instrumentation/logger_listener.rb +151 -22
  53. data/lib/karafka/instrumentation/notifications.rb +26 -7
  54. data/lib/karafka/instrumentation/proctitle_listener.rb +7 -16
  55. data/lib/karafka/instrumentation/vendors/datadog/dashboard.json +1 -1
  56. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +153 -0
  57. data/lib/karafka/instrumentation/vendors/datadog/{listener.rb → metrics_listener.rb} +62 -35
  58. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +166 -0
  59. data/lib/karafka/licenser.rb +53 -50
  60. data/lib/karafka/messages/builders/batch_metadata.rb +8 -8
  61. data/lib/karafka/messages/builders/messages.rb +6 -2
  62. data/lib/karafka/messages/message.rb +14 -2
  63. data/lib/karafka/messages/messages.rb +5 -0
  64. data/lib/karafka/messages/parser.rb +14 -0
  65. data/lib/karafka/patches/rdkafka/bindings.rb +132 -0
  66. data/lib/karafka/pro/active_job/consumer.rb +10 -9
  67. data/lib/karafka/pro/active_job/dispatcher.rb +28 -3
  68. data/lib/karafka/pro/active_job/job_options_contract.rb +16 -3
  69. data/lib/karafka/pro/encryption/cipher.rb +58 -0
  70. data/lib/karafka/pro/encryption/contracts/config.rb +79 -0
  71. data/lib/karafka/pro/encryption/errors.rb +24 -0
  72. data/lib/karafka/pro/encryption/messages/middleware.rb +46 -0
  73. data/lib/karafka/pro/encryption/messages/parser.rb +56 -0
  74. data/lib/karafka/pro/encryption/setup/config.rb +48 -0
  75. data/lib/karafka/pro/encryption.rb +47 -0
  76. data/lib/karafka/pro/iterator.rb +253 -0
  77. data/lib/karafka/pro/loader.rb +52 -26
  78. data/lib/karafka/pro/performance_tracker.rb +10 -6
  79. data/lib/karafka/pro/processing/collapser.rb +62 -0
  80. data/lib/karafka/pro/processing/coordinator.rb +92 -16
  81. data/lib/karafka/pro/processing/filters/base.rb +61 -0
  82. data/lib/karafka/pro/processing/filters/delayer.rb +70 -0
  83. data/lib/karafka/pro/processing/filters/expirer.rb +51 -0
  84. data/lib/karafka/pro/processing/filters/throttler.rb +84 -0
  85. data/lib/karafka/pro/processing/filters/virtual_limiter.rb +52 -0
  86. data/lib/karafka/pro/processing/filters_applier.rb +104 -0
  87. data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +7 -5
  88. data/lib/karafka/pro/processing/jobs/revoked_non_blocking.rb +37 -0
  89. data/lib/karafka/pro/processing/jobs_builder.rb +22 -4
  90. data/lib/karafka/pro/processing/partitioner.rb +33 -24
  91. data/lib/karafka/pro/processing/scheduler.rb +27 -8
  92. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +68 -0
  93. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +74 -0
  94. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +72 -0
  95. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +76 -0
  96. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +64 -0
  97. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +68 -0
  98. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +64 -0
  99. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +69 -0
  100. data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom.rb +38 -0
  101. data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom_vp.rb +64 -0
  102. data/lib/karafka/pro/processing/strategies/aj/ftr_mom.rb +38 -0
  103. data/lib/karafka/pro/processing/strategies/aj/ftr_mom_vp.rb +58 -0
  104. data/lib/karafka/pro/processing/strategies/aj/lrj_mom.rb +37 -0
  105. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +79 -0
  106. data/lib/karafka/pro/processing/strategies/aj/mom.rb +36 -0
  107. data/lib/karafka/pro/processing/strategies/aj/mom_vp.rb +52 -0
  108. data/lib/karafka/pro/processing/strategies/base.rb +26 -0
  109. data/lib/karafka/pro/processing/strategies/default.rb +105 -0
  110. data/lib/karafka/pro/processing/strategies/dlq/default.rb +131 -0
  111. data/lib/karafka/pro/processing/strategies/dlq/ftr.rb +61 -0
  112. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +75 -0
  113. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +71 -0
  114. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom_vp.rb +43 -0
  115. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_vp.rb +41 -0
  116. data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +69 -0
  117. data/lib/karafka/pro/processing/strategies/dlq/ftr_mom_vp.rb +41 -0
  118. data/lib/karafka/pro/processing/strategies/dlq/ftr_vp.rb +40 -0
  119. data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +63 -0
  120. data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +63 -0
  121. data/lib/karafka/pro/processing/strategies/dlq/lrj_mom_vp.rb +36 -0
  122. data/lib/karafka/pro/processing/strategies/dlq/lrj_vp.rb +39 -0
  123. data/lib/karafka/pro/processing/strategies/dlq/mom.rb +68 -0
  124. data/lib/karafka/pro/processing/strategies/dlq/mom_vp.rb +37 -0
  125. data/lib/karafka/pro/processing/strategies/dlq/vp.rb +40 -0
  126. data/lib/karafka/pro/processing/strategies/ftr/default.rb +104 -0
  127. data/lib/karafka/pro/processing/strategies/ftr/vp.rb +40 -0
  128. data/lib/karafka/pro/processing/strategies/lrj/default.rb +87 -0
  129. data/lib/karafka/pro/processing/strategies/lrj/ftr.rb +69 -0
  130. data/lib/karafka/pro/processing/strategies/lrj/ftr_mom.rb +67 -0
  131. data/lib/karafka/pro/processing/strategies/lrj/ftr_mom_vp.rb +40 -0
  132. data/lib/karafka/pro/processing/strategies/lrj/ftr_vp.rb +39 -0
  133. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +80 -0
  134. data/lib/karafka/pro/processing/strategies/lrj/mom_vp.rb +38 -0
  135. data/lib/karafka/pro/processing/strategies/lrj/vp.rb +36 -0
  136. data/lib/karafka/pro/processing/strategies/mom/default.rb +46 -0
  137. data/lib/karafka/pro/processing/strategies/mom/ftr.rb +53 -0
  138. data/lib/karafka/pro/processing/strategies/mom/ftr_vp.rb +37 -0
  139. data/lib/karafka/pro/processing/strategies/mom/vp.rb +35 -0
  140. data/lib/karafka/pro/processing/strategies/vp/default.rb +104 -0
  141. data/lib/karafka/pro/{contracts/base.rb → processing/strategies.rb} +6 -5
  142. data/lib/karafka/pro/processing/strategy_selector.rb +84 -0
  143. data/lib/karafka/pro/processing/virtual_offset_manager.rb +147 -0
  144. data/lib/karafka/pro/routing/features/base.rb +24 -0
  145. data/lib/karafka/pro/routing/features/dead_letter_queue/contract.rb +50 -0
  146. data/lib/karafka/pro/routing/features/dead_letter_queue.rb +27 -0
  147. data/lib/karafka/pro/routing/{builder_extensions.rb → features/delaying/config.rb} +8 -11
  148. data/lib/karafka/pro/routing/features/delaying/contract.rb +38 -0
  149. data/lib/karafka/pro/routing/features/delaying/topic.rb +59 -0
  150. data/lib/karafka/pro/routing/features/delaying.rb +29 -0
  151. data/lib/karafka/pro/routing/features/expiring/config.rb +27 -0
  152. data/lib/karafka/pro/routing/features/expiring/contract.rb +38 -0
  153. data/lib/karafka/pro/routing/features/expiring/topic.rb +59 -0
  154. data/lib/karafka/pro/routing/features/expiring.rb +27 -0
  155. data/lib/karafka/pro/routing/features/filtering/config.rb +40 -0
  156. data/lib/karafka/pro/routing/features/filtering/contract.rb +41 -0
  157. data/lib/karafka/pro/routing/features/filtering/topic.rb +51 -0
  158. data/lib/karafka/pro/routing/features/filtering.rb +27 -0
  159. data/lib/karafka/pro/routing/features/long_running_job/config.rb +28 -0
  160. data/lib/karafka/pro/routing/features/long_running_job/contract.rb +37 -0
  161. data/lib/karafka/pro/routing/features/long_running_job/topic.rb +42 -0
  162. data/lib/karafka/pro/routing/features/long_running_job.rb +28 -0
  163. data/lib/karafka/pro/routing/features/throttling/config.rb +32 -0
  164. data/lib/karafka/pro/routing/features/throttling/contract.rb +41 -0
  165. data/lib/karafka/pro/routing/features/throttling/topic.rb +69 -0
  166. data/lib/karafka/pro/routing/features/throttling.rb +30 -0
  167. data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +30 -0
  168. data/lib/karafka/pro/routing/features/virtual_partitions/contract.rb +52 -0
  169. data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +56 -0
  170. data/lib/karafka/pro/routing/features/virtual_partitions.rb +27 -0
  171. data/lib/karafka/process.rb +23 -8
  172. data/lib/karafka/processing/coordinator.rb +102 -23
  173. data/lib/karafka/processing/coordinators_buffer.rb +5 -1
  174. data/lib/karafka/processing/executor.rb +51 -21
  175. data/lib/karafka/processing/executors_buffer.rb +10 -26
  176. data/lib/karafka/processing/jobs/base.rb +6 -2
  177. data/lib/karafka/processing/jobs/consume.rb +8 -5
  178. data/lib/karafka/processing/jobs/idle.rb +24 -0
  179. data/lib/karafka/processing/jobs_builder.rb +2 -3
  180. data/lib/karafka/processing/jobs_queue.rb +14 -8
  181. data/lib/karafka/processing/partitioner.rb +3 -1
  182. data/lib/karafka/processing/result.rb +14 -1
  183. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +44 -0
  184. data/lib/karafka/processing/strategies/aj_mom.rb +21 -0
  185. data/lib/karafka/processing/strategies/base.rb +52 -0
  186. data/lib/karafka/processing/strategies/default.rb +142 -0
  187. data/lib/karafka/processing/strategies/dlq.rb +88 -0
  188. data/lib/karafka/processing/strategies/dlq_mom.rb +49 -0
  189. data/lib/karafka/processing/strategies/mom.rb +29 -0
  190. data/lib/karafka/processing/strategy_selector.rb +47 -0
  191. data/lib/karafka/processing/worker.rb +8 -3
  192. data/lib/karafka/railtie.rb +24 -12
  193. data/lib/karafka/routing/activity_manager.rb +84 -0
  194. data/lib/karafka/routing/builder.rb +20 -0
  195. data/lib/karafka/routing/consumer_group.rb +38 -4
  196. data/lib/karafka/routing/consumer_mapper.rb +0 -10
  197. data/lib/karafka/routing/features/active_job/builder.rb +33 -0
  198. data/lib/karafka/routing/features/active_job/config.rb +15 -0
  199. data/lib/karafka/routing/features/active_job/contract.rb +41 -0
  200. data/lib/karafka/routing/features/active_job/topic.rb +33 -0
  201. data/lib/karafka/routing/features/active_job.rb +13 -0
  202. data/lib/karafka/routing/features/base/expander.rb +53 -0
  203. data/lib/karafka/routing/features/base.rb +34 -0
  204. data/lib/karafka/routing/features/dead_letter_queue/config.rb +19 -0
  205. data/lib/karafka/routing/features/dead_letter_queue/contract.rb +42 -0
  206. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +41 -0
  207. data/lib/karafka/routing/features/dead_letter_queue.rb +16 -0
  208. data/lib/karafka/routing/features/declaratives/config.rb +18 -0
  209. data/lib/karafka/routing/features/declaratives/contract.rb +30 -0
  210. data/lib/karafka/routing/features/declaratives/topic.rb +44 -0
  211. data/lib/karafka/routing/features/declaratives.rb +14 -0
  212. data/lib/karafka/routing/features/manual_offset_management/config.rb +15 -0
  213. data/lib/karafka/routing/features/manual_offset_management/contract.rb +24 -0
  214. data/lib/karafka/routing/features/manual_offset_management/topic.rb +35 -0
  215. data/lib/karafka/routing/features/manual_offset_management.rb +18 -0
  216. data/lib/karafka/routing/proxy.rb +18 -20
  217. data/lib/karafka/routing/router.rb +27 -2
  218. data/lib/karafka/routing/subscription_group.rb +43 -5
  219. data/lib/karafka/routing/subscription_groups_builder.rb +6 -1
  220. data/lib/karafka/routing/topic.rb +17 -5
  221. data/lib/karafka/routing/topics.rb +8 -0
  222. data/lib/karafka/runner.rb +1 -0
  223. data/lib/karafka/serialization/json/deserializer.rb +1 -0
  224. data/lib/karafka/server.rb +58 -28
  225. data/lib/karafka/setup/attributes_map.rb +344 -0
  226. data/lib/karafka/setup/config.rb +48 -19
  227. data/lib/karafka/status.rb +35 -7
  228. data/lib/karafka/templates/karafka.rb.erb +14 -6
  229. data/lib/karafka/time_trackers/base.rb +1 -6
  230. data/lib/karafka/time_trackers/pause.rb +49 -19
  231. data/lib/karafka/time_trackers/poll.rb +7 -3
  232. data/lib/karafka/version.rb +1 -1
  233. data/lib/karafka.rb +56 -3
  234. data/renovate.json +6 -0
  235. data.tar.gz.sig +0 -0
  236. metadata +189 -61
  237. metadata.gz.sig +0 -0
  238. data/bin/wait_for_kafka +0 -20
  239. data/certs/mensfeld.pem +0 -25
  240. data/lib/karafka/active_job/routing/extensions.rb +0 -31
  241. data/lib/karafka/contracts/consumer_group_topic.rb +0 -42
  242. data/lib/karafka/instrumentation.rb +0 -21
  243. data/lib/karafka/pro/base_consumer.rb +0 -82
  244. data/lib/karafka/pro/contracts/consumer_group.rb +0 -34
  245. data/lib/karafka/pro/contracts/consumer_group_topic.rb +0 -33
  246. data/lib/karafka/pro/routing/topic_extensions.rb +0 -38
@@ -14,17 +14,25 @@ module Karafka
14
14
  # @note Consumer name may change in case we regenerate it
15
15
  attr_reader :name
16
16
 
17
+ # @return [String] id of the client
18
+ attr_reader :id
19
+
17
20
  # How many times should we retry polling in case of a failure
18
- MAX_POLL_RETRIES = 10
21
+ MAX_POLL_RETRIES = 20
22
+
23
+ # We want to make sure we never close several clients in the same moment to prevent
24
+ # potential race conditions and other issues
25
+ SHUTDOWN_MUTEX = Mutex.new
19
26
 
20
- private_constant :MAX_POLL_RETRIES
27
+ private_constant :MAX_POLL_RETRIES, :SHUTDOWN_MUTEX
21
28
 
22
29
  # Creates a new consumer instance.
23
30
  #
24
31
  # @param subscription_group [Karafka::Routing::SubscriptionGroup] subscription group
25
32
  # with all the configuration details needed for us to create a client
26
- # @return [Karafka::Connection::Rdk::Consumer]
33
+ # @return [Karafka::Connection::Client]
27
34
  def initialize(subscription_group)
35
+ @id = SecureRandom.hex(6)
28
36
  # Name is set when we build consumer
29
37
  @name = ''
30
38
  @mutex = Mutex.new
@@ -153,7 +161,7 @@ module Karafka
153
161
 
154
162
  pause_msg = Messages::Seek.new(topic, partition, offset)
155
163
 
156
- internal_commit_offsets(async: false)
164
+ internal_commit_offsets(async: true)
157
165
 
158
166
  # Here we do not use our cached tpls because we should not try to pause something we do
159
167
  # not own anymore.
@@ -161,6 +169,15 @@ module Karafka
161
169
 
162
170
  return unless tpl
163
171
 
172
+ Karafka.monitor.instrument(
173
+ 'client.pause',
174
+ caller: self,
175
+ subscription_group: @subscription_group,
176
+ topic: topic,
177
+ partition: partition,
178
+ offset: offset
179
+ )
180
+
164
181
  @paused_tpls[topic][partition] = tpl
165
182
 
166
183
  @kafka.pause(tpl)
@@ -179,19 +196,26 @@ module Karafka
179
196
 
180
197
  return if @closed
181
198
 
182
- # Always commit synchronously offsets if any when we resume
183
- # This prevents resuming without offset in case it would not be committed prior
184
- # We can skip performance penalty since resuming should not happen too often
185
- internal_commit_offsets(async: false)
199
+ # We now commit offsets on rebalances, thus we can do it async just to make sure
200
+ internal_commit_offsets(async: true)
186
201
 
187
202
  # If we were not able, let's try to reuse the one we have (if we have)
188
203
  tpl = topic_partition_list(topic, partition) || @paused_tpls[topic][partition]
189
204
 
190
205
  return unless tpl
206
+
191
207
  # If we did not have it, it means we never paused this partition, thus no resume should
192
208
  # happen in the first place
193
209
  return unless @paused_tpls[topic].delete(partition)
194
210
 
211
+ Karafka.monitor.instrument(
212
+ 'client.resume',
213
+ caller: self,
214
+ subscription_group: @subscription_group,
215
+ topic: topic,
216
+ partition: partition
217
+ )
218
+
195
219
  @kafka.resume(tpl)
196
220
  ensure
197
221
  @mutex.unlock
@@ -238,6 +262,17 @@ module Karafka
238
262
  end
239
263
  end
240
264
 
265
+ # Runs a single poll ignoring all the potential errors
266
+ # This is used as a keep-alive in the shutdown stage and any errors that happen here are
267
+ # irrelevant from the shutdown process perspective
268
+ #
269
+ # This is used only to trigger rebalance callbacks
270
+ def ping
271
+ poll(100)
272
+ rescue Rdkafka::RdkafkaError
273
+ nil
274
+ end
275
+
241
276
  private
242
277
 
243
278
  # When we cannot store an offset, it means we no longer own the partition
@@ -267,32 +302,41 @@ module Karafka
267
302
 
268
303
  true
269
304
  rescue Rdkafka::RdkafkaError => e
270
- return false if e.code == :assignment_lost
271
- return true if e.code == :no_offset
305
+ case e.code
306
+ when :assignment_lost
307
+ return false
308
+ when :no_offset
309
+ return true
310
+ when :coordinator_load_in_progress
311
+ sleep(1)
312
+ retry
313
+ end
272
314
 
273
315
  raise e
274
316
  end
275
317
 
276
318
  # Commits the stored offsets in a sync way and closes the consumer.
277
319
  def close
278
- # Once client is closed, we should not close it again
279
- # This could only happen in case of a race-condition when forceful shutdown happens
280
- # and triggers this from a different thread
281
- return if @closed
282
-
283
- @mutex.synchronize do
284
- internal_commit_offsets(async: false)
285
-
286
- @closed = true
287
-
288
- # Remove callbacks runners that were registered
289
- ::Karafka::Instrumentation.statistics_callbacks.delete(@subscription_group.id)
290
- ::Karafka::Instrumentation.error_callbacks.delete(@subscription_group.id)
291
-
292
- @kafka.close
293
- @buffer.clear
294
- # @note We do not clear rebalance manager here as we may still have revocation info here
295
- # that we want to consider valid prior to running another reconnection
320
+ # Allow only one client to be closed at the same time
321
+ SHUTDOWN_MUTEX.synchronize do
322
+ # Make sure that no other operations are happening on this client when we close it
323
+ @mutex.synchronize do
324
+ # Once client is closed, we should not close it again
325
+ # This could only happen in case of a race-condition when forceful shutdown happens
326
+ # and triggers this from a different thread
327
+ return if @closed
328
+
329
+ @closed = true
330
+
331
+ # Remove callbacks runners that were registered
332
+ ::Karafka::Core::Instrumentation.statistics_callbacks.delete(@subscription_group.id)
333
+ ::Karafka::Core::Instrumentation.error_callbacks.delete(@subscription_group.id)
334
+
335
+ @kafka.close
336
+ @buffer.clear
337
+ # @note We do not clear rebalance manager here as we may still have revocation info
338
+ # here that we want to consider valid prior to running another reconnection
339
+ end
296
340
  end
297
341
  end
298
342
 
@@ -310,7 +354,7 @@ module Karafka
310
354
  Rdkafka::Consumer::TopicPartitionList.new({ topic => [rdkafka_partition] })
311
355
  end
312
356
 
313
- # Performs a single poll operation.
357
+ # Performs a single poll operation and handles retries and error
314
358
  #
315
359
  # @param timeout [Integer] timeout for a single poll
316
360
  # @return [Rdkafka::Consumer::Message, nil] fetched message or nil if nothing polled
@@ -323,41 +367,56 @@ module Karafka
323
367
 
324
368
  @kafka.poll(timeout)
325
369
  rescue ::Rdkafka::RdkafkaError => e
326
- # We return nil, so we do not restart until running the whole loop
327
- # This allows us to run revocation jobs and other things and we will pick up new work
328
- # next time after dispatching all the things that are needed
329
- #
330
- # If we would retry here, the client reset would become transparent and we would not have
331
- # a chance to take any actions
370
+ early_report = false
371
+
372
+ retryable = time_poll.attempts <= MAX_POLL_RETRIES && time_poll.retryable?
373
+
374
+ # There are retryable issues on which we want to report fast as they are source of
375
+ # problems and can mean some bigger system instabilities
376
+ # Those are mainly network issues and exceeding the max poll interval
377
+ # We want to report early on max poll interval exceeding because it may mean that the
378
+ # underlying processing is taking too much time and it is not LRJ
332
379
  case e.code
333
380
  when :max_poll_exceeded # -147
334
- reset
335
- return nil
336
- when :transport # -195
337
- reset
338
- return nil
339
- when :rebalance_in_progress # -27
340
- reset
341
- return nil
342
- when :not_coordinator # 16
343
- reset
344
- return nil
381
+ early_report = true
345
382
  when :network_exception # 13
346
- reset
347
- return nil
348
- when :unknown_topic_or_part
349
- # This is expected and temporary until rdkafka catches up with metadata
350
- return nil
383
+ early_report = true
384
+ when :transport # -195
385
+ early_report = true
386
+ # @see
387
+ # https://github.com/confluentinc/confluent-kafka-dotnet/issues/1366#issuecomment-821842990
388
+ # This will be raised each time poll detects a non-existing topic. When auto creation is
389
+ # on, we can safely ignore it
390
+ when :unknown_topic_or_part # 3
391
+ return nil if @subscription_group.kafka[:'allow.auto.create.topics']
392
+
393
+ early_report = true
394
+
395
+ # No sense in retrying when no topic/partition and we're no longer running
396
+ retryable = false unless Karafka::App.running?
351
397
  end
352
398
 
353
- raise if time_poll.attempts > MAX_POLL_RETRIES
354
- raise unless time_poll.retryable?
399
+ if early_report || !retryable
400
+ Karafka.monitor.instrument(
401
+ 'error.occurred',
402
+ caller: self,
403
+ error: e,
404
+ type: 'connection.client.poll.error'
405
+ )
406
+ end
407
+
408
+ raise unless retryable
409
+
410
+ # Most of the errors can be safely ignored as librdkafka will recover from them
411
+ # @see https://github.com/edenhill/librdkafka/issues/1987#issuecomment-422008750
412
+ # @see https://github.com/edenhill/librdkafka/wiki/Error-handling
355
413
 
356
414
  time_poll.checkpoint
357
415
  time_poll.backoff
358
416
 
359
- # On unknown errors we do our best to retry and handle them before raising
360
- retry
417
+ # poll may not only return message but also can run callbacks and if they changed,
418
+ # despite the errors we need to delegate to the other app parts
419
+ @rebalance_manager.changed? ? nil : retry
361
420
  end
362
421
 
363
422
  # Builds a new rdkafka consumer instance based on the subscription group configuration
@@ -370,24 +429,22 @@ module Karafka
370
429
  @name = consumer.name
371
430
 
372
431
  # Register statistics runner for this particular type of callbacks
373
- ::Karafka::Instrumentation.statistics_callbacks.add(
432
+ ::Karafka::Core::Instrumentation.statistics_callbacks.add(
374
433
  @subscription_group.id,
375
434
  Instrumentation::Callbacks::Statistics.new(
376
435
  @subscription_group.id,
377
436
  @subscription_group.consumer_group_id,
378
- @name,
379
- ::Karafka::App.config.monitor
437
+ @name
380
438
  )
381
439
  )
382
440
 
383
441
  # Register error tracking callback
384
- ::Karafka::Instrumentation.error_callbacks.add(
442
+ ::Karafka::Core::Instrumentation.error_callbacks.add(
385
443
  @subscription_group.id,
386
444
  Instrumentation::Callbacks::Error.new(
387
445
  @subscription_group.id,
388
446
  @subscription_group.consumer_group_id,
389
- @name,
390
- ::Karafka::App.config.monitor
447
+ @name
391
448
  )
392
449
  )
393
450
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Connection
5
+ # This object represents a collective status of execution of group of listeners running inside
6
+ # of one consumer group but in separate subscription groups.
7
+ #
8
+ # There are cases when we do not want to close a given client when others from the same
9
+ # consumer group are running because it can cause instabilities due to early shutdown of some
10
+ # of the clients out of same consumer group.
11
+ #
12
+ # We also want to make sure, we close one consumer at a time while others can continue polling.
13
+ #
14
+ # This prevents a scenario, where a rebalance is not acknowledged and we loose assignment
15
+ # without having a chance to commit changes.
16
+ class ConsumerGroupCoordinator
17
+ # @param group_size [Integer] number of separate subscription groups in a consumer group
18
+ def initialize(group_size)
19
+ @shutdown_lock = Mutex.new
20
+ @group_size = group_size
21
+ @finished = Set.new
22
+ end
23
+
24
+ # @return [Boolean] true if all the subscription groups from a given consumer group are
25
+ # finished
26
+ def finished?
27
+ @finished.size == @group_size
28
+ end
29
+
30
+ # @return [Boolean] can we start shutdown on a given listener
31
+ # @note If true, will also obtain a lock so no-one else will be closing the same time we do
32
+ def shutdown?
33
+ finished? && @shutdown_lock.try_lock
34
+ end
35
+
36
+ # Unlocks the shutdown lock
37
+ def unlock
38
+ @shutdown_lock.unlock if @shutdown_lock.owned?
39
+ end
40
+
41
+ # Marks given listener as finished
42
+ # @param listener_id [String]
43
+ def finish_work(listener_id)
44
+ @finished << listener_id
45
+ end
46
+ end
47
+ end
48
+ end
@@ -14,16 +14,18 @@ module Karafka
14
14
  # @return [String] id of this listener
15
15
  attr_reader :id
16
16
 
17
+ # @param consumer_group_coordinator [Karafka::Connection::ConsumerGroupCoordinator]
17
18
  # @param subscription_group [Karafka::Routing::SubscriptionGroup]
18
19
  # @param jobs_queue [Karafka::Processing::JobsQueue] queue where we should push work
19
20
  # @return [Karafka::Connection::Listener] listener instance
20
- def initialize(subscription_group, jobs_queue)
21
+ def initialize(consumer_group_coordinator, subscription_group, jobs_queue)
21
22
  proc_config = ::Karafka::App.config.internal.processing
22
23
 
23
- @id = SecureRandom.uuid
24
+ @id = SecureRandom.hex(6)
25
+ @consumer_group_coordinator = consumer_group_coordinator
24
26
  @subscription_group = subscription_group
25
27
  @jobs_queue = jobs_queue
26
- @coordinators = Processing::CoordinatorsBuffer.new
28
+ @coordinators = Processing::CoordinatorsBuffer.new(subscription_group.topics)
27
29
  @client = Client.new(@subscription_group)
28
30
  @executors = Processing::ExecutorsBuffer.new(@client, subscription_group)
29
31
  @jobs_builder = proc_config.jobs_builder
@@ -46,8 +48,8 @@ module Karafka
46
48
  Karafka.monitor.instrument(
47
49
  'connection.listener.before_fetch_loop',
48
50
  caller: self,
49
- subscription_group: @subscription_group,
50
- client: @client
51
+ client: @client,
52
+ subscription_group: @subscription_group
51
53
  )
52
54
 
53
55
  fetch_loop
@@ -67,7 +69,6 @@ module Karafka
67
69
  @stopped = true
68
70
  @executors.clear
69
71
  @coordinators.reset
70
- @client.commit_offsets!
71
72
  @client.stop
72
73
  end
73
74
  end
@@ -83,11 +84,13 @@ module Karafka
83
84
  # Kafka connections / Internet connection issues / Etc. Business logic problems should not
84
85
  # propagate this far.
85
86
  def fetch_loop
86
- until Karafka::App.stopping?
87
+ # Run the main loop as long as we are not stopping or moving into quiet mode
88
+ until Karafka::App.stopping? || Karafka::App.quieting? || Karafka::App.quiet?
87
89
  Karafka.monitor.instrument(
88
90
  'connection.listener.fetch_loop',
89
91
  caller: self,
90
- client: @client
92
+ client: @client,
93
+ subscription_group: @subscription_group
91
94
  )
92
95
 
93
96
  resume_paused_partitions
@@ -95,6 +98,8 @@ module Karafka
95
98
  Karafka.monitor.instrument(
96
99
  'connection.listener.fetch_loop.received',
97
100
  caller: self,
101
+ client: @client,
102
+ subscription_group: @subscription_group,
98
103
  messages_buffer: @messages_buffer
99
104
  ) do
100
105
  # We need to fetch data before we revoke lost partitions details as during the polling
@@ -118,13 +123,9 @@ module Karafka
118
123
  build_and_schedule_consumption_jobs
119
124
 
120
125
  wait
121
-
122
- # We don't use the `#commit_offsets!` here for performance reasons. This can be achieved
123
- # if needed by using manual offset management.
124
- @client.commit_offsets
125
126
  end
126
127
 
127
- # If we are stopping we will no longer schedule any jobs despite polling.
128
+ # If we are stopping we will no longer schedule any regular jobs despite polling.
128
129
  # We need to keep polling not to exceed the `max.poll.interval` for long-running
129
130
  # non-blocking jobs and we need to allow them to finish. We however do not want to
130
131
  # enqueue any new jobs. It's worth keeping in mind that it is the end user responsibility
@@ -133,7 +134,14 @@ module Karafka
133
134
  #
134
135
  # We do not care about resuming any partitions or lost jobs as we do not plan to do
135
136
  # anything with them as we're in the shutdown phase.
136
- wait_with_poll
137
+ #
138
+ # What we do care however is the ability to still run revocation jobs in case anything
139
+ # would change in the cluster. We still want to notify the long-running jobs about changes
140
+ # that occurred in the cluster.
141
+ wait_pinging(
142
+ wait_until: -> { @jobs_queue.empty?(@subscription_group.id) },
143
+ after_ping: -> { build_and_schedule_revoke_lost_partitions_jobs }
144
+ )
137
145
 
138
146
  # We do not want to schedule the shutdown jobs prior to finishing all the jobs
139
147
  # (including non-blocking) as there might be a long-running job with a shutdown and then
@@ -141,7 +149,25 @@ module Karafka
141
149
  # as it could create a race-condition.
142
150
  build_and_schedule_shutdown_jobs
143
151
 
144
- wait_with_poll
152
+ # Wait until all the shutdown jobs are done
153
+ wait_pinging(wait_until: -> { @jobs_queue.empty?(@subscription_group.id) })
154
+
155
+ # Once all the work is done, we need to decrement counter of active subscription groups
156
+ # within this consumer group
157
+ @consumer_group_coordinator.finish_work(id)
158
+
159
+ # Wait if we're in the process of finishing started work or finished all the work and
160
+ # just sitting and being quiet
161
+ wait_pinging(wait_until: -> { !(Karafka::App.quieting? || Karafka::App.quiet?) })
162
+
163
+ # We need to wait until all the work in the whole consumer group (local to the process)
164
+ # is done. Otherwise we may end up with locks and `Timed out LeaveGroupRequest in flight`
165
+ # warning notifications.
166
+ wait_pinging(wait_until: -> { @consumer_group_coordinator.shutdown? })
167
+
168
+ # This extra ping will make sure we've refreshed the rebalance state after other instances
169
+ # potentially shutdown. This will prevent us from closing with a dangling callback
170
+ @client.ping
145
171
 
146
172
  shutdown
147
173
 
@@ -159,6 +185,8 @@ module Karafka
159
185
  restart
160
186
 
161
187
  sleep(1) && retry
188
+ ensure
189
+ @consumer_group_coordinator.unlock
162
190
  end
163
191
 
164
192
  # Resumes processing of partitions that were paused due to an error.
@@ -185,7 +213,9 @@ module Karafka
185
213
  # processed (if it was assigned and revoked really fast), thus we may not have it
186
214
  # here. In cases like this, we do not run a revocation job
187
215
  @executors.find_all(topic, partition).each do |executor|
188
- jobs << @jobs_builder.revoked(executor)
216
+ job = @jobs_builder.revoked(executor)
217
+ job.before_enqueue
218
+ jobs << job
189
219
  end
190
220
 
191
221
  # We need to remove all the executors of a given topic partition that we have lost, so
@@ -204,8 +234,10 @@ module Karafka
204
234
  def build_and_schedule_shutdown_jobs
205
235
  jobs = []
206
236
 
207
- @executors.each do |_, _, executor|
208
- jobs << @jobs_builder.shutdown(executor)
237
+ @executors.each do |executor|
238
+ job = @jobs_builder.shutdown(executor)
239
+ job.before_enqueue
240
+ jobs << job
209
241
  end
210
242
 
211
243
  @scheduler.schedule_shutdown(@jobs_queue, jobs)
@@ -231,20 +263,25 @@ module Karafka
231
263
 
232
264
  @messages_buffer.each do |topic, partition, messages|
233
265
  coordinator = @coordinators.find_or_create(topic, partition)
234
-
235
266
  # Start work coordination for this topic partition
236
267
  coordinator.start(messages)
237
268
 
238
- @partitioner.call(topic, messages) do |group_id, partition_messages|
239
- # Count the job we're going to create here
240
- coordinator.increment
241
-
242
- executor = @executors.find_or_create(topic, partition, group_id)
243
-
244
- jobs << @jobs_builder.consume(executor, partition_messages, coordinator)
269
+ # We do not increment coordinator for idle job because it's not a user related one
270
+ # and it will not go through a standard lifecycle. Same applies to revoked and shutdown
271
+ if messages.empty?
272
+ executor = @executors.find_or_create(topic, partition, 0, coordinator)
273
+ jobs << @jobs_builder.idle(executor)
274
+ else
275
+ @partitioner.call(topic, messages, coordinator) do |group_id, partition_messages|
276
+ executor = @executors.find_or_create(topic, partition, group_id, coordinator)
277
+ coordinator.increment
278
+ jobs << @jobs_builder.consume(executor, partition_messages)
279
+ end
245
280
  end
246
281
  end
247
282
 
283
+ jobs.each(&:before_enqueue)
284
+
248
285
  @scheduler.schedule_consumption(@jobs_queue, jobs)
249
286
  end
250
287
 
@@ -254,10 +291,21 @@ module Karafka
254
291
  end
255
292
 
256
293
  # Waits without blocking the polling
257
- # This should be used only when we no longer plan to use any incoming data and we can safely
258
- # discard it
259
- def wait_with_poll
260
- @client.batch_poll until @jobs_queue.empty?(@subscription_group.id)
294
+ #
295
+ # This should be used only when we no longer plan to use any incoming messages data and we
296
+ # can safely discard it. We can however use the rebalance information if needed.
297
+ #
298
+ # @param wait_until [Proc] until this evaluates to true, we will poll data
299
+ # @param after_ping [Proc] code that we want to run after each ping (if any)
300
+ #
301
+ # @note Performance of this is not relevant (in regards to blocks) because it is used only
302
+ # on shutdown and quiet, hence not in the running mode
303
+ def wait_pinging(wait_until:, after_ping: -> {})
304
+ until wait_until.call
305
+ @client.ping
306
+ after_ping.call
307
+ sleep(0.2)
308
+ end
261
309
  end
262
310
 
263
311
  # We can stop client without a problem, as it will reinitialize itself when running the
@@ -6,11 +6,27 @@ module Karafka
6
6
  class ListenersBatch
7
7
  include Enumerable
8
8
 
9
+ attr_reader :coordinators
10
+
9
11
  # @param jobs_queue [JobsQueue]
10
12
  # @return [ListenersBatch]
11
13
  def initialize(jobs_queue)
12
- @batch = App.subscription_groups.map do |subscription_group|
13
- Connection::Listener.new(subscription_group, jobs_queue)
14
+ @coordinators = []
15
+
16
+ @batch = App.subscription_groups.flat_map do |_consumer_group, subscription_groups|
17
+ consumer_group_coordinator = Connection::ConsumerGroupCoordinator.new(
18
+ subscription_groups.size
19
+ )
20
+
21
+ @coordinators << consumer_group_coordinator
22
+
23
+ subscription_groups.map do |subscription_group|
24
+ Connection::Listener.new(
25
+ consumer_group_coordinator,
26
+ subscription_group,
27
+ jobs_queue
28
+ )
29
+ end
14
30
  end
15
31
  end
16
32
 
@@ -14,6 +14,9 @@ module Karafka
14
14
  # that are lost, are those that got revoked but did not get re-assigned back. We do not
15
15
  # expose this concept outside and we normalize to have them revoked, as it is irrelevant
16
16
  # from the rest of the code perspective as only those that are lost are truly revoked.
17
+ #
18
+ # @note For cooperative-sticky `#assigned_partitions` holds only the recently assigned
19
+ # partitions, not all the partitions that are owned
17
20
  class RebalanceManager
18
21
  # Empty array for internal usage not to create new objects
19
22
  EMPTY_ARRAY = [].freeze
@@ -12,7 +12,7 @@ module Karafka
12
12
  configure do |config|
13
13
  config.error_messages = YAML.safe_load(
14
14
  File.read(
15
- File.join(Karafka.gem_root, 'config', 'errors.yml')
15
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
16
16
  )
17
17
  ).fetch('en').fetch('validations').fetch('config')
18
18
  end
@@ -7,13 +7,13 @@ module Karafka
7
7
  configure do |config|
8
8
  config.error_messages = YAML.safe_load(
9
9
  File.read(
10
- File.join(Karafka.gem_root, 'config', 'errors.yml')
10
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
11
11
  )
12
12
  ).fetch('en').fetch('validations').fetch('consumer_group')
13
13
  end
14
14
 
15
- required(:id) { |id| id.is_a?(String) && Contracts::TOPIC_REGEXP.match?(id) }
16
- required(:topics) { |topics| topics.is_a?(Array) && !topics.empty? }
15
+ required(:id) { |val| val.is_a?(String) && Contracts::TOPIC_REGEXP.match?(val) }
16
+ required(:topics) { |val| val.is_a?(Array) && !val.empty? }
17
17
 
18
18
  virtual do |data, errors|
19
19
  next unless errors.empty?
@@ -27,16 +27,29 @@ module Karafka
27
27
 
28
28
  virtual do |data, errors|
29
29
  next unless errors.empty?
30
+ next unless ::Karafka::App.config.strict_topics_namespacing
30
31
 
31
- fetched_errors = []
32
-
33
- data.fetch(:topics).each do |topic|
34
- ConsumerGroupTopic.new.call(topic).errors.each do |key, value|
35
- fetched_errors << [[topic, key].flatten, value]
32
+ names = data.fetch(:topics).map { |topic| topic[:name] }
33
+ names_hash = names.each_with_object({}) { |n, h| h[n] = true }
34
+ error_occured = false
35
+ names.each do |n|
36
+ # Skip topic names that are not namespaced
37
+ next unless n.chars.find { |c| ['.', '_'].include?(c) }
38
+
39
+ if n.chars.include?('.')
40
+ # Check underscore styled topic
41
+ underscored_topic = n.tr('.', '_')
42
+ error_occured = names_hash[underscored_topic] ? true : false
43
+ else
44
+ # Check dot styled topic
45
+ dot_topic = n.tr('_', '.')
46
+ error_occured = names_hash[dot_topic] ? true : false
36
47
  end
37
48
  end
38
49
 
39
- fetched_errors
50
+ next unless error_occured
51
+
52
+ [[%i[topics], :topics_namespaced_names_not_unique]]
40
53
  end
41
54
  end
42
55
  end