karafka 2.4.18 → 2.5.0.beta2

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +3 -0
  3. data/.github/workflows/ci.yml +58 -14
  4. data/.github/workflows/push.yml +36 -0
  5. data/.github/workflows/verify-action-pins.yml +16 -0
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +60 -0
  8. data/Gemfile +2 -2
  9. data/Gemfile.lock +69 -50
  10. data/LICENSE-COMM +2 -2
  11. data/README.md +1 -1
  12. data/Rakefile +4 -0
  13. data/bin/clean_kafka +43 -0
  14. data/bin/integrations +19 -6
  15. data/bin/rspecs +15 -3
  16. data/bin/verify_kafka_warnings +35 -0
  17. data/bin/verify_topics_naming +27 -0
  18. data/config/locales/errors.yml +3 -0
  19. data/config/locales/pro_errors.yml +13 -2
  20. data/docker-compose.yml +1 -1
  21. data/examples/payloads/json/enrollment_event.json +579 -0
  22. data/examples/payloads/json/ingestion_event.json +30 -0
  23. data/examples/payloads/json/transaction_event.json +17 -0
  24. data/examples/payloads/json/user_event.json +11 -0
  25. data/karafka.gemspec +3 -8
  26. data/lib/karafka/active_job/current_attributes.rb +1 -1
  27. data/lib/karafka/admin/acl.rb +5 -1
  28. data/lib/karafka/admin/configs.rb +5 -1
  29. data/lib/karafka/admin.rb +69 -34
  30. data/lib/karafka/base_consumer.rb +17 -8
  31. data/lib/karafka/cli/base.rb +8 -2
  32. data/lib/karafka/cli/topics/align.rb +7 -4
  33. data/lib/karafka/cli/topics/base.rb +17 -0
  34. data/lib/karafka/cli/topics/create.rb +9 -7
  35. data/lib/karafka/cli/topics/delete.rb +4 -2
  36. data/lib/karafka/cli/topics/help.rb +39 -0
  37. data/lib/karafka/cli/topics/repartition.rb +4 -2
  38. data/lib/karafka/cli/topics.rb +10 -3
  39. data/lib/karafka/cli.rb +2 -0
  40. data/lib/karafka/connection/client.rb +30 -9
  41. data/lib/karafka/connection/listener.rb +24 -12
  42. data/lib/karafka/connection/messages_buffer.rb +1 -1
  43. data/lib/karafka/connection/proxy.rb +3 -0
  44. data/lib/karafka/constraints.rb +3 -3
  45. data/lib/karafka/contracts/config.rb +3 -0
  46. data/lib/karafka/contracts/topic.rb +1 -1
  47. data/lib/karafka/errors.rb +46 -2
  48. data/lib/karafka/helpers/async.rb +3 -1
  49. data/lib/karafka/instrumentation/callbacks/rebalance.rb +5 -1
  50. data/lib/karafka/instrumentation/logger_listener.rb +86 -23
  51. data/lib/karafka/instrumentation/proctitle_listener.rb +5 -1
  52. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +2 -2
  53. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  54. data/lib/karafka/pro/cleaner.rb +8 -0
  55. data/lib/karafka/pro/cli/parallel_segments/base.rb +89 -0
  56. data/lib/karafka/pro/cli/parallel_segments/collapse.rb +164 -0
  57. data/lib/karafka/pro/cli/parallel_segments/distribute.rb +164 -0
  58. data/lib/karafka/pro/cli/parallel_segments.rb +60 -0
  59. data/lib/karafka/pro/connection/manager.rb +5 -8
  60. data/lib/karafka/pro/encryption.rb +8 -0
  61. data/lib/karafka/pro/instrumentation/performance_tracker.rb +1 -1
  62. data/lib/karafka/pro/iterator/expander.rb +5 -3
  63. data/lib/karafka/pro/iterator/tpl_builder.rb +23 -0
  64. data/lib/karafka/pro/loader.rb +10 -0
  65. data/lib/karafka/pro/processing/coordinator.rb +4 -1
  66. data/lib/karafka/pro/processing/coordinators/errors_tracker.rb +27 -3
  67. data/lib/karafka/pro/processing/coordinators/filters_applier.rb +11 -0
  68. data/lib/karafka/pro/processing/filters/base.rb +10 -2
  69. data/lib/karafka/pro/processing/filters/expirer.rb +5 -0
  70. data/lib/karafka/pro/processing/filters/inline_insights_delayer.rb +2 -2
  71. data/lib/karafka/pro/processing/filters/virtual_limiter.rb +5 -0
  72. data/lib/karafka/pro/processing/parallel_segments/filters/base.rb +73 -0
  73. data/lib/karafka/pro/processing/parallel_segments/filters/default.rb +85 -0
  74. data/lib/karafka/pro/processing/parallel_segments/filters/mom.rb +66 -0
  75. data/lib/karafka/pro/processing/partitioner.rb +1 -13
  76. data/lib/karafka/pro/processing/piping/consumer.rb +13 -13
  77. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  78. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  79. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  80. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  81. data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom_vp.rb +1 -1
  82. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +1 -1
  83. data/lib/karafka/pro/processing/strategies/default.rb +36 -8
  84. data/lib/karafka/pro/processing/strategies/dlq/default.rb +14 -10
  85. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +1 -1
  86. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +1 -1
  87. data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +3 -1
  88. data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +1 -1
  89. data/lib/karafka/pro/processing/strategies/ftr/default.rb +1 -1
  90. data/lib/karafka/pro/processing/strategies/lrj/default.rb +4 -1
  91. data/lib/karafka/pro/processing/strategies/lrj/ftr.rb +1 -1
  92. data/lib/karafka/pro/processing/strategies/lrj/ftr_mom.rb +1 -1
  93. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +1 -1
  94. data/lib/karafka/pro/processing/virtual_partitions/distributors/balanced.rb +50 -0
  95. data/lib/karafka/pro/processing/virtual_partitions/distributors/base.rb +29 -0
  96. data/lib/karafka/pro/processing/virtual_partitions/distributors/consistent.rb +27 -0
  97. data/lib/karafka/pro/recurring_tasks/contracts/config.rb +8 -4
  98. data/lib/karafka/pro/recurring_tasks/dispatcher.rb +3 -3
  99. data/lib/karafka/pro/recurring_tasks/setup/config.rb +7 -2
  100. data/lib/karafka/pro/recurring_tasks.rb +13 -0
  101. data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +1 -1
  102. data/lib/karafka/pro/routing/features/multiplexing/config.rb +1 -0
  103. data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +17 -0
  104. data/lib/karafka/pro/routing/features/multiplexing/proxy.rb +5 -2
  105. data/lib/karafka/pro/routing/features/multiplexing/subscription_group.rb +8 -1
  106. data/lib/karafka/pro/routing/features/parallel_segments/builder.rb +47 -0
  107. data/lib/karafka/pro/routing/features/parallel_segments/config.rb +27 -0
  108. data/lib/karafka/pro/routing/features/parallel_segments/consumer_group.rb +83 -0
  109. data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +49 -0
  110. data/lib/karafka/pro/routing/features/parallel_segments/topic.rb +43 -0
  111. data/lib/karafka/pro/routing/features/parallel_segments.rb +24 -0
  112. data/lib/karafka/pro/routing/features/patterns/pattern.rb +1 -1
  113. data/lib/karafka/pro/routing/features/recurring_tasks/builder.rb +2 -2
  114. data/lib/karafka/pro/routing/features/scheduled_messages/builder.rb +10 -6
  115. data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +20 -2
  116. data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -0
  117. data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +8 -2
  118. data/lib/karafka/pro/scheduled_messages/consumer.rb +19 -21
  119. data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +9 -6
  120. data/lib/karafka/pro/scheduled_messages/deserializers/headers.rb +7 -1
  121. data/lib/karafka/pro/scheduled_messages/max_epoch.rb +15 -6
  122. data/lib/karafka/pro/scheduled_messages.rb +13 -0
  123. data/lib/karafka/processing/coordinators_buffer.rb +1 -0
  124. data/lib/karafka/processing/strategies/default.rb +4 -4
  125. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
  126. data/lib/karafka/routing/subscription_group.rb +1 -1
  127. data/lib/karafka/runner.rb +7 -1
  128. data/lib/karafka/server.rb +19 -19
  129. data/lib/karafka/setup/attributes_map.rb +2 -0
  130. data/lib/karafka/setup/config.rb +22 -1
  131. data/lib/karafka/setup/defaults_injector.rb +26 -1
  132. data/lib/karafka/status.rb +6 -1
  133. data/lib/karafka/swarm/node.rb +31 -0
  134. data/lib/karafka/swarm/supervisor.rb +4 -0
  135. data/lib/karafka/templates/karafka.rb.erb +14 -1
  136. data/lib/karafka/version.rb +1 -1
  137. data/lib/karafka.rb +17 -9
  138. data/renovate.json +14 -2
  139. metadata +40 -40
  140. checksums.yaml.gz.sig +0 -0
  141. data/certs/cert.pem +0 -26
  142. data.tar.gz.sig +0 -0
  143. metadata.gz.sig +0 -0
@@ -11,6 +11,10 @@ module Karafka
11
11
  # This API works based on ability to create a `Karafka:Admin::Acl` object that can be then used
12
12
  # using `#create`, `#delete` and `#describe` class API.
13
13
  class Acl
14
+ extend Helpers::ConfigImporter.new(
15
+ max_wait_time: %i[admin max_wait_time]
16
+ )
17
+
14
18
  # Types of resources for which we can assign permissions.
15
19
  #
16
20
  # Resource refers to any entity within the Kafka ecosystem for which access control can be
@@ -162,7 +166,7 @@ module Karafka
162
166
  # Makes sure that admin is closed afterwards.
163
167
  def with_admin_wait
164
168
  Admin.with_admin do |admin|
165
- yield(admin).wait(max_wait_timeout: Karafka::App.config.admin.max_wait_time)
169
+ yield(admin).wait(max_wait_timeout: max_wait_time)
166
170
  end
167
171
  end
168
172
 
@@ -10,6 +10,10 @@ module Karafka
10
10
  #
11
11
  # Altering is done in the incremental way.
12
12
  module Configs
13
+ extend Helpers::ConfigImporter.new(
14
+ max_wait_time: %i[admin max_wait_time]
15
+ )
16
+
13
17
  class << self
14
18
  # Fetches given resources configurations from Kafka
15
19
  #
@@ -94,7 +98,7 @@ module Karafka
94
98
  # Makes sure that admin is closed afterwards.
95
99
  def with_admin_wait
96
100
  Admin.with_admin do |admin|
97
- yield(admin).wait(max_wait_timeout: Karafka::App.config.admin.max_wait_time)
101
+ yield(admin).wait(max_wait_timeout: max_wait_time)
98
102
  end
99
103
  end
100
104
  end
data/lib/karafka/admin.rb CHANGED
@@ -10,11 +10,25 @@ module Karafka
10
10
  # Cluster on which operations are performed can be changed via `admin.kafka` config, however
11
11
  # there is no multi-cluster runtime support.
12
12
  module Admin
13
- # More or less number of seconds of 1 hundred years
14
- # Used for time referencing that does not have to be accurate but needs to be big
15
- HUNDRED_YEARS = 100 * 365.25 * 24 * 60 * 60
16
-
17
- private_constant :HUNDRED_YEARS
13
+ extend Helpers::ConfigImporter.new(
14
+ max_wait_time: %i[admin max_wait_time],
15
+ poll_timeout: %i[admin poll_timeout],
16
+ max_attempts: %i[admin max_attempts],
17
+ group_id: %i[admin group_id],
18
+ app_kafka: %i[kafka],
19
+ admin_kafka: %i[admin kafka]
20
+ )
21
+
22
+ # 2010-01-01 00:00:00 - way before Kafka was released so no messages should exist prior to
23
+ # this date
24
+ # We do not use the explicit -2 librdkafka value here because we resolve this offset without
25
+ # consuming data
26
+ LONG_TIME_AGO = Time.at(1_262_300_400)
27
+
28
+ # one day in seconds for future time reference
29
+ DAY_IN_SECONDS = 60 * 60 * 24
30
+
31
+ private_constant :LONG_TIME_AGO, :DAY_IN_SECONDS
18
32
 
19
33
  class << self
20
34
  # Allows us to read messages from the topic
@@ -55,7 +69,7 @@ module Karafka
55
69
  possible_range = requested_range.select { |offset| available_range.include?(offset) }
56
70
 
57
71
  start_offset = possible_range.first
58
- count = possible_range.count
72
+ count = possible_range.size
59
73
 
60
74
  tpl.add_topic_and_partitions_with_offsets(name, partition => start_offset)
61
75
  consumer.assign(tpl)
@@ -108,7 +122,7 @@ module Karafka
108
122
  handler = admin.create_topic(name, partitions, replication_factor, topic_config)
109
123
 
110
124
  with_re_wait(
111
- -> { handler.wait(max_wait_timeout: app_config.admin.max_wait_time) },
125
+ -> { handler.wait(max_wait_timeout: max_wait_time) },
112
126
  -> { topics_names.include?(name) }
113
127
  )
114
128
  end
@@ -122,7 +136,7 @@ module Karafka
122
136
  handler = admin.delete_topic(name)
123
137
 
124
138
  with_re_wait(
125
- -> { handler.wait(max_wait_timeout: app_config.admin.max_wait_time) },
139
+ -> { handler.wait(max_wait_timeout: max_wait_time) },
126
140
  -> { !topics_names.include?(name) }
127
141
  )
128
142
  end
@@ -137,7 +151,7 @@ module Karafka
137
151
  handler = admin.create_partitions(name, partitions)
138
152
 
139
153
  with_re_wait(
140
- -> { handler.wait(max_wait_timeout: app_config.admin.max_wait_time) },
154
+ -> { handler.wait(max_wait_timeout: max_wait_time) },
141
155
  -> { topic_info(name).fetch(:partition_count) >= partitions }
142
156
  )
143
157
  end
@@ -203,14 +217,14 @@ module Karafka
203
217
  # Earliest is not always 0. When compacting/deleting it can be much later, that's why
204
218
  # we fetch the oldest possible offset
205
219
  when 'earliest'
206
- Time.now - HUNDRED_YEARS
220
+ LONG_TIME_AGO
207
221
  # Latest will always be the high-watermark offset and we can get it just by getting
208
222
  # a future position
209
223
  when 'latest'
210
- Time.now + HUNDRED_YEARS
211
- # Same as `'latest'`
224
+ Time.now + DAY_IN_SECONDS
225
+ # Same as `'earliest'`
212
226
  when false
213
- Time.now - HUNDRED_YEARS
227
+ LONG_TIME_AGO
214
228
  # Regular offset case
215
229
  else
216
230
  position
@@ -274,27 +288,24 @@ module Karafka
274
288
  end
275
289
  end
276
290
 
277
- # Takes consumer group and its topics and migrates all the offsets to a new named group
291
+ # Takes consumer group and its topics and copies all the offsets to a new named group
278
292
  #
279
293
  # @param previous_name [String] old consumer group name
280
294
  # @param new_name [String] new consumer group name
281
295
  # @param topics [Array<String>] topics for which we want to migrate offsets during rename
282
- # @param delete_previous [Boolean] should we delete previous consumer group after rename.
283
- # Defaults to true.
296
+ # @return [Boolean] true if anything was migrated, otherwise false
284
297
  #
285
298
  # @note This method should **not** be executed on a running consumer group as it creates a
286
299
  # "fake" consumer and uses it to move offsets.
287
300
  #
288
- # @note After migration unless `delete_previous` is set to `false`, old group will be
289
- # removed.
290
- #
291
301
  # @note If new consumer group exists, old offsets will be added to it.
292
- def rename_consumer_group(previous_name, new_name, topics, delete_previous: true)
302
+ def copy_consumer_group(previous_name, new_name, topics)
293
303
  remap = Hash.new { |h, k| h[k] = {} }
294
304
 
295
305
  old_lags = read_lags_with_offsets({ previous_name => topics })
296
306
 
297
- return if old_lags.empty?
307
+ return false if old_lags.empty?
308
+ return false if old_lags.values.all? { |topic_data| topic_data.values.all?(&:empty?) }
298
309
 
299
310
  read_lags_with_offsets({ previous_name => topics })
300
311
  .fetch(previous_name)
@@ -311,9 +322,35 @@ module Karafka
311
322
 
312
323
  seek_consumer_group(new_name, remap)
313
324
 
314
- return unless delete_previous
325
+ true
326
+ end
327
+
328
+ # Takes consumer group and its topics and migrates all the offsets to a new named group
329
+ #
330
+ # @param previous_name [String] old consumer group name
331
+ # @param new_name [String] new consumer group name
332
+ # @param topics [Array<String>] topics for which we want to migrate offsets during rename
333
+ # @param delete_previous [Boolean] should we delete previous consumer group after rename.
334
+ # Defaults to true.
335
+ # @return [Boolean] true if rename (and optionally removal) was ok or false if there was
336
+ # nothing really to rename
337
+ #
338
+ # @note This method should **not** be executed on a running consumer group as it creates a
339
+ # "fake" consumer and uses it to move offsets.
340
+ #
341
+ # @note After migration unless `delete_previous` is set to `false`, old group will be
342
+ # removed.
343
+ #
344
+ # @note If new consumer group exists, old offsets will be added to it.
345
+ def rename_consumer_group(previous_name, new_name, topics, delete_previous: true)
346
+ copy_result = copy_consumer_group(previous_name, new_name, topics)
347
+
348
+ return false unless copy_result
349
+ return copy_result unless delete_previous
315
350
 
316
351
  delete_consumer_group(previous_name)
352
+
353
+ true
317
354
  end
318
355
 
319
356
  # Removes given consumer group (if exists)
@@ -325,7 +362,7 @@ module Karafka
325
362
  def delete_consumer_group(consumer_group_id)
326
363
  with_admin do |admin|
327
364
  handler = admin.delete_group(consumer_group_id)
328
- handler.wait(max_wait_timeout: app_config.admin.max_wait_time)
365
+ handler.wait(max_wait_timeout: max_wait_time)
329
366
  end
330
367
  end
331
368
 
@@ -509,7 +546,11 @@ module Karafka
509
546
  def with_admin
510
547
  bind_id = SecureRandom.uuid
511
548
 
512
- admin = config(:producer, {}).admin(native_kafka_auto_start: false)
549
+ admin = config(:producer, {}).admin(
550
+ native_kafka_auto_start: false,
551
+ native_kafka_poll_timeout_ms: poll_timeout
552
+ )
553
+
513
554
  bind_oauth(bind_id, admin)
514
555
 
515
556
  admin.start
@@ -572,7 +613,7 @@ module Karafka
572
613
  rescue Rdkafka::AbstractHandle::WaitTimeoutError, Errors::ResultNotVisibleError
573
614
  return if breaker.call
574
615
 
575
- retry if attempt <= app_config.admin.max_attempts
616
+ retry if attempt <= max_attempts
576
617
 
577
618
  raise
578
619
  end
@@ -581,11 +622,10 @@ module Karafka
581
622
  # @param settings [Hash] extra settings for config (if needed)
582
623
  # @return [::Rdkafka::Config] rdkafka config
583
624
  def config(type, settings)
584
- app_config
585
- .kafka
625
+ app_kafka
586
626
  .then(&:dup)
587
- .merge(app_config.admin.kafka)
588
- .tap { |config| config[:'group.id'] = app_config.admin.group_id }
627
+ .merge(admin_kafka)
628
+ .tap { |config| config[:'group.id'] = group_id }
589
629
  # We merge after setting the group id so it can be altered if needed
590
630
  # In general in admin we only should alter it when we need to impersonate a given
591
631
  # consumer group or do something similar
@@ -619,11 +659,6 @@ module Karafka
619
659
  offset
620
660
  end
621
661
  end
622
-
623
- # @return [Karafka::Core::Configurable::Node] root node config
624
- def app_config
625
- ::Karafka::App.config
626
- end
627
662
  end
628
663
  end
629
664
  end
@@ -21,7 +21,7 @@ module Karafka
21
21
 
22
22
  # @return [String] id of the current consumer
23
23
  attr_reader :id
24
- # @return [Karafka::Routing::Topic] topic to which a given consumer is subscribed
24
+ # @return [Karafka::Messages::Messages] current messages batch
25
25
  attr_accessor :messages
26
26
  # @return [Karafka::Connection::Client] kafka connection client
27
27
  attr_accessor :client
@@ -304,7 +304,12 @@ module Karafka
304
304
 
305
305
  offset = nil if offset == :consecutive
306
306
 
307
- client.pause(topic.name, partition, offset)
307
+ client.pause(
308
+ topic.name,
309
+ partition,
310
+ offset,
311
+ coordinator.pause_tracker.current_timeout
312
+ )
308
313
 
309
314
  # Indicate, that user took a manual action of pausing
310
315
  coordinator.manual_pause if manual_pause
@@ -333,17 +338,21 @@ module Karafka
333
338
 
334
339
  # Seeks in the context of current topic and partition
335
340
  #
336
- # @param offset [Integer, Time] offset where we want to seek or time of the offset where we
337
- # want to seek.
341
+ # @param offset [Integer, Time, Symbol, String] one of:
342
+ # - offset where we want to seek
343
+ # - time of the offset where we want to seek
344
+ # - :earliest (or as a string) to move to earliest message
345
+ # - :latest (or as a string) to move to latest (high-watermark)
346
+ #
338
347
  # @param manual_seek [Boolean] Flag to differentiate between user seek and system/strategy
339
348
  # based seek. User seek operations should take precedence over system actions, hence we need
340
349
  # to know who invoked it.
341
- # @param reset_offset [Boolean] should we reset offset when seeking backwards. It is false by
342
- # default to prevent marking in the offset that was earlier than the highest marked offset
343
- # for given consumer group. It can be set to true if we want to reprocess data once again and
350
+ # @param reset_offset [Boolean] should we reset offset when seeking backwards. It is false
351
+ # it prevents marking in the offset that was earlier than the highest marked offset
352
+ # for given consumer group. It is set to true by default to reprocess data once again and
344
353
  # want to make sure that the marking starts from where we moved to.
345
354
  # @note Please note, that if you are seeking to a time offset, getting the offset is blocking
346
- def seek(offset, manual_seek = true, reset_offset: false)
355
+ def seek(offset, manual_seek = true, reset_offset: true)
347
356
  coordinator.manual_seek if manual_seek
348
357
  self.seek_offset = nil if reset_offset
349
358
 
@@ -112,7 +112,7 @@ module Karafka
112
112
  *[names, option[2], option[1]].flatten
113
113
  ) { |value| options[option[0]] = value }
114
114
  end
115
- end.parse(ARGV)
115
+ end.parse(ARGV.dup)
116
116
 
117
117
  options
118
118
  end
@@ -130,8 +130,14 @@ module Karafka
130
130
  # given Cli command
131
131
  # @example for Karafka::Cli::Install
132
132
  # name #=> 'install'
133
+ # @example for Karafka::Cli::TestMe
134
+ # name => 'test_me'
133
135
  def name
134
- to_s.split('::').last.downcase
136
+ to_s
137
+ .split('::')
138
+ .last
139
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
140
+ .downcase
135
141
  end
136
142
 
137
143
  # @return [Array<String>] names and aliases for command matching
@@ -30,10 +30,13 @@ module Karafka
30
30
  return false
31
31
  end
32
32
 
33
- names = resources_to_migrate.map(&:name).join(', ')
34
- puts "Updating configuration of the following topics: #{names}"
35
- Karafka::Admin::Configs.alter(resources_to_migrate)
36
- puts "#{green('Updated')} all requested topics configuration."
33
+ resources_to_migrate.each do |resource|
34
+ supervised("Updating topic: #{resource.name} configuration") do
35
+ Karafka::Admin::Configs.alter(resource)
36
+ end
37
+
38
+ puts "#{green('Updated')} topic #{resource.name} configuration."
39
+ end
37
40
 
38
41
  true
39
42
  end
@@ -12,6 +12,23 @@ module Karafka
12
12
 
13
13
  private
14
14
 
15
+ # Used to run Karafka Admin commands that talk with Kafka and that can fail due to broker
16
+ # errors and other issues. We catch errors and provide nicer printed output prior to
17
+ # re-raising the mapped error for proper exit code status handling
18
+ #
19
+ # @param operation_message [String] message that we use to print that it is going to run
20
+ # and if case if failed with a failure indication.
21
+ def supervised(operation_message)
22
+ puts "#{operation_message}..."
23
+
24
+ yield
25
+ rescue Rdkafka::RdkafkaError => e
26
+ puts "#{operation_message} #{red('failed')}:"
27
+ puts e
28
+
29
+ raise Errors::CommandValidationError, cause: e
30
+ end
31
+
15
32
  # @return [Array<Karafka::Routing::Topic>] all available topics that can be managed
16
33
  # @note If topic is defined in multiple consumer groups, first config will be used. This
17
34
  # means, that this CLI will not work for simultaneous management of multiple clusters
@@ -15,13 +15,15 @@ module Karafka
15
15
  if existing_topics_names.include?(name)
16
16
  puts "#{yellow('Skipping')} because topic #{name} already exists."
17
17
  else
18
- puts "Creating topic #{name}..."
19
- Admin.create_topic(
20
- name,
21
- topic.declaratives.partitions,
22
- topic.declaratives.replication_factor,
23
- topic.declaratives.details
24
- )
18
+ supervised("Creating topic #{name}") do
19
+ Admin.create_topic(
20
+ name,
21
+ topic.declaratives.partitions,
22
+ topic.declaratives.replication_factor,
23
+ topic.declaratives.details
24
+ )
25
+ end
26
+
25
27
  puts "#{green('Created')} topic #{name}."
26
28
  any_created = true
27
29
  end
@@ -13,8 +13,10 @@ module Karafka
13
13
  name = topic.name
14
14
 
15
15
  if existing_topics_names.include?(name)
16
- puts "Deleting topic #{name}..."
17
- Admin.delete_topic(name)
16
+ supervised("Deleting topic #{name}") do
17
+ Admin.delete_topic(name)
18
+ end
19
+
18
20
  puts "#{green('Deleted')} topic #{name}."
19
21
  any_deleted = true
20
22
  else
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ class Cli
5
+ class Topics < Cli::Base
6
+ # Declarative topics CLI sub-help
7
+ class Help < Base
8
+ # Displays help information for all available topics management commands
9
+ def call
10
+ puts <<~HELP
11
+ Karafka topics commands:
12
+ align # Aligns configuration of all declarative topics based on definitions
13
+ create # Creates topics with appropriate settings
14
+ delete # Deletes all topics defined in the routes
15
+ help # Describes available topics management commands
16
+ migrate # Creates missing topics, repartitions existing and aligns configuration
17
+ plan # Plans migration process and prints changes to be applied
18
+ repartition # Adds additional partitions to topics with fewer partitions than expected
19
+ reset # Deletes and re-creates all topics
20
+
21
+ Options:
22
+ --detailed-exitcode # Provides detailed exit codes (0=no changes, 1=error, 2=changes applied)
23
+
24
+ Examples:
25
+ karafka topics create
26
+ karafka topics plan --detailed-exitcode
27
+ karafka topics migrate
28
+ karafka topics align
29
+
30
+ Note: All admin operations run on the default cluster only.
31
+ HELP
32
+
33
+ # We return false to indicate with exit code 0 that no changes were applied
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -21,8 +21,10 @@ module Karafka
21
21
  existing_count = existing_partitions.fetch(name, false)
22
22
 
23
23
  if existing_count && existing_count < desired_count
24
- puts "Increasing number of partitions to #{desired_count} on topic #{name}..."
25
- Admin.create_partitions(name, desired_count)
24
+ supervised("Increasing number of partitions to #{desired_count} on topic #{name}") do
25
+ Admin.create_partitions(name, desired_count)
26
+ end
27
+
26
28
  change = desired_count - existing_count
27
29
  puts "#{green('Created')} #{change} additional partitions on topic #{name}."
28
30
  any_repartitioned = true
@@ -27,10 +27,13 @@ module Karafka
27
27
  # crashes
28
28
  CHANGES_EXIT_CODE = 2
29
29
 
30
- private_constant :NO_CHANGES_EXIT_CODE, :CHANGES_EXIT_CODE
30
+ # Used when there was an error during execution.
31
+ ERROR_EXIT_CODE = 1
32
+
33
+ private_constant :NO_CHANGES_EXIT_CODE, :CHANGES_EXIT_CODE, :ERROR_EXIT_CODE
31
34
 
32
35
  # @param action [String] action we want to take
33
- def call(action = 'missing')
36
+ def call(action = 'help')
34
37
  detailed_exit_code = options.fetch(:detailed_exitcode, false)
35
38
 
36
39
  command = case action
@@ -48,8 +51,10 @@ module Karafka
48
51
  Topics::Align
49
52
  when 'plan'
50
53
  Topics::Plan
54
+ when 'help'
55
+ Topics::Help
51
56
  else
52
- raise ::ArgumentError, "Invalid topics action: #{action}"
57
+ raise Errors::UnrecognizedCommandError, "Unrecognized topics action: #{action}"
53
58
  end
54
59
 
55
60
  changes = command.new.call
@@ -57,6 +62,8 @@ module Karafka
57
62
  return unless detailed_exit_code
58
63
 
59
64
  changes ? exit(CHANGES_EXIT_CODE) : exit(NO_CHANGES_EXIT_CODE)
65
+ rescue Errors::CommandValidationError
66
+ exit(ERROR_EXIT_CODE)
60
67
  end
61
68
  end
62
69
  end
data/lib/karafka/cli.rb CHANGED
@@ -21,6 +21,8 @@ module Karafka
21
21
  args = action ? [action] : []
22
22
 
23
23
  command.new.call(*args)
24
+ elsif command_name.nil?
25
+ Help.new.call
24
26
  else
25
27
  raise(
26
28
  Karafka::Errors::UnrecognizedCommandError,
@@ -9,6 +9,11 @@ module Karafka
9
9
  # closed consumer instance as it causes Ruby VM process to crash.
10
10
  class Client
11
11
  include ::Karafka::Core::Helpers::Time
12
+ include Helpers::ConfigImporter.new(
13
+ logger: %i[logger],
14
+ tick_interval: %i[internal tick_interval],
15
+ shutdown_timeout: %i[shutdown_timeout]
16
+ )
12
17
 
13
18
  attr_reader :rebalance_manager
14
19
 
@@ -65,9 +70,8 @@ module Karafka
65
70
  @closed = false
66
71
  @subscription_group = subscription_group
67
72
  @buffer = RawMessagesBuffer.new
68
- @tick_interval = ::Karafka::App.config.internal.tick_interval
69
73
  @rebalance_manager = RebalanceManager.new(@subscription_group.id, @buffer)
70
- @rebalance_callback = Instrumentation::Callbacks::Rebalance.new(@subscription_group)
74
+ @rebalance_callback = Instrumentation::Callbacks::Rebalance.new(@subscription_group, id)
71
75
 
72
76
  @interval_runner = Helpers::IntervalRunner.new do
73
77
  events_poll
@@ -221,10 +225,14 @@ module Karafka
221
225
  # @param offset [Integer, nil] offset of the message on which we want to pause (this message
222
226
  # will be reprocessed after getting back to processing) or nil if we want to pause and
223
227
  # resume from the consecutive offset (+1 from the last message passed to us by librdkafka)
228
+ # @param timeout [Integer] number of ms timeout of pause. It is used only for
229
+ # instrumentation and not in the pause itself as pausing on this level is infinite always.
224
230
  # @note This will pause indefinitely and requires manual `#resume`
225
231
  # @note When `#internal_seek` is not involved (when offset is `nil`) we will not purge the
226
232
  # librdkafka buffers and continue from the last cursor offset
227
- def pause(topic, partition, offset = nil)
233
+ # @note We accept the timeout value on this layer to have a cohesive pause/resume
234
+ # instrumentation, where all the details are available. It is especially needed, when
235
+ def pause(topic, partition, offset = nil, timeout = 0)
228
236
  @mutex.synchronize do
229
237
  # Do not pause if the client got closed, would not change anything
230
238
  return if @closed
@@ -243,7 +251,8 @@ module Karafka
243
251
  subscription_group: @subscription_group,
244
252
  topic: topic,
245
253
  partition: partition,
246
- offset: offset
254
+ offset: offset,
255
+ timeout: timeout
247
256
  )
248
257
 
249
258
  @paused_tpls[topic][partition] = tpl
@@ -313,7 +322,7 @@ module Karafka
313
322
  @unsubscribing = true
314
323
 
315
324
  # Give 50% of time for the final close before we reach the forceful
316
- max_wait = ::Karafka::App.config.shutdown_timeout * COOP_UNSUBSCRIBE_FACTOR
325
+ max_wait = shutdown_timeout * COOP_UNSUBSCRIBE_FACTOR
317
326
  used = 0
318
327
  stopped_at = monotonic_now
319
328
 
@@ -472,15 +481,27 @@ module Karafka
472
481
  message.offset = detected_partition&.offset || raise(Errors::InvalidTimeBasedOffsetError)
473
482
  end
474
483
 
484
+ # Those two are librdkafka hardcoded values
485
+ message.offset = -1 if message.offset.to_s == 'latest'
486
+ message.offset = -2 if message.offset.to_s == 'earliest'
487
+
475
488
  # Never seek if we would get the same location as we would get without seeking
476
489
  # This prevents us from the expensive buffer purges that can lead to increased network
477
490
  # traffic and can cost a lot of money
478
491
  #
479
492
  # This code adds around 0.01 ms per seek but saves from many user unexpected behaviours in
480
493
  # seeking and pausing
481
- return if message.offset == topic_partition_position(message.topic, message.partition)
494
+ position = topic_partition_position(message.topic, message.partition)
495
+
496
+ # Always seek if current position cannot be fetched or is negative. Offset seek can also
497
+ # be negative (-1 or -2) and we should not compare it with the position because they are
498
+ # special (earliest or latest)
499
+ return kafka.seek(message) if position.negative?
500
+ # If offset is the same as the next position, we don't have to seek to get there, hence
501
+ # only in such case we can do nothing.
502
+ return kafka.seek(message) if message.offset != position
482
503
 
483
- kafka.seek(message)
504
+ nil
484
505
  end
485
506
 
486
507
  # Commits the stored offsets in a sync way and closes the consumer.
@@ -573,7 +594,7 @@ module Karafka
573
594
  # We should not run a single poll longer than the tick frequency. Otherwise during a single
574
595
  # `#batch_poll` we would not be able to run `#events_poll` often enough effectively
575
596
  # blocking events from being handled.
576
- poll_tick = timeout > @tick_interval ? @tick_interval : timeout
597
+ poll_tick = timeout > tick_interval ? tick_interval : timeout
577
598
 
578
599
  result = kafka.poll(poll_tick)
579
600
 
@@ -655,7 +676,7 @@ module Karafka
655
676
  # Builds a new rdkafka consumer instance based on the subscription group configuration
656
677
  # @return [Rdkafka::Consumer]
657
678
  def build_consumer
658
- ::Rdkafka::Config.logger = ::Karafka::App.config.logger
679
+ ::Rdkafka::Config.logger = logger
659
680
 
660
681
  # We need to refresh the setup of this subscription group in case we started running in a
661
682
  # swarm. The initial configuration for validation comes from the parent node, but it needs