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,34 @@
1
+ en:
2
+ validations:
3
+ topic:
4
+ virtual_partitions.partitioner_respond_to_call: needs to be defined and needs to respond to `#call`
5
+ virtual_partitions.max_partitions_format: needs to be equal or more than 1
6
+
7
+ long_running_job.active_format: needs to be either true or false
8
+
9
+ dead_letter_queue_with_virtual_partitions: when using Dead Letter Queue with Virtual Partitions, at least one retry is required.
10
+
11
+ throttling.active_format: needs to be either true or false
12
+ throttling.limit_format: needs to be equal or more than 1
13
+ throttling.interval_format: needs to be equal or more than 1
14
+
15
+ filtering.active_missing: needs to be present
16
+ filtering.factory_format: 'needs to respond to #call'
17
+ filtering.factories_format: 'needs to contain only factories responding to #call'
18
+ filtering.active_format: 'needs to be boolean'
19
+
20
+ expiring.ttl_format: 'needs to be equal or more than 0 and an integer'
21
+ expiring.active_format: 'needs to be boolean'
22
+
23
+ delaying.delay_format: 'needs to be equal or more than 0 and an integer'
24
+ delaying.active_format: 'needs to be boolean'
25
+
26
+ config:
27
+ encryption.active_format: 'needs to be either true or false'
28
+ encryption.public_key_invalid: 'is not a valid public RSA key'
29
+ encryption.public_key_needs_to_be_public: 'is a private RSA key not a public one'
30
+ encryption.private_keys_format: 'needs to be a hash of version and private key value'
31
+ encryption.private_keys_need_to_be_private: 'all keys need to be private'
32
+ encryption.version_format: must be a non-empty string
33
+ encryption.public_key_format: 'is not a valid public RSA key'
34
+ encryption.private_keys_invalid: 'contains an invalid private RSA key string'
data/docker-compose.yml CHANGED
@@ -3,8 +3,10 @@ services:
3
3
  zookeeper:
4
4
  container_name: karafka_20_zookeeper
5
5
  image: wurstmeister/zookeeper
6
+ restart: on-failure
6
7
  ports:
7
8
  - '2181:2181'
9
+
8
10
  kafka:
9
11
  container_name: karafka_20_kafka
10
12
  image: wurstmeister/kafka
@@ -16,38 +18,10 @@ services:
16
18
  KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
17
19
  KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
18
20
  KAFKA_CREATE_TOPICS:
19
- "integrations_00_02:2:1,\
20
- integrations_01_02:2:1,\
21
- integrations_02_02:2:1,\
22
- integrations_03_02:2:1,\
23
- integrations_04_02:2:1,\
24
- integrations_05_02:2:1,\
25
- integrations_06_02:2:1,\
26
- integrations_07_02:2:1,\
27
- integrations_08_02:2:1,\
28
- integrations_09_02:2:1,\
29
- integrations_10_02:2:1,\
30
- integrations_11_02:2:1,\
31
- integrations_12_02:2:1,\
32
- integrations_13_02:2:1,\
33
- integrations_14_02:2:1,\
34
- integrations_15_02:2:1,\
35
- integrations_16_02:2:1,\
36
- integrations_17_02:2:1,\
37
- integrations_18_02:2:1,\
38
- integrations_19_02:2:1,\
39
- integrations_20_02:2:1,\
40
- integrations_21_02:2:1,\
41
- integrations_00_03:3:1,\
42
- integrations_01_03:3:1,\
43
- integrations_02_03:3:1,\
44
- integrations_03_03:3:1,\
45
- integrations_04_03:3:1,\
46
- integrations_00_10:10:1,\
47
- integrations_01_10:10:1,\
48
- benchmarks_00_01:1:1,\
21
+ "benchmarks_00_01:1:1,\
49
22
  benchmarks_00_05:5:1,\
50
23
  benchmarks_01_05:5:1,\
51
24
  benchmarks_00_10:10:1"
52
25
  volumes:
53
26
  - /var/run/docker.sock:/var/run/docker.sock
27
+ restart: on-failure
data/karafka.gemspec CHANGED
@@ -10,31 +10,38 @@ Gem::Specification.new do |spec|
10
10
  spec.version = ::Karafka::VERSION
11
11
  spec.platform = Gem::Platform::RUBY
12
12
  spec.authors = ['Maciej Mensfeld']
13
- spec.email = %w[maciej@mensfeld.pl]
13
+ spec.email = %w[contact@karafka.io]
14
14
  spec.homepage = 'https://karafka.io'
15
- spec.summary = 'Efficient Kafka processing framework for Ruby and Rails'
16
- spec.description = 'Framework used to simplify Apache Kafka based Ruby applications development'
17
- spec.licenses = ['LGPL-3.0', 'Commercial']
15
+ spec.licenses = %w[LGPL-3.0 Commercial]
16
+ spec.summary = 'Karafka is Ruby and Rails efficient Kafka processing framework.'
17
+ spec.description = <<-DESC
18
+ Karafka is Ruby and Rails efficient Kafka processing framework.
18
19
 
19
- spec.add_dependency 'karafka-core', '>= 2.0.2', '< 3.0.0'
20
- spec.add_dependency 'rdkafka', '>= 0.12'
20
+ Karafka allows you to capture everything that happens in your systems in large scale,
21
+ without having to focus on things that are not your business domain.
22
+ DESC
23
+
24
+ spec.add_dependency 'karafka-core', '>= 2.0.12', '< 3.0.0'
21
25
  spec.add_dependency 'thor', '>= 0.20'
22
- spec.add_dependency 'waterdrop', '>= 2.4.1', '< 3.0.0'
26
+ spec.add_dependency 'waterdrop', '>= 2.4.10', '< 3.0.0'
23
27
  spec.add_dependency 'zeitwerk', '~> 2.3'
24
28
 
25
- spec.required_ruby_version = '>= 2.7.0'
26
-
27
29
  if $PROGRAM_NAME.end_with?('gem')
28
30
  spec.signing_key = File.expand_path('~/.ssh/gem-private_key.pem')
29
31
  end
30
32
 
31
- spec.cert_chain = %w[certs/mensfeld.pem]
33
+ spec.cert_chain = %w[certs/cert_chain.pem]
32
34
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
33
35
  spec.executables = %w[karafka]
34
36
  spec.require_paths = %w[lib]
35
37
 
36
38
  spec.metadata = {
39
+ 'funding_uri' => 'https://karafka.io/#become-pro',
40
+ 'homepage_uri' => 'https://karafka.io',
41
+ 'changelog_uri' => 'https://github.com/karafka/karafka/blob/master/CHANGELOG.md',
42
+ 'bug_tracker_uri' => 'https://github.com/karafka/karafka/issues',
37
43
  'source_code_uri' => 'https://github.com/karafka/karafka',
44
+ 'documentation_uri' => 'https://karafka.io/docs',
38
45
  'rubygems_mfa_required' => 'true'
39
46
  }
40
47
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
- require 'active_job'
4
+ # Do not load active job if already loaded
5
+ require 'active_job' unless Object.const_defined?('ActiveJob')
6
+
5
7
  require_relative 'queue_adapters/karafka_adapter'
6
8
 
7
9
  module ActiveJob
@@ -10,12 +12,6 @@ begin
10
12
  module Karafka
11
13
  end
12
14
  end
13
-
14
- # We extend routing builder by adding a simple wrapper for easier jobs topics defining
15
- # This needs to be extended here as it is going to be used in karafka routes, hence doing that in
16
- # the railtie initializer would be too late
17
- ::Karafka::Routing::Builder.include ::Karafka::ActiveJob::Routing::Extensions
18
- ::Karafka::Routing::Proxy.include ::Karafka::ActiveJob::Routing::Extensions
19
15
  rescue LoadError
20
16
  # We extend ActiveJob stuff in the railtie
21
17
  end
@@ -11,7 +11,13 @@ module ActiveJob
11
11
  #
12
12
  # @param job [Object] job that should be enqueued
13
13
  def enqueue(job)
14
- ::Karafka::App.config.internal.active_job.dispatcher.call(job)
14
+ ::Karafka::App.config.internal.active_job.dispatcher.dispatch(job)
15
+ end
16
+
17
+ # Enqueues multiple jobs in one go
18
+ # @param jobs [Array<Object>] jobs that we want to enqueue
19
+ def enqueue_all(jobs)
20
+ ::Karafka::App.config.internal.active_job.dispatcher.dispatch_many(jobs)
15
21
  end
16
22
 
17
23
  # Raises info, that Karafka backend does not support scheduling jobs
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Karafka
4
+ # Namespace for all the ActiveJob related things from within Karafka
4
5
  module ActiveJob
5
6
  # This is the consumer for ActiveJob that eats the messages enqueued with it one after another.
6
7
  # It marks the offset after each message, so we make sure, none of the jobs is executed twice
@@ -11,16 +12,38 @@ module Karafka
11
12
  messages.each do |message|
12
13
  break if Karafka::App.stopping?
13
14
 
14
- ::ActiveJob::Base.execute(
15
- # We technically speaking could set this as deserializer and reference it from the
16
- # message instead of using the `#raw_payload`. This is not done on purpose to simplify
17
- # the ActiveJob setup here
18
- ::ActiveSupport::JSON.decode(message.raw_payload)
19
- )
15
+ consume_job(message)
20
16
 
21
17
  mark_as_consumed(message)
22
18
  end
23
19
  end
20
+
21
+ private
22
+
23
+ # Consumes a message with the job and runs needed instrumentation
24
+ #
25
+ # @param job_message [Karafka::Messages::Message] message with active job
26
+ def consume_job(job_message)
27
+ with_deserialized_job(job_message) do |job|
28
+ tags.add(:job_class, job['job_class'])
29
+
30
+ payload = { caller: self, job: job, message: job_message }
31
+
32
+ # We publish both to make it consistent with `consumer.x` events
33
+ Karafka.monitor.instrument('active_job.consume', payload)
34
+ Karafka.monitor.instrument('active_job.consumed', payload) do
35
+ ::ActiveJob::Base.execute(job)
36
+ end
37
+ end
38
+ end
39
+
40
+ # @param job_message [Karafka::Messages::Message] message with active job
41
+ def with_deserialized_job(job_message)
42
+ # We technically speaking could set this as deserializer and reference it from the
43
+ # message instead of using the `#raw_payload`. This is not done on purpose to simplify
44
+ # the ActiveJob setup here
45
+ yield ::ActiveSupport::JSON.decode(job_message.raw_payload)
46
+ end
24
47
  end
25
48
  end
26
49
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module ActiveJob
5
+ module CurrentAttributes
6
+ # Module expanding the job deserialization to extract current attributes and load them
7
+ # for the time of the job execution
8
+ module Loading
9
+ # @param job_message [Karafka::Messages::Message] message with active job
10
+ def with_deserialized_job(job_message)
11
+ super(job_message) do |job|
12
+ resetable = []
13
+
14
+ _cattr_klasses.each do |key, cattr_klass_str|
15
+ next unless job.key?(key)
16
+
17
+ attributes = job.delete(key)
18
+
19
+ cattr_klass = cattr_klass_str.constantize
20
+
21
+ attributes.each do |name, value|
22
+ cattr_klass.public_send("#{name}=", value)
23
+ end
24
+
25
+ resetable << cattr_klass
26
+ end
27
+
28
+ yield(job)
29
+
30
+ resetable.each(&:reset)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module ActiveJob
5
+ module CurrentAttributes
6
+ # Module adding the current attributes persistence into the ActiveJob jobs
7
+ module Persistence
8
+ # Alters the job serialization to inject the current attributes into the json before we
9
+ # send it to Kafka
10
+ #
11
+ # @param job [ActiveJob::Base] job
12
+ def serialize_job(job)
13
+ json = super(job)
14
+
15
+ _cattr_klasses.each do |key, cattr_klass_str|
16
+ next if json.key?(key)
17
+
18
+ attrs = cattr_klass_str.constantize.attributes
19
+
20
+ json[key] = attrs unless attrs.empty?
21
+ end
22
+
23
+ json
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/current_attributes'
4
+ require_relative 'current_attributes/loading'
5
+ require_relative 'current_attributes/persistence'
6
+
7
+ # This code is based on Sidekiqs approach to persisting current attributes
8
+ # @see https://github.com/sidekiq/sidekiq/blob/main/lib/sidekiq/middleware/current_attributes.rb
9
+ module Karafka
10
+ module ActiveJob
11
+ # Module that allows to persist current attributes on Karafka jobs
12
+ module CurrentAttributes
13
+ # Allows for persistence of given current attributes via AJ + Karafka
14
+ #
15
+ # @param klasses [Array<String, Class>] classes or names of the current attributes classes
16
+ def persist(*klasses)
17
+ # Support for providing multiple classes
18
+ klasses = Array(klasses).flatten
19
+
20
+ [Dispatcher, Consumer]
21
+ .reject { |expandable| expandable.respond_to?(:_cattr_klasses) }
22
+ .each { |expandable| expandable.class_attribute :_cattr_klasses, default: {} }
23
+
24
+ # Do not double inject in case of running persist multiple times
25
+ Dispatcher.prepend(Persistence) unless Dispatcher.ancestors.include?(Persistence)
26
+ Consumer.prepend(Loading) unless Consumer.ancestors.include?(Loading)
27
+
28
+ klasses.map(&:to_s).each do |stringified_klass|
29
+ # Prevent registering same klass multiple times
30
+ next if Dispatcher._cattr_klasses.value?(stringified_klass)
31
+
32
+ key = "cattr_#{Dispatcher._cattr_klasses.count}"
33
+
34
+ Dispatcher._cattr_klasses[key] = stringified_klass
35
+ Consumer._cattr_klasses[key] = stringified_klass
36
+ end
37
+ end
38
+
39
+ module_function :persist
40
+ end
41
+ end
42
+ end
@@ -7,20 +7,45 @@ module Karafka
7
7
  # Defaults for dispatching
8
8
  # The can be updated by using `#karafka_options` on the job
9
9
  DEFAULTS = {
10
- dispatch_method: :produce_async
10
+ dispatch_method: :produce_async,
11
+ dispatch_many_method: :produce_many_async
11
12
  }.freeze
12
13
 
13
14
  private_constant :DEFAULTS
14
15
 
15
16
  # @param job [ActiveJob::Base] job
16
- def call(job)
17
+ def dispatch(job)
17
18
  ::Karafka.producer.public_send(
18
19
  fetch_option(job, :dispatch_method, DEFAULTS),
19
20
  topic: job.queue_name,
20
- payload: ::ActiveSupport::JSON.encode(job.serialize)
21
+ payload: ::ActiveSupport::JSON.encode(serialize_job(job))
21
22
  )
22
23
  end
23
24
 
25
+ # Bulk dispatches multiple jobs using the Rails 7.1+ API
26
+ # @param jobs [Array<ActiveJob::Base>] jobs we want to dispatch
27
+ def dispatch_many(jobs)
28
+ # Group jobs by their desired dispatch method
29
+ # It can be configured per job class, so we need to make sure we divide them
30
+ dispatches = Hash.new { |hash, key| hash[key] = [] }
31
+
32
+ jobs.each do |job|
33
+ d_method = fetch_option(job, :dispatch_many_method, DEFAULTS)
34
+
35
+ dispatches[d_method] << {
36
+ topic: job.queue_name,
37
+ payload: ::ActiveSupport::JSON.encode(serialize_job(job))
38
+ }
39
+ end
40
+
41
+ dispatches.each do |type, messages|
42
+ ::Karafka.producer.public_send(
43
+ type,
44
+ messages
45
+ )
46
+ end
47
+ end
48
+
24
49
  private
25
50
 
26
51
  # @param job [ActiveJob::Base] job
@@ -33,6 +58,12 @@ module Karafka
33
58
  .karafka_options
34
59
  .fetch(key, defaults.fetch(key))
35
60
  end
61
+
62
+ # @param job [ActiveJob::Base] job
63
+ # @return [Hash] json representation of the job
64
+ def serialize_job(job)
65
+ job.serialize
66
+ end
36
67
  end
37
68
  end
38
69
  end
@@ -10,12 +10,23 @@ module Karafka
10
10
  configure do |config|
11
11
  config.error_messages = YAML.safe_load(
12
12
  File.read(
13
- File.join(Karafka.gem_root, 'config', 'errors.yml')
13
+ File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
14
14
  )
15
15
  ).fetch('en').fetch('validations').fetch('job_options')
16
16
  end
17
17
 
18
- optional(:dispatch_method) { |val| %i[produce_async produce_sync].include?(val) }
18
+ optional(:dispatch_method) do |val|
19
+ %i[
20
+ produce_async
21
+ produce_sync
22
+ ].include?(val)
23
+ end
24
+ optional(:dispatch_many_method) do |val|
25
+ %i[
26
+ produce_many_async
27
+ produce_many_sync
28
+ ].include?(val)
29
+ end
19
30
  end
20
31
  end
21
32
  end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Simple admin actions that we can perform via Karafka on our Kafka cluster
5
+ #
6
+ # @note It always initializes a new admin instance as we want to ensure it is always closed
7
+ # Since admin actions are not performed that often, that should be ok.
8
+ #
9
+ # @note It always uses the primary defined cluster and does not support multi-cluster work.
10
+ # If you need this, just replace the cluster info for the time you use this
11
+ module Admin
12
+ # A fake admin topic representation that we use for messages fetched using this API
13
+ # We cannot use the topics directly because we may want to request data from topics that we
14
+ # do not have in the routing
15
+ Topic = Struct.new(:name, :deserializer)
16
+
17
+ # We wait only for this amount of time before raising error as we intercept this error and
18
+ # retry after checking that the operation was finished or failed using external factor.
19
+ MAX_WAIT_TIMEOUT = 1
20
+
21
+ # How many times should be try. 1 x 60 => 60 seconds wait in total
22
+ MAX_ATTEMPTS = 60
23
+
24
+ # Defaults for config
25
+ CONFIG_DEFAULTS = {
26
+ 'group.id': 'karafka_admin',
27
+ # We want to know when there is no more data not to end up with an endless loop
28
+ 'enable.partition.eof': true,
29
+ 'statistics.interval.ms': 0,
30
+ # Fetch at most 5 MBs when using admin
31
+ 'fetch.message.max.bytes': 5 * 1_048_576,
32
+ # Do not commit offset automatically, this prevents offset tracking for operations involving
33
+ # a consumer instance
34
+ 'enable.auto.commit': false
35
+ }.freeze
36
+
37
+ private_constant :Topic, :CONFIG_DEFAULTS, :MAX_WAIT_TIMEOUT, :MAX_ATTEMPTS
38
+
39
+ class << self
40
+ # Allows us to read messages from the topic
41
+ #
42
+ # @param name [String, Symbol] topic name
43
+ # @param partition [Integer] partition
44
+ # @param count [Integer] how many messages we want to get at most
45
+ # @param start_offset [Integer] offset from which we should start. If -1 is provided
46
+ # (default) we will start from the latest offset
47
+ # @param settings [Hash] kafka extra settings (optional)
48
+ #
49
+ # @return [Array<Karafka::Messages::Message>] array with messages
50
+ def read_topic(name, partition, count, start_offset = -1, settings = {})
51
+ messages = []
52
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
53
+ low_offset, high_offset = nil
54
+
55
+ with_consumer(settings) do |consumer|
56
+ low_offset, high_offset = consumer.query_watermark_offsets(name, partition)
57
+
58
+ # Select offset dynamically if -1 or less
59
+ start_offset = high_offset - count if start_offset.negative?
60
+
61
+ # Build the requested range - since first element is on the start offset we need to
62
+ # subtract one from requested count to end up with expected number of elements
63
+ requested_range = (start_offset..start_offset + (count - 1))
64
+ # Establish theoretical available range. Note, that this does not handle cases related to
65
+ # log retention or compaction
66
+ available_range = (low_offset..high_offset)
67
+ # Select only offset that we can select. This will remove all the potential offsets that
68
+ # are below the low watermark offset
69
+ possible_range = requested_range.select { |offset| available_range.include?(offset) }
70
+
71
+ start_offset = possible_range.first
72
+ count = possible_range.count
73
+
74
+ tpl.add_topic_and_partitions_with_offsets(name, partition => start_offset)
75
+ consumer.assign(tpl)
76
+
77
+ # We should poll as long as we don't have all the messages that we need or as long as
78
+ # we do not read all the messages from the topic
79
+ loop do
80
+ # If we've got as many messages as we've wanted stop
81
+ break if messages.size >= count
82
+
83
+ message = consumer.poll(200)
84
+
85
+ next unless message
86
+
87
+ # If the message we've got is beyond the requested range, stop
88
+ break unless possible_range.include?(message.offset)
89
+
90
+ messages << message
91
+ rescue Rdkafka::RdkafkaError => e
92
+ # End of partition
93
+ break if e.code == :partition_eof
94
+
95
+ raise e
96
+ end
97
+ end
98
+
99
+ # Use topic from routes if we can match it or create a dummy one
100
+ # Dummy one is used in case we cannot match the topic with routes. This can happen
101
+ # when admin API is used to read topics that are not part of the routing
102
+ topic = ::Karafka::Routing::Router.find_or_initialize_by_name(name)
103
+
104
+ messages.map! do |message|
105
+ Messages::Builders::Message.call(
106
+ message,
107
+ topic,
108
+ Time.now
109
+ )
110
+ end
111
+ end
112
+
113
+ # Creates Kafka topic with given settings
114
+ #
115
+ # @param name [String] topic name
116
+ # @param partitions [Integer] number of partitions we expect
117
+ # @param replication_factor [Integer] number of replicas
118
+ # @param topic_config [Hash] topic config details as described here:
119
+ # https://kafka.apache.org/documentation/#topicconfigs
120
+ def create_topic(name, partitions, replication_factor, topic_config = {})
121
+ with_admin do |admin|
122
+ handler = admin.create_topic(name, partitions, replication_factor, topic_config)
123
+
124
+ with_re_wait(
125
+ -> { handler.wait(max_wait_timeout: MAX_WAIT_TIMEOUT) },
126
+ -> { topics_names.include?(name) }
127
+ )
128
+ end
129
+ end
130
+
131
+ # Deleted a given topic
132
+ #
133
+ # @param name [String] topic name
134
+ def delete_topic(name)
135
+ with_admin do |admin|
136
+ handler = admin.delete_topic(name)
137
+
138
+ with_re_wait(
139
+ -> { handler.wait(max_wait_timeout: MAX_WAIT_TIMEOUT) },
140
+ -> { !topics_names.include?(name) }
141
+ )
142
+ end
143
+ end
144
+
145
+ # Creates more partitions for a given topic
146
+ #
147
+ # @param name [String] topic name
148
+ # @param partitions [Integer] total number of partitions we expect to end up with
149
+ def create_partitions(name, partitions)
150
+ with_admin do |admin|
151
+ handler = admin.create_partitions(name, partitions)
152
+
153
+ with_re_wait(
154
+ -> { handler.wait(max_wait_timeout: MAX_WAIT_TIMEOUT) },
155
+ -> { topic(name).fetch(:partition_count) >= partitions }
156
+ )
157
+ end
158
+ end
159
+
160
+ # Fetches the watermark offsets for a given topic partition
161
+ #
162
+ # @param name [String, Symbol] topic name
163
+ # @param partition [Integer] partition
164
+ # @return [Array<Integer, Integer>] low watermark offset and high watermark offset
165
+ def read_watermark_offsets(name, partition)
166
+ with_consumer do |consumer|
167
+ consumer.query_watermark_offsets(name, partition)
168
+ end
169
+ end
170
+
171
+ # @return [Rdkafka::Metadata] cluster metadata info
172
+ def cluster_info
173
+ with_admin do |admin|
174
+ Rdkafka::Metadata.new(admin.instance_variable_get('@native_kafka'))
175
+ end
176
+ end
177
+
178
+ # Creates consumer instance and yields it. After usage it closes the consumer instance
179
+ # This API can be used in other pieces of code and allows for low-level consumer usage
180
+ #
181
+ # @param settings [Hash] extra settings to customize consumer
182
+ def with_consumer(settings = {})
183
+ consumer = config(:consumer, settings).consumer
184
+ yield(consumer)
185
+ ensure
186
+ consumer&.close
187
+ end
188
+
189
+ private
190
+
191
+ # @return [Array<String>] topics names
192
+ def topics_names
193
+ cluster_info.topics.map { |topic| topic.fetch(:topic_name) }
194
+ end
195
+
196
+ # Finds details about given topic
197
+ # @param name [String] topic name
198
+ # @return [Hash] topic details
199
+ def topic(name)
200
+ cluster_info.topics.find { |topic| topic[:topic_name] == name }
201
+ end
202
+
203
+ # Creates admin instance and yields it. After usage it closes the admin instance
204
+ def with_admin
205
+ admin = config(:producer, {}).admin
206
+ yield(admin)
207
+ ensure
208
+ admin&.close
209
+ end
210
+
211
+ # There are some cases where rdkafka admin operations finish successfully but without the
212
+ # callback being triggered to materialize the post-promise object. Until this is fixed we
213
+ # can figure out, that operation we wanted to do finished successfully by checking that the
214
+ # effect of the command (new topic, more partitions, etc) is handled. Exactly for that we
215
+ # use the breaker. It we get a timeout, we can check that what we wanted to achieve has
216
+ # happened via the breaker check, hence we do not need to wait any longer.
217
+ #
218
+ # @param handler [Proc] the wait handler operation
219
+ # @param breaker [Proc] extra condition upon timeout that indicates things were finished ok
220
+ def with_re_wait(handler, breaker)
221
+ attempt ||= 0
222
+ attempt += 1
223
+
224
+ handler.call
225
+ rescue Rdkafka::AbstractHandle::WaitTimeoutError
226
+ return if breaker.call
227
+
228
+ retry if attempt <= MAX_ATTEMPTS
229
+
230
+ raise
231
+ end
232
+
233
+ # @param type [Symbol] type of config we want
234
+ # @param settings [Hash] extra settings for config (if needed)
235
+ # @return [::Rdkafka::Config] rdkafka config
236
+ def config(type, settings)
237
+ config_hash = Karafka::Setup::AttributesMap.public_send(
238
+ type,
239
+ Karafka::App.config.kafka.dup.merge(CONFIG_DEFAULTS).merge!(settings)
240
+ )
241
+
242
+ ::Rdkafka::Config.new(config_hash)
243
+ end
244
+ end
245
+ end
246
+ end