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
@@ -10,60 +10,105 @@ module Karafka
10
10
  # listener thread, but we go with thread-safe by default for all not to worry about potential
11
11
  # future mistakes.
12
12
  class Coordinator
13
- # @return [Karafka::TimeTrackers::Pause]
14
- attr_reader :pause_tracker
13
+ attr_reader :pause_tracker, :seek_offset, :topic, :partition
15
14
 
15
+ # @param topic [Karafka::Routing::Topic]
16
+ # @param partition [Integer]
16
17
  # @param pause_tracker [Karafka::TimeTrackers::Pause] pause tracker for given topic partition
17
- def initialize(pause_tracker)
18
+ def initialize(topic, partition, pause_tracker)
19
+ @topic = topic
20
+ @partition = partition
18
21
  @pause_tracker = pause_tracker
19
22
  @revoked = false
20
23
  @consumptions = {}
21
24
  @running_jobs = 0
25
+ @manual_pause = false
22
26
  @mutex = Mutex.new
27
+ @marked = false
28
+ @failure = false
23
29
  end
24
30
 
25
31
  # Starts the coordinator for given consumption jobs
26
- # @param _messages [Array<Karafka::Messages::Message>] batch of message for which we are
32
+ # @param messages [Array<Karafka::Messages::Message>] batch of message for which we are
27
33
  # going to coordinate work. Not used with regular coordinator.
28
- def start(_messages)
29
- @mutex.synchronize do
30
- @running_jobs = 0
31
- # We need to clear the consumption results hash here, otherwise we could end up storing
32
- # consumption results of consumer instances we no longer control
33
- @consumptions.clear
34
+ def start(messages)
35
+ @failure = false
36
+ @running_jobs = 0
37
+ # We need to clear the consumption results hash here, otherwise we could end up storing
38
+ # consumption results of consumer instances we no longer control
39
+ @consumptions.clear
40
+
41
+ # When starting to run, no pause is expected and no manual pause as well
42
+ @manual_pause = false
43
+
44
+ # We set it on the first encounter and never again, because then the offset setting
45
+ # should be up to the consumers logic (our or the end user)
46
+ # Seek offset needs to be always initialized as for case where manual offset management
47
+ # is turned on, we need to have reference to the first offset even in case of running
48
+ # multiple batches without marking any messages as consumed. Rollback needs to happen to
49
+ # the last place we know of or the last message + 1 that was marked
50
+ #
51
+ # It is however worth keeping in mind, that this may need to be used with `#marked?` to
52
+ # make sure that the first offset is an offset that has been marked.
53
+ @seek_offset ||= messages.first.offset
54
+ end
55
+
56
+ # @param offset [Integer] message offset
57
+ def seek_offset=(offset)
58
+ synchronize do
59
+ @marked = true
60
+ @seek_offset = offset
34
61
  end
35
62
  end
36
63
 
37
64
  # Increases number of jobs that we handle with this coordinator
38
65
  def increment
39
- @mutex.synchronize { @running_jobs += 1 }
66
+ synchronize { @running_jobs += 1 }
40
67
  end
41
68
 
42
69
  # Decrements number of jobs we handle at the moment
43
70
  def decrement
44
- @mutex.synchronize do
71
+ synchronize do
45
72
  @running_jobs -= 1
46
73
 
47
74
  return @running_jobs unless @running_jobs.negative?
48
75
 
49
76
  # This should never happen. If it does, something is heavily out of sync. Please reach
50
77
  # out to us if you encounter this
51
- raise Karafka::Errors::InvalidCoordinatorState, 'Was zero before decrementation'
78
+ raise Karafka::Errors::InvalidCoordinatorStateError, 'Was zero before decrementation'
52
79
  end
53
80
  end
54
81
 
55
- # @param consumer [Object] karafka consumer (normal or pro)
56
- # @return [Karafka::Processing::Result] result object which we can use to indicate
57
- # consumption processing state.
58
- def consumption(consumer)
59
- @mutex.synchronize do
60
- @consumptions[consumer] ||= Processing::Result.new
82
+ # Is all the consumption done and finished successfully for this coordinator
83
+ # We do not say we're successful until all work is done, because running work may still
84
+ # crash.
85
+ def success?
86
+ synchronize do
87
+ @running_jobs.zero? && @consumptions.values.all?(&:success?)
61
88
  end
62
89
  end
63
90
 
64
- # Is all the consumption done and finished successfully for this coordinator
65
- def success?
66
- @mutex.synchronize { @running_jobs.zero? && @consumptions.values.all?(&:success?) }
91
+ # Mark given consumption on consumer as successful
92
+ # @param consumer [Karafka::BaseConsumer] consumer that finished successfully
93
+ def success!(consumer)
94
+ synchronize do
95
+ consumption(consumer).success!
96
+ end
97
+ end
98
+
99
+ # Mark given consumption on consumer as failed
100
+ # @param consumer [Karafka::BaseConsumer] consumer that failed
101
+ # @param error [StandardError] error that occurred
102
+ def failure!(consumer, error)
103
+ synchronize do
104
+ @failure = true
105
+ consumption(consumer).failure!(error)
106
+ end
107
+ end
108
+
109
+ # @return [Boolean] true if any of work we were running failed
110
+ def failure?
111
+ @failure
67
112
  end
68
113
 
69
114
  # Marks given coordinator for processing group as revoked
@@ -76,13 +121,47 @@ module Karafka
76
121
  # listener loop dispatching the revocation job. It is ok, as effectively nothing will be
77
122
  # processed until revocation jobs are done.
78
123
  def revoke
79
- @mutex.synchronize { @revoked = true }
124
+ synchronize { @revoked = true }
80
125
  end
81
126
 
82
127
  # @return [Boolean] is the partition we are processing revoked or not
83
128
  def revoked?
84
129
  @revoked
85
130
  end
131
+
132
+ # @return [Boolean] was the new seek offset assigned at least once. This is needed because
133
+ # by default we assign seek offset of a first message ever, however this is insufficient
134
+ # for DLQ in a scenario where the first message would be broken. We would never move
135
+ # out of it and would end up in an endless loop.
136
+ def marked?
137
+ @marked
138
+ end
139
+
140
+ # Store in the coordinator info, that this pause was done manually by the end user and not
141
+ # by the system itself
142
+ def manual_pause
143
+ @manual_pause = true
144
+ end
145
+
146
+ # @return [Boolean] are we in a pause that was initiated by the user
147
+ def manual_pause?
148
+ @pause_tracker.paused? && @manual_pause
149
+ end
150
+
151
+ # Allows to run synchronized (locked) code that can operate in between virtual partitions
152
+ # @param block [Proc] code we want to run in the synchronized mode
153
+ def synchronize(&block)
154
+ @mutex.synchronize(&block)
155
+ end
156
+
157
+ private
158
+
159
+ # @param consumer [Object] karafka consumer (normal or pro)
160
+ # @return [Karafka::Processing::Result] result object which we can use to indicate
161
+ # consumption processing state.
162
+ def consumption(consumer)
163
+ @consumptions[consumer] ||= Processing::Result.new
164
+ end
86
165
  end
87
166
  end
88
167
  end
@@ -9,16 +9,20 @@ module Karafka
9
9
  # @note This buffer operates only from the listener loop, thus we do not have to make it
10
10
  # thread-safe.
11
11
  class CoordinatorsBuffer
12
- def initialize
12
+ # @param topics [Karafka::Routing::Topics]
13
+ def initialize(topics)
13
14
  @pauses_manager = Connection::PausesManager.new
14
15
  @coordinator_class = ::Karafka::App.config.internal.processing.coordinator_class
15
16
  @coordinators = Hash.new { |h, k| h[k] = {} }
17
+ @topics = topics
16
18
  end
17
19
 
18
20
  # @param topic [String] topic name
19
21
  # @param partition [Integer] partition number
20
22
  def find_or_create(topic, partition)
21
23
  @coordinators[topic][partition] ||= @coordinator_class.new(
24
+ @topics.find(topic),
25
+ partition,
22
26
  @pauses_manager.fetch(topic, partition)
23
27
  )
24
28
  end
@@ -12,6 +12,10 @@ module Karafka
12
12
  # @note Executors are not removed after partition is revoked. They are not that big and will
13
13
  # be re-used in case of a re-claim
14
14
  class Executor
15
+ extend Forwardable
16
+
17
+ def_delegators :@coordinator, :topic, :partition
18
+
15
19
  # @return [String] unique id that we use to ensure, that we use for state tracking
16
20
  attr_reader :id
17
21
 
@@ -21,45 +25,46 @@ module Karafka
21
25
  # @return [Karafka::Messages::Messages] messages batch
22
26
  attr_reader :messages
23
27
 
24
- # Topic accessibility may be needed for the jobs builder to be able to build a proper job
25
- # based on the topic settings defined by the end user
26
- #
27
- # @return [Karafka::Routing::Topic] topic of this executor
28
- attr_reader :topic
28
+ # @return [Karafka::Processing::Coordinator] coordinator for this executor
29
+ attr_reader :coordinator
29
30
 
30
31
  # @param group_id [String] id of the subscription group to which the executor belongs
31
32
  # @param client [Karafka::Connection::Client] kafka client
32
- # @param topic [Karafka::Routing::Topic] topic for which this executor will run
33
- def initialize(group_id, client, topic)
34
- @id = SecureRandom.uuid
33
+ # @param coordinator [Karafka::Processing::Coordinator]
34
+ def initialize(group_id, client, coordinator)
35
+ @id = SecureRandom.hex(6)
35
36
  @group_id = group_id
36
37
  @client = client
37
- @topic = topic
38
+ @coordinator = coordinator
38
39
  end
39
40
 
40
- # Builds the consumer instance, builds messages batch and sets all that is needed to run the
41
- # user consumption logic
41
+ # Allows us to prepare the consumer in the listener thread prior to the job being send to
42
+ # the queue. It also allows to run some code that is time sensitive and cannot wait in the
43
+ # queue as it could cause starvation.
42
44
  #
43
45
  # @param messages [Array<Karafka::Messages::Message>]
44
- # @param received_at [Time] the moment we've received the batch (actually the moment we've)
45
- # enqueued it, but good enough
46
- # @param coordinator [Karafka::Processing::Coordinator] coordinator for processing management
47
- def before_consume(messages, received_at, coordinator)
46
+ def before_enqueue(messages)
48
47
  # Recreate consumer with each batch if persistence is not enabled
49
48
  # We reload the consumers with each batch instead of relying on some external signals
50
49
  # when needed for consistency. That way devs may have it on or off and not in this
51
50
  # middle state, where re-creation of a consumer instance would occur only sometimes
52
51
  @consumer = nil unless ::Karafka::App.config.consumer_persistence
53
52
 
54
- consumer.coordinator = coordinator
55
-
56
53
  # First we build messages batch...
57
54
  consumer.messages = Messages::Builders::Messages.call(
58
55
  messages,
59
- @topic,
60
- received_at
56
+ topic,
57
+ partition,
58
+ # the moment we've received the batch or actually the moment we've enqueued it,
59
+ # but good enough
60
+ Time.now
61
61
  )
62
62
 
63
+ consumer.on_before_enqueue
64
+ end
65
+
66
+ # Runs setup and warm-up code in the worker prior to running the consumption
67
+ def before_consume
63
68
  consumer.on_before_consume
64
69
  end
65
70
 
@@ -74,6 +79,23 @@ module Karafka
74
79
  consumer.on_after_consume
75
80
  end
76
81
 
82
+ # Runs consumer idle operations
83
+ # This may include house-keeping or other state management changes that can occur but that
84
+ # not mean there are any new messages available for the end user to process
85
+ def idle
86
+ # Initializes the messages set in case idle operation would happen before any processing
87
+ # This prevents us from having no messages object at all as the messages object and
88
+ # its metadata may be used for statistics
89
+ consumer.messages ||= Messages::Builders::Messages.call(
90
+ [],
91
+ topic,
92
+ partition,
93
+ Time.now
94
+ )
95
+
96
+ consumer.on_idle
97
+ end
98
+
77
99
  # Runs the controller `#revoked` method that should be triggered when a given consumer is
78
100
  # no longer needed due to partitions reassignment.
79
101
  #
@@ -106,10 +128,18 @@ module Karafka
106
128
  # @return [Object] cached consumer instance
107
129
  def consumer
108
130
  @consumer ||= begin
109
- consumer = @topic.consumer_class.new
110
- consumer.topic = @topic
131
+ topic = @coordinator.topic
132
+
133
+ strategy = ::Karafka::App.config.internal.processing.strategy_selector.find(topic)
134
+
135
+ consumer = topic.consumer_class.new
136
+ # We use singleton class as the same consumer class may be used to process different
137
+ # topics with different settings
138
+ consumer.singleton_class.include(strategy)
111
139
  consumer.client = @client
112
140
  consumer.producer = ::Karafka::App.producer
141
+ consumer.coordinator = @coordinator
142
+
113
143
  consumer
114
144
  end
115
145
  end
@@ -20,14 +20,13 @@ module Karafka
20
20
  # @param topic [String] topic name
21
21
  # @param partition [Integer] partition number
22
22
  # @param parallel_key [String] parallel group key
23
+ # @param coordinator [Karafka::Processing::Coordinator]
23
24
  # @return [Executor] consumer executor
24
- def find_or_create(topic, partition, parallel_key)
25
- ktopic = find_topic(topic)
26
-
27
- @buffer[ktopic][partition][parallel_key] ||= Executor.new(
25
+ def find_or_create(topic, partition, parallel_key, coordinator)
26
+ @buffer[topic][partition][parallel_key] ||= Executor.new(
28
27
  @subscription_group.id,
29
28
  @client,
30
- ktopic
29
+ coordinator
31
30
  )
32
31
  end
33
32
 
@@ -37,9 +36,7 @@ module Karafka
37
36
  # @param topic [String] topic name
38
37
  # @param partition [Integer] partition number
39
38
  def revoke(topic, partition)
40
- ktopic = find_topic(topic)
41
-
42
- @buffer[ktopic][partition].clear
39
+ @buffer[topic][partition].clear
43
40
  end
44
41
 
45
42
  # Finds all the executors available for a given topic partition
@@ -48,9 +45,7 @@ module Karafka
48
45
  # @param partition [Integer] partition number
49
46
  # @return [Array<Executor>] executors in use for this topic + partition
50
47
  def find_all(topic, partition)
51
- ktopic = find_topic(topic)
52
-
53
- @buffer[ktopic][partition].values
48
+ @buffer[topic][partition].values
54
49
  end
55
50
 
56
51
  # Iterates over all available executors and yields them together with topic and partition
@@ -59,11 +54,10 @@ module Karafka
59
54
  # @yieldparam [Integer] partition number
60
55
  # @yieldparam [Executor] given executor
61
56
  def each
62
- @buffer.each do |ktopic, partitions|
63
- partitions.each do |partition, executors|
64
- executors.each do |_parallel_key, executor|
65
- # We skip the parallel key here as it does not serve any value when iterating
66
- yield(ktopic, partition, executor)
57
+ @buffer.each do |_, partitions|
58
+ partitions.each do |_, executors|
59
+ executors.each do |_, executor|
60
+ yield(executor)
67
61
  end
68
62
  end
69
63
  end
@@ -73,16 +67,6 @@ module Karafka
73
67
  def clear
74
68
  @buffer.clear
75
69
  end
76
-
77
- private
78
-
79
- # Finds topic based on its name
80
- #
81
- # @param topic [String] topic we're looking for
82
- # @return [Karafka::Routing::Topic] topic we're interested in
83
- def find_topic(topic)
84
- @subscription_group.topics.find(topic) || raise(Errors::TopicNotFoundError, topic)
85
- end
86
70
  end
87
71
  end
88
72
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Karafka
4
4
  module Processing
5
- # Namespace for all the jobs that are suppose to run in workers.
5
+ # Namespace for all the jobs that are supposed to run in workers.
6
6
  module Jobs
7
7
  # Base class for all the jobs types that are suppose to run in workers threads.
8
8
  # Each job can have 3 main entry-points: `#before_call`, `#call` and `#after_call`
@@ -10,7 +10,7 @@ module Karafka
10
10
  class Base
11
11
  extend Forwardable
12
12
 
13
- # @note Since one job has always one executer, we use the jobs id and group id as reference
13
+ # @note Since one job has always one executor, we use the jobs id and group id as reference
14
14
  def_delegators :executor, :id, :group_id
15
15
 
16
16
  attr_reader :executor
@@ -22,6 +22,10 @@ module Karafka
22
22
  @non_blocking = false
23
23
  end
24
24
 
25
+ # When redefined can run any code prior to the job being enqueued
26
+ # @note This will run in the listener thread and not in the worker
27
+ def before_enqueue; end
28
+
25
29
  # When redefined can run any code that should run before executing the proper code
26
30
  def before_call; end
27
31
 
@@ -12,19 +12,22 @@ module Karafka
12
12
  # @param executor [Karafka::Processing::Executor] executor that is suppose to run a given
13
13
  # job
14
14
  # @param messages [Karafka::Messages::Messages] karafka messages batch
15
- # @param coordinator [Karafka::Processing::Coordinator] processing coordinator
16
15
  # @return [Consume]
17
- def initialize(executor, messages, coordinator)
16
+ def initialize(executor, messages)
18
17
  @executor = executor
19
18
  @messages = messages
20
- @coordinator = coordinator
21
- @created_at = Time.now
22
19
  super()
23
20
  end
24
21
 
22
+ # Runs all the preparation code on the executor that needs to happen before the job is
23
+ # enqueued.
24
+ def before_enqueue
25
+ executor.before_enqueue(@messages)
26
+ end
27
+
25
28
  # Runs the before consumption preparations on the executor
26
29
  def before_call
27
- executor.before_consume(@messages, @created_at, @coordinator)
30
+ executor.before_consume
28
31
  end
29
32
 
30
33
  # Runs the given executor
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Jobs
6
+ # Type of job that we may use to run some extra handling that happens without the user
7
+ # related lifecycle event like consumption, revocation, etc.
8
+ class Idle < Base
9
+ # @param executor [Karafka::Processing::Executor] executor that is suppose to run a given
10
+ # job on an active consumer
11
+ # @return [Shutdown]
12
+ def initialize(executor)
13
+ @executor = executor
14
+ super()
15
+ end
16
+
17
+ # Run the idle work via the executor
18
+ def call
19
+ executor.idle
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -7,10 +7,9 @@ module Karafka
7
7
  class JobsBuilder
8
8
  # @param executor [Karafka::Processing::Executor]
9
9
  # @param messages [Karafka::Messages::Messages] messages batch to be consumed
10
- # @param coordinator [Karafka::Processing::Coordinator]
11
10
  # @return [Karafka::Processing::Jobs::Consume] consumption job
12
- def consume(executor, messages, coordinator)
13
- Jobs::Consume.new(executor, messages, coordinator)
11
+ def consume(executor, messages)
12
+ Jobs::Consume.new(executor, messages)
14
13
  end
15
14
 
16
15
  # @param executor [Karafka::Processing::Executor]
@@ -13,15 +13,19 @@ module Karafka
13
13
  # @return [Karafka::Processing::JobsQueue]
14
14
  def initialize
15
15
  @queue = Queue.new
16
- # Those queues will act as a semaphores internally. Since we need an indicator for waiting
16
+ # Those queues will act as semaphores internally. Since we need an indicator for waiting
17
17
  # we could use Thread.pass but this is expensive. Instead we can just lock until any
18
18
  # of the workers finishes their work and we can re-check. This means that in the worse
19
19
  # scenario, we will context switch 10 times per poll instead of getting this thread
20
20
  # scheduled by Ruby hundreds of thousands of times per group.
21
21
  # We cannot use a single semaphore as it could potentially block in listeners that should
22
22
  # process with their data and also could unlock when a given group needs to remain locked
23
- @semaphores = Hash.new { |h, k| h[k] = Queue.new }
23
+ @semaphores = Concurrent::Map.new do |h, k|
24
+ h.compute_if_absent(k) { Queue.new }
25
+ end
26
+
24
27
  @in_processing = Hash.new { |h, k| h[k] = [] }
28
+
25
29
  @mutex = Mutex.new
26
30
  end
27
31
 
@@ -47,9 +51,9 @@ module Karafka
47
51
  raise(Errors::JobsQueueSynchronizationError, job.group_id) if group.include?(job)
48
52
 
49
53
  group << job
50
- end
51
54
 
52
- @queue << job
55
+ @queue << job
56
+ end
53
57
  end
54
58
 
55
59
  # @return [Jobs::Base, nil] waits for a job from the main queue and returns it once available
@@ -96,7 +100,7 @@ module Karafka
96
100
  return if @queue.closed?
97
101
 
98
102
  @queue.close
99
- @semaphores.values.each(&:close)
103
+ @semaphores.each_value(&:close)
100
104
  end
101
105
  end
102
106
 
@@ -105,7 +109,9 @@ module Karafka
105
109
  # @return [Boolean] tell us if we have anything in the processing (or for processing) from
106
110
  # a given group.
107
111
  def empty?(group_id)
108
- @in_processing[group_id].empty?
112
+ @mutex.synchronize do
113
+ @in_processing[group_id].empty?
114
+ end
109
115
  end
110
116
 
111
117
  # Blocks when there are things in the queue in a given group and waits until all the blocking
@@ -119,13 +125,13 @@ module Karafka
119
125
  @semaphores[group_id].pop while wait?(group_id)
120
126
  end
121
127
 
122
- # - `processing` - number of jobs that are currently being processed (active work)
128
+ # - `busy` - number of jobs that are currently being processed (active work)
123
129
  # - `enqueued` - number of jobs in the queue that are waiting to be picked up by a worker
124
130
  #
125
131
  # @return [Hash] hash with basic usage statistics of this queue.
126
132
  def statistics
127
133
  {
128
- processing: size - @queue.size,
134
+ busy: size - @queue.size,
129
135
  enqueued: @queue.size
130
136
  }.freeze
131
137
  end
@@ -12,9 +12,11 @@ module Karafka
12
12
 
13
13
  # @param _topic [String] topic name
14
14
  # @param messages [Array<Karafka::Messages::Message>] karafka messages
15
+ # @param _coordinator [Karafka::Processing::Coordinator] processing coordinator that will
16
+ # be used with those messages
15
17
  # @yieldparam [Integer] group id
16
18
  # @yieldparam [Array<Karafka::Messages::Message>] karafka messages
17
- def call(_topic, messages)
19
+ def call(_topic, messages, _coordinator)
18
20
  yield(0, messages)
19
21
  end
20
22
  end
@@ -6,8 +6,11 @@ module Karafka
6
6
  # It allows to indicate if given thing moved from success to a failure or the other way around
7
7
  # Useful for tracking consumption state
8
8
  class Result
9
+ attr_reader :cause
10
+
9
11
  def initialize
10
12
  @success = true
13
+ @cause = false
11
14
  end
12
15
 
13
16
  # @return [Boolean]
@@ -18,11 +21,21 @@ module Karafka
18
21
  # Marks state as successful
19
22
  def success!
20
23
  @success = true
24
+ # We set cause to false so the previous error that occurred does not leak when error is
25
+ # no longer present
26
+ @cause = false
21
27
  end
22
28
 
23
29
  # Marks state as failure
24
- def failure!
30
+ # @param cause [StandardError] error that occurred and caused failure
31
+ def failure!(cause)
25
32
  @success = false
33
+ @cause = cause
34
+ end
35
+
36
+ # @return [Boolean] true if processing failed
37
+ def failure?
38
+ !@success
26
39
  end
27
40
  end
28
41
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Strategies
6
+ # ActiveJob strategy to cooperate with the DLQ.
7
+ #
8
+ # While AJ is uses MOM by default because it delegates the offset management to the AJ
9
+ # consumer. With DLQ however there is an extra case for skipping broken jobs with offset
10
+ # marking due to ordered processing.
11
+ module AjDlqMom
12
+ include DlqMom
13
+
14
+ # Apply strategy when only when using AJ with MOM and DLQ
15
+ FEATURES = %i[
16
+ active_job
17
+ dead_letter_queue
18
+ manual_offset_management
19
+ ].freeze
20
+
21
+ # How should we post-finalize consumption.
22
+ def handle_after_consume
23
+ return if revoked?
24
+
25
+ if coordinator.success?
26
+ # Do NOT commit offsets, they are comitted after each job in the AJ consumer.
27
+ coordinator.pause_tracker.reset
28
+ elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
29
+ retry_after_pause
30
+ else
31
+ coordinator.pause_tracker.reset
32
+ skippable_message, = find_skippable_message
33
+ dispatch_to_dlq(skippable_message)
34
+ # We can commit the offset here because we know that we skip it "forever" and
35
+ # since AJ consumer commits the offset after each job, we also know that the
36
+ # previous job was successful
37
+ mark_as_consumed(skippable_message)
38
+ pause(coordinator.seek_offset, nil, false)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Strategies
6
+ # ActiveJob enabled
7
+ # Manual offset management enabled
8
+ #
9
+ # This is the default AJ strategy since AJ cannot be used without MOM
10
+ module AjMom
11
+ include Mom
12
+
13
+ # Apply strategy when only when using AJ with MOM
14
+ FEATURES = %i[
15
+ active_job
16
+ manual_offset_management
17
+ ].freeze
18
+ end
19
+ end
20
+ end
21
+ end