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
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ # Our processing patterns differ depending on various features configurations
6
+ # In this namespace we collect strategies for particular feature combinations to simplify the
7
+ # design. Based on features combinations we can then select handling strategy for a given case.
8
+ #
9
+ # @note The lack of common code here is intentional. It would get complex if there would be
10
+ # any type of composition, so each strategy is expected to be self-sufficient
11
+ module Strategies
12
+ # Base strategy that should be included in each strategy, just to ensure the API
13
+ module Base
14
+ # What should happen before jobs are enqueued
15
+ # @note This runs from the listener thread, not recommended to put anything slow here
16
+ def handle_before_enqueue
17
+ raise NotImplementedError, 'Implement in a subclass'
18
+ end
19
+
20
+ # What should happen before we kick in the processing
21
+ def handle_before_consume
22
+ raise NotImplementedError, 'Implement in a subclass'
23
+ end
24
+
25
+ # What should happen in the processing
26
+ def handle_consume
27
+ raise NotImplementedError, 'Implement in a subclass'
28
+ end
29
+
30
+ # Post-consumption handling
31
+ def handle_after_consume
32
+ raise NotImplementedError, 'Implement in a subclass'
33
+ end
34
+
35
+ # Idle run handling
36
+ def handle_idle
37
+ raise NotImplementedError, 'Implement in a subclass'
38
+ end
39
+
40
+ # Revocation handling
41
+ def handle_revoked
42
+ raise NotImplementedError, 'Implement in a subclass'
43
+ end
44
+
45
+ # Shutdown handling
46
+ def handle_shutdown
47
+ raise NotImplementedError, 'Implement in a subclass'
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Strategies
6
+ # No features enabled.
7
+ # No manual offset management
8
+ # No long running jobs
9
+ # Nothing. Just standard, automatic flow
10
+ module Default
11
+ include Base
12
+
13
+ # Apply strategy for a non-feature based flow
14
+ FEATURES = %i[].freeze
15
+
16
+ # Marks message as consumed in an async way.
17
+ #
18
+ # @param message [Messages::Message] last successfully processed message.
19
+ # @return [Boolean] true if we were able to mark the offset, false otherwise.
20
+ # False indicates that we were not able and that we have lost the partition.
21
+ #
22
+ # @note We keep track of this offset in case we would mark as consumed and got error when
23
+ # processing another message. In case like this we do not pause on the message we've
24
+ # already processed but rather at the next one. This applies to both sync and async
25
+ # versions of this method.
26
+ def mark_as_consumed(message)
27
+ # Ignore earlier offsets than the one we already committed
28
+ return true if coordinator.seek_offset > message.offset
29
+ return false if revoked?
30
+
31
+ unless client.mark_as_consumed(message)
32
+ coordinator.revoke
33
+
34
+ return false
35
+ end
36
+
37
+ coordinator.seek_offset = message.offset + 1
38
+
39
+ true
40
+ end
41
+
42
+ # Marks message as consumed in a sync way.
43
+ #
44
+ # @param message [Messages::Message] last successfully processed message.
45
+ # @return [Boolean] true if we were able to mark the offset, false otherwise.
46
+ # False indicates that we were not able and that we have lost the partition.
47
+ def mark_as_consumed!(message)
48
+ # Ignore earlier offsets than the one we already committed
49
+ return true if coordinator.seek_offset > message.offset
50
+ return false if revoked?
51
+
52
+ unless client.mark_as_consumed!(message)
53
+ coordinator.revoke
54
+
55
+ return false
56
+ end
57
+
58
+ coordinator.seek_offset = message.offset + 1
59
+
60
+ true
61
+ end
62
+
63
+ # No actions needed for the standard flow here
64
+ def handle_before_enqueue
65
+ nil
66
+ end
67
+
68
+ # Increment number of attempts
69
+ def handle_before_consume
70
+ coordinator.pause_tracker.increment
71
+ end
72
+
73
+ # Run the user consumption code
74
+ def handle_consume
75
+ Karafka.monitor.instrument('consumer.consume', caller: self)
76
+ Karafka.monitor.instrument('consumer.consumed', caller: self) do
77
+ consume
78
+ end
79
+
80
+ # Mark job as successful
81
+ coordinator.success!(self)
82
+ rescue StandardError => e
83
+ coordinator.failure!(self, e)
84
+
85
+ # Re-raise so reported in the consumer
86
+ raise e
87
+ ensure
88
+ # We need to decrease number of jobs that this coordinator coordinates as it has finished
89
+ coordinator.decrement
90
+ end
91
+
92
+ # Standard flow marks work as consumed and moves on if everything went ok.
93
+ # If there was a processing error, we will pause and continue from the next message
94
+ # (next that is +1 from the last one that was successfully marked as consumed)
95
+ def handle_after_consume
96
+ return if revoked?
97
+
98
+ if coordinator.success?
99
+ coordinator.pause_tracker.reset
100
+
101
+ # We should not move the offset automatically when the partition was paused
102
+ # If we would not do this upon a revocation during the pause time, a different process
103
+ # would pick not from the place where we paused but from the offset that would be
104
+ # automatically committed here
105
+ return if coordinator.manual_pause?
106
+
107
+ mark_as_consumed(messages.last)
108
+ else
109
+ retry_after_pause
110
+ end
111
+ end
112
+
113
+ # Code that should run on idle runs without messages available
114
+ def handle_idle
115
+ nil
116
+ end
117
+
118
+ # We need to always un-pause the processing in case we have lost a given partition.
119
+ # Otherwise the underlying librdkafka would not know we may want to continue processing and
120
+ # the pause could in theory last forever
121
+ def handle_revoked
122
+ resume
123
+
124
+ coordinator.revoke
125
+
126
+ Karafka.monitor.instrument('consumer.revoke', caller: self)
127
+ Karafka.monitor.instrument('consumer.revoked', caller: self) do
128
+ revoked
129
+ end
130
+ end
131
+
132
+ # Runs the shutdown code
133
+ def handle_shutdown
134
+ Karafka.monitor.instrument('consumer.shutting_down', caller: self)
135
+ Karafka.monitor.instrument('consumer.shutdown', caller: self) do
136
+ shutdown
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Strategies
6
+ # When using dead letter queue, processing won't stop after defined number of retries
7
+ # upon encountering non-critical errors but the messages that error will be moved to a
8
+ # separate topic with their payload and metadata, so they can be handled differently.
9
+ module Dlq
10
+ include Default
11
+
12
+ # Apply strategy when only dead letter queue is turned on
13
+ FEATURES = %i[
14
+ dead_letter_queue
15
+ ].freeze
16
+
17
+ # When manual offset management is on, we do not mark anything as consumed automatically
18
+ # and we rely on the user to figure things out
19
+ def handle_after_consume
20
+ return if revoked?
21
+
22
+ if coordinator.success?
23
+ coordinator.pause_tracker.reset
24
+
25
+ return if coordinator.manual_pause?
26
+
27
+ mark_as_consumed(messages.last)
28
+ elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
29
+ retry_after_pause
30
+ # If we've reached number of retries that we could, we need to skip the first message
31
+ # that was not marked as consumed, pause and continue, while also moving this message
32
+ # to the dead topic
33
+ else
34
+ # We reset the pause to indicate we will now consider it as "ok".
35
+ coordinator.pause_tracker.reset
36
+
37
+ skippable_message, = find_skippable_message
38
+
39
+ # Send skippable message to the dql topic
40
+ dispatch_to_dlq(skippable_message)
41
+
42
+ # We mark the broken message as consumed and move on
43
+ mark_as_consumed(skippable_message)
44
+
45
+ return if revoked?
46
+
47
+ # We pause to backoff once just in case.
48
+ pause(coordinator.seek_offset, nil, false)
49
+ end
50
+ end
51
+
52
+ # Finds the message may want to skip (all, starting from first)
53
+ # @private
54
+ # @return [Array<Karafka::Messages::Message, Boolean>] message we may want to skip and
55
+ # information if this message was from marked offset or figured out via mom flow
56
+ def find_skippable_message
57
+ skippable_message = messages.find do |msg|
58
+ coordinator.marked? && msg.offset == coordinator.seek_offset
59
+ end
60
+
61
+ # If we don't have the message matching the last comitted offset, it means that
62
+ # user operates with manual offsets and we're beyond the batch in which things
63
+ # broke for the first time. Then we skip the first (as no markings) and we
64
+ # move on one by one.
65
+ skippable_message ? [skippable_message, true] : [messages.first, false]
66
+ end
67
+
68
+ # Moves the broken message into a separate queue defined via the settings
69
+ # @private
70
+ # @param skippable_message [Karafka::Messages::Message] message we are skipping that also
71
+ # should go to the dlq topic
72
+ def dispatch_to_dlq(skippable_message)
73
+ producer.produce_async(
74
+ topic: topic.dead_letter_queue.topic,
75
+ payload: skippable_message.raw_payload
76
+ )
77
+
78
+ # Notify about dispatch on the events bus
79
+ Karafka.monitor.instrument(
80
+ 'dead_letter_queue.dispatched',
81
+ caller: self,
82
+ message: skippable_message
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Strategies
6
+ # Same as pure dead letter queue but we do not marked failed message as consumed
7
+ module DlqMom
8
+ include Dlq
9
+
10
+ # Apply strategy when dlq is on with manual offset management
11
+ FEATURES = %i[
12
+ dead_letter_queue
13
+ manual_offset_management
14
+ ].freeze
15
+
16
+ # When manual offset management is on, we do not mark anything as consumed automatically
17
+ # and we rely on the user to figure things out
18
+ def handle_after_consume
19
+ return if revoked?
20
+
21
+ if coordinator.success?
22
+ coordinator.pause_tracker.reset
23
+ elsif coordinator.pause_tracker.attempt <= topic.dead_letter_queue.max_retries
24
+ retry_after_pause
25
+ # If we've reached number of retries that we could, we need to skip the first message
26
+ # that was not marked as consumed, pause and continue, while also moving this message
27
+ # to the dead topic
28
+ else
29
+ # We reset the pause to indicate we will now consider it as "ok".
30
+ coordinator.pause_tracker.reset
31
+
32
+ skippable_message, = find_skippable_message
33
+
34
+ dispatch_to_dlq(skippable_message)
35
+
36
+ # Save the next offset we want to go with after moving given message to DLQ
37
+ # Without this, we would not be able to move forward and we would end up
38
+ # in an infinite loop trying to un-pause from the message we've already processed
39
+ # Of course, since it's a MoM a rebalance or kill, will move it back as no
40
+ # offsets are being committed
41
+ coordinator.seek_offset = skippable_message.offset + 1
42
+
43
+ pause(coordinator.seek_offset, nil, false)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ module Strategies
6
+ # When using manual offset management, we do not mark as consumed after successful processing
7
+ module Mom
8
+ include Default
9
+
10
+ # Apply strategy when only manual offset management is turned on
11
+ FEATURES = %i[
12
+ manual_offset_management
13
+ ].freeze
14
+
15
+ # When manual offset management is on, we do not mark anything as consumed automatically
16
+ # and we rely on the user to figure things out
17
+ def handle_after_consume
18
+ return if revoked?
19
+
20
+ if coordinator.success?
21
+ coordinator.pause_tracker.reset
22
+ else
23
+ retry_after_pause
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Processing
5
+ # Selector of appropriate processing strategy matching topic combinations
6
+ class StrategySelector
7
+ attr_reader :strategies
8
+
9
+ # Features we support in the OSS offering.
10
+ SUPPORTED_FEATURES = %i[
11
+ active_job
12
+ manual_offset_management
13
+ dead_letter_queue
14
+ ].freeze
15
+
16
+ def initialize
17
+ # We load them once for performance reasons not to do too many lookups
18
+ @strategies = find_all
19
+ end
20
+
21
+ # @param topic [Karafka::Routing::Topic] topic with settings based on which we find strategy
22
+ # @return [Module] module with proper strategy
23
+ def find(topic)
24
+ feature_set = SUPPORTED_FEATURES.map do |feature_name|
25
+ topic.public_send("#{feature_name}?") ? feature_name : nil
26
+ end
27
+
28
+ feature_set.compact!
29
+
30
+ @strategies.find do |strategy|
31
+ strategy::FEATURES.sort == feature_set.sort
32
+ end || raise(Errors::StrategyNotFoundError, topic.name)
33
+ end
34
+
35
+ private
36
+
37
+ # @return [Array<Module>] available strategies
38
+ def find_all
39
+ Strategies
40
+ .constants
41
+ .delete_if { |k| k == :Base }
42
+ .map { |k| Strategies.const_get(k) }
43
+ .uniq
44
+ end
45
+ end
46
+ end
47
+ end
@@ -25,7 +25,7 @@ module Karafka
25
25
  # @param jobs_queue [JobsQueue]
26
26
  # @return [Worker]
27
27
  def initialize(jobs_queue)
28
- @id = SecureRandom.uuid
28
+ @id = SecureRandom.hex(6)
29
29
  @jobs_queue = jobs_queue
30
30
  end
31
31
 
@@ -46,9 +46,9 @@ module Karafka
46
46
  def process
47
47
  job = @jobs_queue.pop
48
48
 
49
- if job
50
- instrument_details = { caller: self, job: job, jobs_queue: @jobs_queue }
49
+ instrument_details = { caller: self, job: job, jobs_queue: @jobs_queue }
51
50
 
51
+ if job
52
52
  Karafka.monitor.instrument('worker.process', instrument_details)
53
53
 
54
54
  Karafka.monitor.instrument('worker.processed', instrument_details) do
@@ -76,12 +76,17 @@ module Karafka
76
76
  Karafka.monitor.instrument(
77
77
  'error.occurred',
78
78
  caller: self,
79
+ job: job,
80
+ jobs_queue: @jobs_queue,
79
81
  error: e,
80
82
  type: 'worker.process.error'
81
83
  )
82
84
  ensure
83
85
  # job can be nil when the queue is being closed
84
86
  @jobs_queue.complete(job) if job
87
+
88
+ # Always publish info, that we completed all the work despite its result
89
+ Karafka.monitor.instrument('worker.completed', instrument_details)
85
90
  end
86
91
  end
87
92
  end
@@ -2,13 +2,7 @@
2
2
 
3
3
  # This file contains Railtie for auto-configuration
4
4
 
5
- rails = false
6
-
7
- begin
8
- require 'rails'
9
-
10
- rails = true
11
- rescue LoadError
5
+ unless Karafka.rails?
12
6
  # Without defining this in any way, Zeitwerk ain't happy so we do it that way
13
7
  module Karafka
14
8
  class Railtie
@@ -16,10 +10,7 @@ rescue LoadError
16
10
  end
17
11
  end
18
12
 
19
- if rails
20
- # Load Karafka
21
- require 'karafka'
22
-
13
+ if Karafka.rails?
23
14
  # Load ActiveJob adapter
24
15
  require 'active_job/karafka'
25
16
 
@@ -47,10 +38,17 @@ if rails
47
38
 
48
39
  next unless Rails.env.development?
49
40
  next unless ENV.key?('KARAFKA_CLI')
41
+ # If we are already publishing to STDOUT, no need to add it again.
42
+ # If added again, would print stuff twice
43
+ next if ActiveSupport::Logger.logger_outputs_to?(Rails.logger, $stdout)
44
+
45
+ logger = ActiveSupport::Logger.new($stdout)
46
+ # Inherit the logger level from Rails, otherwise would always run with the debug level
47
+ logger.level = Rails.logger.level
50
48
 
51
49
  Rails.logger.extend(
52
50
  ActiveSupport::Logger.broadcast(
53
- ActiveSupport::Logger.new($stdout)
51
+ logger
54
52
  )
55
53
  )
56
54
  end
@@ -74,11 +72,25 @@ if rails
74
72
  ::Karafka::App.monitor.subscribe('connection.listener.fetch_loop') do
75
73
  # Reload code each time there is a change in the code
76
74
  next unless Rails.application.reloaders.any?(&:updated?)
75
+ # If consumer persistence is enabled, no reason to reload because we will still keep
76
+ # old consumer instances in memory.
77
+ next if Karafka::App.config.consumer_persistence
77
78
 
78
79
  Rails.application.reloader.reload!
79
80
  end
80
81
  end
81
82
 
83
+ initializer 'karafka.release_active_record_connections' do
84
+ ActiveSupport.on_load(:active_record) do
85
+ ::Karafka::App.monitor.subscribe('worker.completed') do
86
+ # Always release the connection after processing is done. Otherwise thread may hang
87
+ # blocking the reload and further processing
88
+ # @see https://github.com/rails/rails/issues/44183
89
+ ActiveRecord::Base.clear_active_connections!
90
+ end
91
+ end
92
+ end
93
+
82
94
  initializer 'karafka.require_karafka_boot_file' do |app|
83
95
  rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
84
96
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ # Allows us to get track of which consumer groups, subscription groups and topics are enabled
6
+ # or disabled via CLI
7
+ class ActivityManager
8
+ # Supported types of inclusions and exclusions
9
+ SUPPORTED_TYPES = %i[
10
+ consumer_groups
11
+ subscription_groups
12
+ topics
13
+ ].freeze
14
+
15
+ def initialize
16
+ @included = Hash.new { |h, k| h[k] = [] }
17
+ @excluded = Hash.new { |h, k| h[k] = [] }
18
+ end
19
+
20
+ # Adds resource to included/active
21
+ # @param type [Symbol] type for inclusion
22
+ # @param name [String] name of the element
23
+ def include(type, name)
24
+ validate!(type)
25
+
26
+ @included[type] << name
27
+ end
28
+
29
+ # Adds resource to excluded
30
+ # @param type [Symbol] type for inclusion
31
+ # @param name [String] name of the element
32
+ def exclude(type, name)
33
+ validate!(type)
34
+
35
+ @excluded[type] << name
36
+ end
37
+
38
+ # @param type [Symbol] type for inclusion
39
+ # @param name [String] name of the element
40
+ # @return [Boolean] is the given resource active or not
41
+ def active?(type, name)
42
+ validate!(type)
43
+
44
+ included = @included[type]
45
+ excluded = @excluded[type]
46
+
47
+ # If nothing defined, all active by default
48
+ return true if included.empty? && excluded.empty?
49
+ # Inclusion supersedes exclusion in case someone wrote both
50
+ return true if !included.empty? && included.include?(name)
51
+
52
+ # If there are exclusions but our is not excluded and no inclusions or included, it's ok
53
+ !excluded.empty? &&
54
+ !excluded.include?(name) &&
55
+ (included.empty? || included.include?(name))
56
+ end
57
+
58
+ # @return [Hash] accumulated data in a hash for validations
59
+ def to_h
60
+ (
61
+ SUPPORTED_TYPES.map { |type| ["include_#{type}".to_sym, @included[type]] } +
62
+ SUPPORTED_TYPES.map { |type| ["exclude_#{type}".to_sym, @excluded[type]] }
63
+ ).to_h
64
+ end
65
+
66
+ # Clears the manager
67
+ def clear
68
+ @included.clear
69
+ @excluded.clear
70
+ end
71
+
72
+ private
73
+
74
+ # Checks if the type we want to register is supported
75
+ #
76
+ # @param type [Symbol] type for inclusion
77
+ def validate!(type)
78
+ return if SUPPORTED_TYPES.include?(type)
79
+
80
+ raise(::Karafka::Errors::UnsupportedCaseError, type)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -33,7 +33,16 @@ module Karafka
33
33
  instance_eval(&block)
34
34
 
35
35
  each do |consumer_group|
36
+ # Validate consumer group settings
36
37
  Contracts::ConsumerGroup.new.validate!(consumer_group.to_h)
38
+
39
+ # and then its topics settings
40
+ consumer_group.topics.each do |topic|
41
+ Contracts::Topic.new.validate!(topic.to_h)
42
+ end
43
+
44
+ # Initialize subscription groups after all the routing is done
45
+ consumer_group.subscription_groups
37
46
  end
38
47
  end
39
48
 
@@ -66,6 +75,17 @@ module Karafka
66
75
  end
67
76
  end
68
77
 
78
+ # Handles the simple routing case where we create one consumer group and allow for further
79
+ # subscription group customization
80
+ # @param subscription_group_name [String, Symbol] subscription group id. When not provided,
81
+ # a random uuid will be used
82
+ # @param block [Proc] further topics definitions
83
+ def subscription_group(subscription_group_name = SubscriptionGroup.id, &block)
84
+ consumer_group('app') do
85
+ target.public_send(:subscription_group=, subscription_group_name.to_s, &block)
86
+ end
87
+ end
88
+
69
89
  # In case we use simple style of routing, all topics will be assigned to the same consumer
70
90
  # group that will be based on the client_id
71
91
  #