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
data/lib/karafka/app.rb CHANGED
@@ -14,20 +14,43 @@ module Karafka
14
14
  .builder
15
15
  end
16
16
 
17
- # @return [Array<Karafka::Routing::SubscriptionGroup>] active subscription groups
17
+ # @return [Hash] active subscription groups grouped based on consumer group in a hash
18
18
  def subscription_groups
19
+ # We first build all the subscription groups, so they all get the same position, despite
20
+ # later narrowing that. It allows us to maintain same position number for static members
21
+ # even when we want to run subset of consumer groups or subscription groups
22
+ #
23
+ # We then narrow this to active consumer groups from which we select active subscription
24
+ # groups.
19
25
  consumer_groups
20
- .active
21
- .flat_map(&:subscription_groups)
26
+ .map { |cg| [cg, cg.subscription_groups] }
27
+ .select { |cg, _| cg.active? }
28
+ .select { |_, sgs| sgs.delete_if { |sg| !sg.active? } }
29
+ .delete_if { |_, sgs| sgs.empty? }
30
+ .each { |_, sgs| sgs.each { |sg| sg.topics.delete_if { |top| !top.active? } } }
31
+ .each { |_, sgs| sgs.delete_if { |sg| sg.topics.empty? } }
32
+ .reject { |cg, _| cg.subscription_groups.empty? }
33
+ .to_h
22
34
  end
23
35
 
24
36
  # Just a nicer name for the consumer groups
25
37
  alias routes consumer_groups
26
38
 
27
- Status.instance_methods(false).each do |delegated|
28
- define_method(delegated) do
29
- App.config.internal.status.send(delegated)
30
- end
39
+ # Allow for easier status management via `Karafka::App` by aliasing status methods here
40
+ Status::STATES.each do |state, transition|
41
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
42
+ def #{state}
43
+ App.config.internal.status.#{state}
44
+ end
45
+
46
+ def #{state}?
47
+ App.config.internal.status.#{state}?
48
+ end
49
+
50
+ def #{transition}
51
+ App.config.internal.status.#{transition}
52
+ end
53
+ RUBY
31
54
  end
32
55
 
33
56
  # Methods that should be delegated to Karafka module
@@ -4,9 +4,16 @@
4
4
  module Karafka
5
5
  # Base consumer from which all Karafka consumers should inherit
6
6
  class BaseConsumer
7
+ # Allow for consumer instance tagging for instrumentation
8
+ include ::Karafka::Core::Taggable
9
+
10
+ extend Forwardable
11
+
12
+ def_delegators :@coordinator, :topic, :partition
13
+
14
+ # @return [String] id of the current consumer
15
+ attr_reader :id
7
16
  # @return [Karafka::Routing::Topic] topic to which a given consumer is subscribed
8
- attr_accessor :topic
9
- # @return [Karafka::Messages::Messages] current messages batch
10
17
  attr_accessor :messages
11
18
  # @return [Karafka::Connection::Client] kafka connection client
12
19
  attr_accessor :client
@@ -15,13 +22,49 @@ module Karafka
15
22
  # @return [Waterdrop::Producer] producer instance
16
23
  attr_accessor :producer
17
24
 
18
- # Can be used to run preparation code
25
+ # Creates new consumer and assigns it an id
26
+ def initialize
27
+ @id = SecureRandom.hex(6)
28
+ end
29
+
30
+ # Can be used to run preparation code prior to the job being enqueued
19
31
  #
20
32
  # @private
21
- # @note This should not be used by the end users as it is part of the lifecycle of things but
33
+ # @note This should not be used by the end users as it is part of the lifecycle of things and
34
+ # not as a part of the public api. This should not perform any extensive operations as it is
35
+ # blocking and running in the listener thread.
36
+ def on_before_enqueue
37
+ handle_before_enqueue
38
+ rescue StandardError => e
39
+ Karafka.monitor.instrument(
40
+ 'error.occurred',
41
+ error: e,
42
+ caller: self,
43
+ type: 'consumer.before_enqueue.error'
44
+ )
45
+ end
46
+
47
+ # Can be used to run preparation code in the worker
48
+ #
49
+ # @private
50
+ # @note This should not be used by the end users as it is part of the lifecycle of things and
22
51
  # not as part of the public api. This can act as a hook when creating non-blocking
23
52
  # consumers and doing other advanced stuff
24
- def on_before_consume; end
53
+ def on_before_consume
54
+ messages.metadata.processed_at = Time.now
55
+ messages.metadata.freeze
56
+
57
+ # We run this after the full metadata setup, so we can use all the messages information
58
+ # if needed
59
+ handle_before_consume
60
+ rescue StandardError => e
61
+ Karafka.monitor.instrument(
62
+ 'error.occurred',
63
+ error: e,
64
+ caller: self,
65
+ type: 'consumer.before_consume.error'
66
+ )
67
+ end
25
68
 
26
69
  # Executes the default consumer flow.
27
70
  #
@@ -31,54 +74,50 @@ module Karafka
31
74
  # that may not yet kick in when error occurs. That way we pause always on the last processed
32
75
  # message.
33
76
  def on_consume
34
- Karafka.monitor.instrument('consumer.consumed', caller: self) do
35
- consume
36
- end
37
-
38
- coordinator.consumption(self).success!
77
+ handle_consume
39
78
  rescue StandardError => e
40
- coordinator.consumption(self).failure!
41
-
42
79
  Karafka.monitor.instrument(
43
80
  'error.occurred',
44
81
  error: e,
45
82
  caller: self,
83
+ seek_offset: coordinator.seek_offset,
46
84
  type: 'consumer.consume.error'
47
85
  )
48
- ensure
49
- # We need to decrease number of jobs that this coordinator coordinates as it has finished
50
- coordinator.decrement
51
86
  end
52
87
 
53
88
  # @private
54
89
  # @note This should not be used by the end users as it is part of the lifecycle of things but
55
90
  # not as part of the public api.
56
91
  def on_after_consume
57
- return if revoked?
58
-
59
- if coordinator.success?
60
- coordinator.pause_tracker.reset
61
-
62
- # Mark as consumed only if manual offset management is not on
63
- return if topic.manual_offset_management?
92
+ handle_after_consume
93
+ rescue StandardError => e
94
+ Karafka.monitor.instrument(
95
+ 'error.occurred',
96
+ error: e,
97
+ caller: self,
98
+ type: 'consumer.after_consume.error'
99
+ )
100
+ end
64
101
 
65
- # We use the non-blocking one here. If someone needs the blocking one, can implement it
66
- # with manual offset management
67
- mark_as_consumed(messages.last)
68
- else
69
- pause(@seek_offset || messages.first.offset)
70
- end
102
+ # Trigger method for running on idle runs without messages
103
+ #
104
+ # @private
105
+ def on_idle
106
+ handle_idle
107
+ rescue StandardError => e
108
+ Karafka.monitor.instrument(
109
+ 'error.occurred',
110
+ error: e,
111
+ caller: self,
112
+ type: 'consumer.idle.error'
113
+ )
71
114
  end
72
115
 
73
- # Trigger method for running on shutdown.
116
+ # Trigger method for running on partition revocation.
74
117
  #
75
118
  # @private
76
119
  def on_revoked
77
- coordinator.revoke
78
-
79
- Karafka.monitor.instrument('consumer.revoked', caller: self) do
80
- revoked
81
- end
120
+ handle_revoked
82
121
  rescue StandardError => e
83
122
  Karafka.monitor.instrument(
84
123
  'error.occurred',
@@ -92,9 +131,7 @@ module Karafka
92
131
  #
93
132
  # @private
94
133
  def on_shutdown
95
- Karafka.monitor.instrument('consumer.shutdown', caller: self) do
96
- shutdown
97
- end
134
+ handle_shutdown
98
135
  rescue StandardError => e
99
136
  Karafka.monitor.instrument(
100
137
  'error.occurred',
@@ -108,7 +145,7 @@ module Karafka
108
145
 
109
146
  # Method that will perform business logic and on data received from Kafka (it will consume
110
147
  # the data)
111
- # @note This method needs bo be implemented in a subclass. We stub it here as a failover if
148
+ # @note This method needs to be implemented in a subclass. We stub it here as a failover if
112
149
  # someone forgets about it or makes on with typo
113
150
  def consume
114
151
  raise NotImplementedError, 'Implement this in a subclass'
@@ -122,63 +159,43 @@ module Karafka
122
159
  # some teardown procedures (closing file handler, etc).
123
160
  def shutdown; end
124
161
 
125
- # Marks message as consumed in an async way.
126
- #
127
- # @param message [Messages::Message] last successfully processed message.
128
- # @return [Boolean] true if we were able to mark the offset, false otherwise. False indicates
129
- # that we were not able and that we have lost the partition.
130
- #
131
- # @note We keep track of this offset in case we would mark as consumed and got error when
132
- # processing another message. In case like this we do not pause on the message we've already
133
- # processed but rather at the next one. This applies to both sync and async versions of this
134
- # method.
135
- def mark_as_consumed(message)
136
- unless client.mark_as_consumed(message)
137
- coordinator.revoke
138
-
139
- return false
140
- end
141
-
142
- @seek_offset = message.offset + 1
143
-
144
- true
145
- end
146
-
147
- # Marks message as consumed in a sync way.
148
- #
149
- # @param message [Messages::Message] last successfully processed message.
150
- # @return [Boolean] true if we were able to mark the offset, false otherwise. False indicates
151
- # that we were not able and that we have lost the partition.
152
- def mark_as_consumed!(message)
153
- unless client.mark_as_consumed!(message)
154
- coordinator.revoke
155
-
156
- return false
157
- end
158
-
159
- @seek_offset = message.offset + 1
160
-
161
- true
162
- end
163
-
164
162
  # Pauses processing on a given offset for the current topic partition
165
163
  #
166
164
  # After given partition is resumed, it will continue processing from the given offset
167
165
  # @param offset [Integer] offset from which we want to restart the processing
168
166
  # @param timeout [Integer, nil] how long in milliseconds do we want to pause or nil to use the
169
167
  # default exponential pausing strategy defined for retries
170
- def pause(offset, timeout = nil)
168
+ # @param manual_pause [Boolean] Flag to differentiate between user pause and system/strategy
169
+ # based pause. While they both pause in exactly the same way, the strategy application
170
+ # may need to differentiate between them.
171
+ def pause(offset, timeout = nil, manual_pause = true)
171
172
  timeout ? coordinator.pause_tracker.pause(timeout) : coordinator.pause_tracker.pause
172
173
 
173
174
  client.pause(
174
- messages.metadata.topic,
175
- messages.metadata.partition,
175
+ topic.name,
176
+ partition,
176
177
  offset
177
178
  )
179
+
180
+ # Indicate, that user took a manual action of pausing
181
+ coordinator.manual_pause if manual_pause
182
+
183
+ Karafka.monitor.instrument(
184
+ 'consumer.consuming.pause',
185
+ caller: self,
186
+ manual: manual_pause,
187
+ topic: topic.name,
188
+ partition: partition,
189
+ offset: offset,
190
+ timeout: coordinator.pause_tracker.current_timeout,
191
+ attempt: coordinator.pause_tracker.attempt
192
+ )
178
193
  end
179
194
 
180
195
  # Resumes processing of the current topic partition
181
196
  def resume
197
+ return unless coordinator.pause_tracker.paused?
198
+
182
199
  # This is sufficient to expire a partition pause, as with it will be resumed by the listener
183
200
  # thread before the next poll.
184
201
  coordinator.pause_tracker.expire
@@ -190,8 +207,8 @@ module Karafka
190
207
  def seek(offset)
191
208
  client.seek(
192
209
  Karafka::Messages::Seek.new(
193
- messages.metadata.topic,
194
- messages.metadata.partition,
210
+ topic.name,
211
+ partition,
195
212
  offset
196
213
  )
197
214
  )
@@ -203,5 +220,30 @@ module Karafka
203
220
  def revoked?
204
221
  coordinator.revoked?
205
222
  end
223
+
224
+ # @return [Boolean] are we retrying processing after an error. This can be used to provide a
225
+ # different flow after there is an error, for example for resources cleanup, small manual
226
+ # backoff or different instrumentation tracking.
227
+ def retrying?
228
+ coordinator.pause_tracker.attempt.positive?
229
+ end
230
+
231
+ # Pauses the processing from the last offset to retry on given message
232
+ # @private
233
+ def retry_after_pause
234
+ pause(coordinator.seek_offset, nil, false)
235
+
236
+ # Instrumentation needs to run **after** `#pause` invocation because we rely on the states
237
+ # set by `#pause`
238
+ Karafka.monitor.instrument(
239
+ 'consumer.consuming.retry',
240
+ caller: self,
241
+ topic: topic.name,
242
+ partition: partition,
243
+ offset: coordinator.seek_offset,
244
+ timeout: coordinator.pause_tracker.current_timeout,
245
+ attempt: coordinator.pause_tracker.attempt
246
+ )
247
+ end
206
248
  end
207
249
  end
@@ -34,6 +34,26 @@ module Karafka
34
34
  end
35
35
 
36
36
  class << self
37
+ # Loads proper environment with what is needed to run the CLI
38
+ def load
39
+ # If there is a boot file, we need to require it as we expect it to contain
40
+ # Karafka app setup, routes, etc
41
+ if File.exist?(::Karafka.boot_file)
42
+ rails_env_rb = File.join(Dir.pwd, 'config/environment.rb')
43
+
44
+ # Load Rails environment file that starts Rails, so we can reference consumers and
45
+ # other things from `karafka.rb` file. This will work only for Rails, for non-rails
46
+ # a manual setup is needed
47
+ require rails_env_rb if Kernel.const_defined?(:Rails) && File.exist?(rails_env_rb)
48
+
49
+ require Karafka.boot_file.to_s
50
+ # However when it is unavailable, we still want to be able to run help command
51
+ # and install command as they don't require configured app itself to run
52
+ elsif %w[-h install].none? { |cmd| cmd == ARGV[0] }
53
+ raise ::Karafka::Errors::MissingBootFileError, ::Karafka.boot_file
54
+ end
55
+ end
56
+
37
57
  # Allows to set options for Thor cli
38
58
  # @see https://github.com/erikhuda/thor
39
59
  # @param option Single option details
@@ -9,22 +9,27 @@ module Karafka
9
9
  option aliases: 'c'
10
10
 
11
11
  class << self
12
- # @return [String] Console executing command
12
+ # @return [String] Console executing command for non-Rails setup
13
13
  # @example
14
14
  # Karafka::Cli::Console.command #=> 'KARAFKA_CONSOLE=true bundle exec irb...'
15
- def command
16
- envs = [
17
- "IRBRC='#{Karafka.gem_root}/.console_irbrc'",
18
- 'KARAFKA_CONSOLE=true'
19
- ]
20
- "#{envs.join(' ')} bundle exec irb -r #{Karafka.boot_file}"
15
+ def console
16
+ "IRBRC='#{Karafka.gem_root}/.console_irbrc' bundle exec irb -r #{Karafka.boot_file}"
17
+ end
18
+
19
+ # @return [String] Console executing command for Rails setup
20
+ # @note In case of Rails, it has its own console, hence we can just defer to it
21
+ def rails_console
22
+ 'bundle exec rails console'
21
23
  end
22
24
  end
23
25
 
24
26
  # Start the Karafka console
25
27
  def call
26
28
  cli.info
27
- exec self.class.command
29
+
30
+ command = ::Karafka.rails? ? self.class.rails_console : self.class.console
31
+
32
+ exec "KARAFKA_CONSOLE=true #{command}"
28
33
  end
29
34
  end
30
35
  end
@@ -35,9 +35,10 @@ module Karafka
35
35
 
36
36
  [
37
37
  "Karafka version: #{Karafka::VERSION}#{postfix}",
38
- "Ruby version: #{RUBY_VERSION}",
38
+ "Ruby version: #{RUBY_DESCRIPTION}",
39
39
  "Rdkafka version: #{::Rdkafka::VERSION}",
40
- "Subscription groups count: #{Karafka::App.subscription_groups.size}",
40
+ "Consumer groups count: #{Karafka::App.consumer_groups.size}",
41
+ "Subscription groups count: #{Karafka::App.subscription_groups.values.flatten.size}",
41
42
  "Workers count: #{Karafka::App.config.concurrency}",
42
43
  "Application client id: #{config.client_id}",
43
44
  "Boot file: #{Karafka.boot_file}",
@@ -7,37 +7,84 @@ module Karafka
7
7
  class Server < Base
8
8
  include Helpers::Colorize
9
9
 
10
+ # Types of things we can include / exclude from the routing via the CLI options
11
+ SUPPORTED_TYPES = ::Karafka::Routing::ActivityManager::SUPPORTED_TYPES
12
+
13
+ private_constant :SUPPORTED_TYPES
14
+
10
15
  desc 'Start the Karafka server (short-cut alias: "s")'
16
+
11
17
  option aliases: 's'
12
- option :consumer_groups, type: :array, default: nil, aliases: :g
18
+
19
+ # Thor does not work well with many aliases combinations, hence we remap the aliases
20
+ # by ourselves in the code
21
+ option :consumer_groups, type: :array, default: [], aliases: :g
22
+ option :subscription_groups, type: :array, default: []
23
+ option :topics, type: :array, default: []
24
+
25
+ %i[
26
+ include
27
+ exclude
28
+ ].each do |action|
29
+ SUPPORTED_TYPES.each do |type|
30
+ option(
31
+ "#{action}_#{type}",
32
+ type: :array,
33
+ default: []
34
+ )
35
+ end
36
+ end
13
37
 
14
38
  # Start the Karafka server
15
39
  def call
16
40
  # Print our banner and info in the dev mode
17
41
  print_marketing_info if Karafka::App.env.development?
18
42
 
19
- Contracts::ServerCliOptions.new.validate!(cli.options)
20
-
21
- # We assign active topics on a server level, as only server is expected to listen on
22
- # part of the topics
23
- Karafka::Server.consumer_groups = cli.options[:consumer_groups]
43
+ register_inclusions(cli)
44
+ register_exclusions(cli)
24
45
 
25
46
  Karafka::Server.run
26
47
  end
27
48
 
28
49
  private
29
50
 
51
+ # Registers things we want to include (if defined)
52
+ # @param cli [Karafka::Cli] Thor cli handler
53
+ def register_inclusions(cli)
54
+ activities = ::Karafka::App.config.internal.routing.activity_manager
55
+
56
+ SUPPORTED_TYPES.each do |type|
57
+ v1 = cli.options[type] || []
58
+ v2 = cli.options[:"include_#{type}"] || []
59
+ names = v1 + v2
60
+
61
+ names.each { |name| activities.include(type, name) }
62
+ end
63
+ end
64
+
65
+ # Registers things we want to exclude (if defined)
66
+ # @param cli [Karafka::Cli] Thor cli handler
67
+ def register_exclusions(cli)
68
+ activities = ::Karafka::App.config.internal.routing.activity_manager
69
+
70
+ activities.class::SUPPORTED_TYPES.each do |type|
71
+ names = cli.options[:"exclude_#{type}"] || []
72
+
73
+ names.each { |name| activities.exclude(type, name) }
74
+ end
75
+ end
76
+
30
77
  # Prints marketing info
31
78
  def print_marketing_info
32
79
  Karafka.logger.info Info::BANNER
33
80
 
34
81
  if Karafka.pro?
35
82
  Karafka.logger.info(
36
- green('Thank you for investing in the Karafka Pro subscription!')
83
+ green('Thank you for using Karafka Pro!')
37
84
  )
38
85
  else
39
86
  Karafka.logger.info(
40
- red('You like Karafka? Please consider getting a Pro version!')
87
+ red('Upgrade to Karafka Pro for more features and support: https://karafka.io')
41
88
  )
42
89
  end
43
90
  end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ class Cli < Thor
5
+ # CLI actions related to Kafka cluster topics management
6
+ class Topics < Base
7
+ include Helpers::Colorize
8
+
9
+ desc 'Allows for the topics management (create, delete, reset, repartition)'
10
+ # @param action [String] action we want to take
11
+ def call(action = 'missing')
12
+ case action
13
+ when 'create'
14
+ create
15
+ when 'delete'
16
+ delete
17
+ when 'reset'
18
+ reset
19
+ when 'repartition'
20
+ repartition
21
+ when 'migrate'
22
+ migrate
23
+ else
24
+ raise ::ArgumentError, "Invalid topics action: #{action}"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Creates topics based on the routing setup and configuration
31
+ def create
32
+ declaratives_routing_topics.each do |topic|
33
+ name = topic.name
34
+
35
+ if existing_topics_names.include?(name)
36
+ puts "#{yellow('Skipping')} because topic #{name} already exists."
37
+ else
38
+ puts "Creating topic #{name}..."
39
+ Admin.create_topic(
40
+ name,
41
+ topic.declaratives.partitions,
42
+ topic.declaratives.replication_factor,
43
+ topic.declaratives.details
44
+ )
45
+ puts "#{green('Created')} topic #{name}."
46
+ end
47
+ end
48
+ end
49
+
50
+ # Deletes routing based topics
51
+ def delete
52
+ declaratives_routing_topics.each do |topic|
53
+ name = topic.name
54
+
55
+ if existing_topics_names.include?(name)
56
+ puts "Deleting topic #{name}..."
57
+ Admin.delete_topic(name)
58
+ puts "#{green('Deleted')} topic #{name}."
59
+ else
60
+ puts "#{yellow('Skipping')} because topic #{name} does not exist."
61
+ end
62
+ end
63
+ end
64
+
65
+ # Deletes routing based topics and re-creates them
66
+ def reset
67
+ delete
68
+
69
+ # We need to invalidate the metadata cache, otherwise we will think, that the topic
70
+ # already exists
71
+ @existing_topics = nil
72
+
73
+ create
74
+ end
75
+
76
+ # Creates missing topics and aligns the partitions count
77
+ def migrate
78
+ create
79
+
80
+ @existing_topics = nil
81
+
82
+ repartition
83
+ end
84
+
85
+ # Increases number of partitions on topics that have less partitions than defined
86
+ # Will **not** create topics if missing.
87
+ def repartition
88
+ existing_partitions = existing_topics.map do |topic|
89
+ [topic.fetch(:topic_name), topic.fetch(:partition_count)]
90
+ end.to_h
91
+
92
+ declaratives_routing_topics.each do |topic|
93
+ name = topic.name
94
+
95
+ desired_count = topic.config.partitions
96
+ existing_count = existing_partitions.fetch(name, false)
97
+
98
+ if existing_count && existing_count < desired_count
99
+ puts "Increasing number of partitions to #{desired_count} on topic #{name}..."
100
+ Admin.create_partitions(name, desired_count)
101
+ change = desired_count - existing_count
102
+ puts "#{green('Created')} #{change} additional partitions on topic #{name}."
103
+ elsif existing_count
104
+ puts "#{yellow('Skipping')} because topic #{name} has #{existing_count} partitions."
105
+ else
106
+ puts "#{yellow('Skipping')} because topic #{name} does not exist."
107
+ end
108
+ end
109
+ end
110
+
111
+ # @return [Array<Karafka::Routing::Topic>] all available topics that can be managed
112
+ # @note If topic is defined in multiple consumer groups, first config will be used. This
113
+ # means, that this CLI will not work for simultaneous management of multiple clusters from
114
+ # a single CLI command execution flow.
115
+ def declaratives_routing_topics
116
+ return @declaratives_routing_topics if @declaratives_routing_topics
117
+
118
+ collected_topics = {}
119
+ default_servers = Karafka::App.config.kafka[:'bootstrap.servers']
120
+
121
+ App.consumer_groups.each do |consumer_group|
122
+ consumer_group.topics.each do |topic|
123
+ # Skip topics that were explicitly disabled from management
124
+ next unless topic.declaratives.active?
125
+ # If bootstrap servers are different, consider this a different cluster
126
+ next unless default_servers == topic.kafka[:'bootstrap.servers']
127
+
128
+ collected_topics[topic.name] ||= topic
129
+ end
130
+ end
131
+
132
+ @declaratives_routing_topics = collected_topics.values
133
+ end
134
+
135
+ # @return [Array<Hash>] existing topics details
136
+ def existing_topics
137
+ @existing_topics ||= Admin.cluster_info.topics
138
+ end
139
+
140
+ # @return [Array<String>] names of already existing topics
141
+ def existing_topics_names
142
+ existing_topics.map { |topic| topic.fetch(:topic_name) }
143
+ end
144
+ end
145
+ end
146
+ end