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,19 +10,28 @@ module Karafka
10
10
  class ConsumerGroup
11
11
  attr_reader :id, :topics, :name
12
12
 
13
+ # This is a "virtual" attribute that is not building subscription groups.
14
+ # It allows us to store the "current" subscription group defined in the routing
15
+ # This subscription group id is then injected into topics, so we can compute the subscription
16
+ # groups
17
+ attr_accessor :current_subscription_group_id
18
+
13
19
  # @param name [String, Symbol] raw name of this consumer group. Raw means, that it does not
14
20
  # yet have an application client_id namespace, this will be added here by default.
15
21
  # We add it to make a multi-system development easier for people that don't use
16
22
  # kafka and don't understand the concept of consumer groups.
17
23
  def initialize(name)
18
- @name = name
24
+ @name = name.to_s
19
25
  @id = Karafka::App.config.consumer_mapper.call(name)
20
26
  @topics = Topics.new([])
27
+ # Initialize the subscription group so there's always a value for it, since even if not
28
+ # defined directly, a subscription group will be created
29
+ @current_subscription_group_id = SubscriptionGroup.id
21
30
  end
22
31
 
23
32
  # @return [Boolean] true if this consumer group should be active in our current process
24
33
  def active?
25
- Karafka::Server.consumer_groups.include?(name)
34
+ Karafka::App.config.internal.routing.activity_manager.active?(:consumer_groups, name)
26
35
  end
27
36
 
28
37
  # Builds a topic representation inside of a current consumer group route
@@ -32,13 +41,38 @@ module Karafka
32
41
  def topic=(name, &block)
33
42
  topic = Topic.new(name, self)
34
43
  @topics << Proxy.new(topic, &block).target
35
- @topics.last
44
+ built_topic = @topics.last
45
+ # We overwrite it conditionally in case it was not set by the user inline in the topic
46
+ # block definition
47
+ built_topic.subscription_group ||= current_subscription_group_id
48
+ built_topic
49
+ end
50
+
51
+ # Assigns the current subscription group id based on the defined one and allows for further
52
+ # topic definition
53
+ # @param name [String, Symbol] name of the current subscription group
54
+ # @param block [Proc] block that may include topics definitions
55
+ def subscription_group=(name = SubscriptionGroup.id, &block)
56
+ # We cast it here, so the routing supports symbol based but that's anyhow later on
57
+ # validated as a string
58
+ @current_subscription_group_id = name
59
+
60
+ Proxy.new(self, &block)
61
+
62
+ # We need to reset the current subscription group after it is used, so it won't leak
63
+ # outside to other topics that would be defined without a defined subscription group
64
+ @current_subscription_group_id = SubscriptionGroup.id
36
65
  end
37
66
 
38
67
  # @return [Array<Routing::SubscriptionGroup>] all the subscription groups build based on
39
68
  # the consumer group topics
40
69
  def subscription_groups
41
- App.config.internal.routing.subscription_groups_builder.call(topics)
70
+ @subscription_groups ||= App
71
+ .config
72
+ .internal
73
+ .routing
74
+ .subscription_groups_builder
75
+ .call(topics)
42
76
  end
43
77
 
44
78
  # Hashed version of consumer group that can be used for validation purposes
@@ -12,16 +12,6 @@ module Karafka
12
12
  # raw_consumer_group_name
13
13
  # end
14
14
  # end
15
- #
16
- # @example Mapper for replacing "_" with "." in topic names
17
- # class MyMapper
18
- # def call(raw_consumer_group_name)
19
- # [
20
- # Karafka::Helpers::Inflector.map(Karafka::App.config.client_id.to_s),
21
- # raw_consumer_group_name
22
- # ].join('_').gsub('_', '.')
23
- # end
24
- # end
25
15
  class ConsumerMapper
26
16
  # @param raw_consumer_group_name [String, Symbol] string or symbolized consumer group name
27
17
  # @return [String] remapped final consumer group name
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ActiveJob < Base
7
+ # Routing extensions for ActiveJob
8
+ module Builder
9
+ # This method simplifies routes definition for ActiveJob topics / queues by
10
+ # auto-injecting the consumer class
11
+ #
12
+ # @param name [String, Symbol] name of the topic where ActiveJobs jobs should go
13
+ # @param block [Proc] block that we can use for some extra configuration
14
+ def active_job_topic(name, &block)
15
+ topic(name) do
16
+ consumer App.config.internal.active_job.consumer_class
17
+ active_job true
18
+
19
+ # This is handled by our custom ActiveJob consumer
20
+ # Without this, default behaviour would cause messages to skip upon shutdown as the
21
+ # offset would be committed for the last message
22
+ manual_offset_management true
23
+
24
+ next unless block
25
+
26
+ instance_eval(&block)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ActiveJob < Base
7
+ # Config for ActiveJob usage
8
+ Config = Struct.new(
9
+ :active,
10
+ keyword_init: true
11
+ ) { alias_method :active?, :active }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ActiveJob < Base
7
+ # Rules around using ActiveJob routing - basically you need to have ActiveJob available
8
+ # in order to be able to use active job routing
9
+ class Contract < Contracts::Base
10
+ configure do |config|
11
+ config.error_messages = YAML.safe_load(
12
+ File.read(
13
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
14
+ )
15
+ ).fetch('en').fetch('validations').fetch('topic')
16
+ end
17
+
18
+ virtual do |data, errors|
19
+ next unless errors.empty?
20
+ next unless data[:active_job][:active]
21
+ # One should not define active job jobs without ActiveJob being available for usage
22
+ next if Object.const_defined?('ActiveJob::Base')
23
+
24
+ [[%i[consumer], :active_job_missing]]
25
+ end
26
+
27
+ # ActiveJob needs to always run with manual offset management
28
+ # Automatic offset management cannot work with ActiveJob. Otherwise we could mark as
29
+ # consumed jobs that did not run because of shutdown.
30
+ virtual do |data, errors|
31
+ next unless errors.empty?
32
+ next unless data[:active_job][:active]
33
+ next if data[:manual_offset_management][:active]
34
+
35
+ [[%i[manual_offset_management], :must_be_enabled]]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ActiveJob < Base
7
+ # Topic extensions to be able to check if given topic is ActiveJob topic
8
+ module Topic
9
+ # @param active [Boolean] should this topic be considered one working with ActiveJob
10
+ #
11
+ # @note Since this feature supports only one setting (active), we can use the old API
12
+ # where the boolean would be an argument
13
+ def active_job(active = false)
14
+ @active_job ||= Config.new(active: active)
15
+ end
16
+
17
+ # @return [Boolean] is this an ActiveJob topic
18
+ def active_job?
19
+ active_job.active?
20
+ end
21
+
22
+ # @return [Hash] topic with all its native configuration options plus active job
23
+ # namespace settings
24
+ def to_h
25
+ super.merge(
26
+ active_job: active_job.to_h
27
+ ).freeze
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ # Active-Job related components
7
+ # @note We can load it always, despite someone not using ActiveJob as it just adds a method
8
+ # to the routing, without actually breaking anything.
9
+ class ActiveJob < Base
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Base
7
+ # Routing builder expander that injects feature related drawing operations into it
8
+ class Expander < Module
9
+ # @param scope [Module] feature scope in which contract and other things should be
10
+ # @return [Expander] builder expander instance
11
+ def initialize(scope)
12
+ super()
13
+ @scope = scope
14
+ end
15
+
16
+ # Builds anonymous module that alters how `#draw` behaves allowing the feature contracts
17
+ # to run.
18
+ # @param mod [::Karafka::Routing::Builder] builder we will prepend to
19
+ def prepended(mod)
20
+ super
21
+
22
+ mod.prepend(prepended_module)
23
+ end
24
+
25
+ private
26
+
27
+ # @return [Module] builds an anonymous module with `#draw` that will alter the builder
28
+ # `#draw` allowing to run feature context aware code.
29
+ def prepended_module
30
+ scope = @scope
31
+
32
+ Module.new do
33
+ # Runs validations related to this feature on a topic
34
+ #
35
+ # @param block [Proc] routing defining block
36
+ define_method :draw do |&block|
37
+ result = super(&block)
38
+
39
+ each do |consumer_group|
40
+ consumer_group.topics.each do |topic|
41
+ scope::Contract.new.validate!(topic.to_h)
42
+ end
43
+ end
44
+
45
+ result
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ # Namespace for all the topic related features we support
6
+ #
7
+ # @note Not all the Karafka features need to be defined here as only those that have routing
8
+ # or other extensions need to be here. That is why we keep (for now) features under the
9
+ # routing namespace.
10
+ module Features
11
+ # Base for all the features
12
+ class Base
13
+ class << self
14
+ # Extends topic and builder with given feature API
15
+ def activate
16
+ Topic.prepend(self::Topic) if const_defined?('Topic', false)
17
+ Proxy.prepend(self::Builder) if const_defined?('Builder', false)
18
+ Builder.prepend(self::Builder) if const_defined?('Builder', false)
19
+ Builder.prepend(Base::Expander.new(self)) if const_defined?('Contract', false)
20
+ end
21
+
22
+ # Loads all the features and activates them
23
+ def load_all
24
+ ObjectSpace
25
+ .each_object(Class)
26
+ .select { |klass| klass < self }
27
+ .sort_by(&:to_s)
28
+ .each(&:activate)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class DeadLetterQueue < Base
7
+ # Config for dead letter queue feature
8
+ Config = Struct.new(
9
+ :active,
10
+ # We add skip variants but in regular we support only `:one`
11
+ :max_retries,
12
+ # To what topic the skipped messages should be moved
13
+ :topic,
14
+ keyword_init: true
15
+ ) { alias_method :active?, :active }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class DeadLetterQueue < Base
7
+ # Rules around dead letter queue settings
8
+ class Contract < Contracts::Base
9
+ configure do |config|
10
+ config.error_messages = YAML.safe_load(
11
+ File.read(
12
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
13
+ )
14
+ ).fetch('en').fetch('validations').fetch('topic')
15
+ end
16
+
17
+ nested :dead_letter_queue do
18
+ required(:active) { |val| [true, false].include?(val) }
19
+ required(:max_retries) { |val| val.is_a?(Integer) && val >= 0 }
20
+ end
21
+
22
+ # Validate topic name only if dlq is active
23
+ virtual do |data, errors|
24
+ next unless errors.empty?
25
+
26
+ dead_letter_queue = data[:dead_letter_queue]
27
+
28
+ next unless dead_letter_queue[:active]
29
+
30
+ topic = dead_letter_queue[:topic]
31
+
32
+ # When topic is set to false, it means we just want to skip dispatch on DLQ
33
+ next if topic == false
34
+ next if topic.is_a?(String) && Contracts::TOPIC_REGEXP.match?(topic)
35
+
36
+ [[%i[dead_letter_queue topic], :format]]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class DeadLetterQueue < Base
7
+ # DLQ topic extensions
8
+ module Topic
9
+ # After how many retries should be move data to DLQ
10
+ DEFAULT_MAX_RETRIES = 3
11
+
12
+ private_constant :DEFAULT_MAX_RETRIES
13
+
14
+ # @param max_retries [Integer] after how many retries should we move data to dlq
15
+ # @param topic [String, false] where the messages should be moved if failing or false
16
+ # if we do not want to move it anywhere and just skip
17
+ # @return [Config] defined config
18
+ def dead_letter_queue(max_retries: DEFAULT_MAX_RETRIES, topic: nil)
19
+ @dead_letter_queue ||= Config.new(
20
+ active: !topic.nil?,
21
+ max_retries: max_retries,
22
+ topic: topic
23
+ )
24
+ end
25
+
26
+ # @return [Boolean] is the dlq active or not
27
+ def dead_letter_queue?
28
+ dead_letter_queue.active?
29
+ end
30
+
31
+ # @return [Hash] topic with all its native configuration options plus dlq settings
32
+ def to_h
33
+ super.merge(
34
+ dead_letter_queue: dead_letter_queue.to_h
35
+ ).freeze
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ # This feature allows to continue processing when encountering errors.
7
+ # After certain number of retries, given messages will be moved to alternative topic,
8
+ # unclogging processing.
9
+ #
10
+ # @note This feature has an expanded version in the Pro mode. We do not use a new feature
11
+ # injection in Pro (topic settings)
12
+ class DeadLetterQueue < Base
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Declaratives < Base
7
+ # Config for declarative topics feature
8
+ Config = Struct.new(
9
+ :active,
10
+ :partitions,
11
+ :replication_factor,
12
+ :details,
13
+ keyword_init: true
14
+ ) { alias_method :active?, :active }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Declaratives < Base
7
+ # Basic validation of the Kafka expected config details
8
+ class Contract < Contracts::Base
9
+ configure do |config|
10
+ config.error_messages = YAML.safe_load(
11
+ File.read(
12
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
13
+ )
14
+ ).fetch('en').fetch('validations').fetch('topic')
15
+ end
16
+
17
+ nested :declaratives do
18
+ required(:active) { |val| [true, false].include?(val) }
19
+ required(:partitions) { |val| val.is_a?(Integer) && val.positive? }
20
+ required(:replication_factor) { |val| val.is_a?(Integer) && val.positive? }
21
+ required(:details) do |val|
22
+ val.is_a?(Hash) &&
23
+ val.keys.all? { |key| key.is_a?(Symbol) }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class Declaratives < Base
7
+ # Extension for managing Kafka topic configuration
8
+ module Topic
9
+ # @param active [Boolean] is the topic structure management feature active
10
+ # @param partitions [Integer]
11
+ # @param replication_factor [Integer]
12
+ # @param details [Hash] extra configuration for the topic
13
+ # @return [Config] defined structure
14
+ def config(active: true, partitions: 1, replication_factor: 1, **details)
15
+ @declaratives ||= Config.new(
16
+ active: active,
17
+ partitions: partitions,
18
+ replication_factor: replication_factor,
19
+ details: details
20
+ )
21
+ end
22
+
23
+ # @return [Config] config details
24
+ def declaratives
25
+ config
26
+ end
27
+
28
+ # @return [true] declaratives is always active
29
+ def declaratives?
30
+ declaratives.active?
31
+ end
32
+
33
+ # @return [Hash] topic with all its native configuration options plus declaratives
34
+ # settings
35
+ def to_h
36
+ super.merge(
37
+ declaratives: declaratives.to_h
38
+ ).freeze
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ # This feature allows to store per topic structure that can be later on used to bootstrap
7
+ # topics structure for test/development, etc. This allows to share the same set of settings
8
+ # for topics despite the environment. Pretty much similar to how the `structure.sql` or
9
+ # `schema.rb` operate for SQL dbs.
10
+ class Declaratives < Base
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ManualOffsetManagement < Base
7
+ # Config for manual offset management feature
8
+ Config = Struct.new(
9
+ :active,
10
+ keyword_init: true
11
+ ) { alias_method :active?, :active }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ManualOffsetManagement < Base
7
+ # Rules around manual offset management settings
8
+ class Contract < Contracts::Base
9
+ configure do |config|
10
+ config.error_messages = YAML.safe_load(
11
+ File.read(
12
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
13
+ )
14
+ ).fetch('en').fetch('validations').fetch('topic')
15
+ end
16
+
17
+ nested :manual_offset_management do
18
+ required(:active) { |val| [true, false].include?(val) }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ class ManualOffsetManagement < Base
7
+ # Topic extensions to be able to manage manual offset management settings
8
+ module Topic
9
+ # @param active [Boolean] should we stop managing the offset in Karafka and make the user
10
+ # responsible for marking messages as consumed.
11
+ # @return [Config] defined config
12
+ #
13
+ # @note Since this feature supports only one setting (active), we can use the old API
14
+ # where the boolean would be an argument
15
+ def manual_offset_management(active = false)
16
+ @manual_offset_management ||= Config.new(active: active)
17
+ end
18
+
19
+ # @return [Boolean] is manual offset management enabled for a given topic
20
+ def manual_offset_management?
21
+ manual_offset_management.active?
22
+ end
23
+
24
+ # @return [Hash] topic with all its native configuration options plus manual offset
25
+ # management namespace settings
26
+ def to_h
27
+ super.merge(
28
+ manual_offset_management: manual_offset_management.to_h
29
+ ).freeze
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Routing
5
+ module Features
6
+ # All the things needed to be able to manage manual offset management from the routing
7
+ # perspective.
8
+ #
9
+ # Manual offset management allows users to completely disable automatic management of the
10
+ # offset. This can be used for implementing long-living window operations and other things
11
+ # where we do not want to commit the offset with each batch.
12
+ #
13
+ # Not all the Karafka and Karafka Pro features may be compatible with this feature being on.
14
+ class ManualOffsetManagement < Base
15
+ end
16
+ end
17
+ end
18
+ end