karafka 2.3.3 → 2.4.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 (127) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +12 -38
  4. data/CHANGELOG.md +59 -0
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +29 -27
  7. data/bin/integrations +1 -1
  8. data/config/locales/errors.yml +21 -2
  9. data/config/locales/pro_errors.yml +16 -1
  10. data/karafka.gemspec +4 -2
  11. data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
  12. data/lib/karafka/admin/configs/config.rb +81 -0
  13. data/lib/karafka/admin/configs/resource.rb +88 -0
  14. data/lib/karafka/admin/configs.rb +103 -0
  15. data/lib/karafka/admin.rb +211 -90
  16. data/lib/karafka/base_consumer.rb +2 -2
  17. data/lib/karafka/cli/info.rb +9 -7
  18. data/lib/karafka/cli/server.rb +7 -7
  19. data/lib/karafka/cli/topics/align.rb +109 -0
  20. data/lib/karafka/cli/topics/base.rb +66 -0
  21. data/lib/karafka/cli/topics/create.rb +35 -0
  22. data/lib/karafka/cli/topics/delete.rb +30 -0
  23. data/lib/karafka/cli/topics/migrate.rb +31 -0
  24. data/lib/karafka/cli/topics/plan.rb +169 -0
  25. data/lib/karafka/cli/topics/repartition.rb +41 -0
  26. data/lib/karafka/cli/topics/reset.rb +18 -0
  27. data/lib/karafka/cli/topics.rb +13 -123
  28. data/lib/karafka/connection/client.rb +55 -37
  29. data/lib/karafka/connection/listener.rb +22 -17
  30. data/lib/karafka/connection/proxy.rb +93 -4
  31. data/lib/karafka/connection/status.rb +14 -2
  32. data/lib/karafka/constraints.rb +3 -3
  33. data/lib/karafka/contracts/config.rb +14 -1
  34. data/lib/karafka/contracts/topic.rb +1 -1
  35. data/lib/karafka/deserializers/headers.rb +15 -0
  36. data/lib/karafka/deserializers/key.rb +15 -0
  37. data/lib/karafka/deserializers/payload.rb +16 -0
  38. data/lib/karafka/embedded.rb +2 -0
  39. data/lib/karafka/helpers/async.rb +5 -2
  40. data/lib/karafka/helpers/colorize.rb +6 -0
  41. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
  42. data/lib/karafka/instrumentation/logger_listener.rb +23 -3
  43. data/lib/karafka/instrumentation/notifications.rb +10 -0
  44. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
  45. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  46. data/lib/karafka/messages/batch_metadata.rb +1 -1
  47. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  48. data/lib/karafka/messages/builders/message.rb +10 -6
  49. data/lib/karafka/messages/message.rb +2 -1
  50. data/lib/karafka/messages/metadata.rb +20 -4
  51. data/lib/karafka/messages/parser.rb +1 -1
  52. data/lib/karafka/pro/base_consumer.rb +12 -23
  53. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  54. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  55. data/lib/karafka/pro/encryption/errors.rb +4 -1
  56. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  57. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  58. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  59. data/lib/karafka/pro/iterator/expander.rb +2 -1
  60. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  61. data/lib/karafka/pro/iterator.rb +28 -2
  62. data/lib/karafka/pro/loader.rb +3 -0
  63. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  64. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  65. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  66. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  67. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  68. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  76. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  77. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  78. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  79. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  80. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  81. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  82. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  87. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  88. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  89. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  90. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
  91. data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
  92. data/lib/karafka/pro/routing/features/swarm.rb +11 -0
  93. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  94. data/lib/karafka/processing/coordinator.rb +17 -8
  95. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  96. data/lib/karafka/processing/executor.rb +6 -2
  97. data/lib/karafka/processing/executors_buffer.rb +5 -2
  98. data/lib/karafka/processing/jobs_queue.rb +9 -4
  99. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  100. data/lib/karafka/processing/strategies/default.rb +7 -1
  101. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  102. data/lib/karafka/processing/workers_batch.rb +4 -1
  103. data/lib/karafka/routing/builder.rb +6 -2
  104. data/lib/karafka/routing/consumer_group.rb +2 -1
  105. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  106. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  107. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  108. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  109. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  110. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  111. data/lib/karafka/routing/features/deserializers.rb +11 -0
  112. data/lib/karafka/routing/proxy.rb +9 -14
  113. data/lib/karafka/routing/router.rb +11 -2
  114. data/lib/karafka/routing/subscription_group.rb +9 -1
  115. data/lib/karafka/routing/topic.rb +0 -1
  116. data/lib/karafka/runner.rb +1 -1
  117. data/lib/karafka/setup/config.rb +50 -9
  118. data/lib/karafka/status.rb +7 -8
  119. data/lib/karafka/swarm/supervisor.rb +16 -2
  120. data/lib/karafka/templates/karafka.rb.erb +28 -1
  121. data/lib/karafka/version.rb +1 -1
  122. data.tar.gz.sig +0 -0
  123. metadata +38 -12
  124. metadata.gz.sig +0 -0
  125. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  126. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  127. data/lib/karafka/time_trackers/partition_usage.rb +0 -56
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Admin
5
+ # Namespace for admin operations related to configuration management
6
+ #
7
+ # At the moment Karafka supports configuration management for brokers and topics
8
+ #
9
+ # You can describe configuration as well as alter it.
10
+ #
11
+ # Altering is done in the incremental way.
12
+ module Configs
13
+ class << self
14
+ # Fetches given resources configurations from Kafka
15
+ #
16
+ # @param resources [Resource, Array<Resource>] single resource we want to describe or
17
+ # list of resources we are interested in. It is useful to provide multiple resources
18
+ # when you need data from multiple topics, etc. Karafka will make one query for all the
19
+ # data instead of doing one per topic.
20
+ #
21
+ # @return [Array<Resource>] array with resources containing their configuration details
22
+ #
23
+ # @note Even if you request one resource, result will always be an array with resources
24
+ #
25
+ # @example Describe topic named "example" and print its config
26
+ # resource = Karafka::Admin::Configs::Resource.new(type: :topic, name: 'example')
27
+ # results = Karafka::Admin::Configs.describe(resource)
28
+ # results.first.configs.each do |config|
29
+ # puts "#{config.name} - #{config.value}"
30
+ # end
31
+ def describe(*resources)
32
+ operate_on_resources(
33
+ :describe_configs,
34
+ resources
35
+ )
36
+ end
37
+
38
+ # Alters given resources based on the alteration operations accumulated in the provided
39
+ # resources
40
+ #
41
+ # @param resources [Resource, Array<Resource>] single resource we want to alter or
42
+ # list of resources.
43
+ #
44
+ # @note This operation is not transactional and can work only partially if some config
45
+ # options are not valid. Always make sure, your alterations are correct.
46
+ #
47
+ # @note We call it `#alter` despite using the Kafka incremental alter API because the
48
+ # regular alter is deprecated.
49
+ #
50
+ # @example Alter the `delete.retention.ms` and set it to 8640001
51
+ # resource = Karafka::Admin::Configs::Resource.new(type: :topic, name: 'example')
52
+ # resource.set('delete.retention.ms', '8640001')
53
+ # Karafka::Admin::Configs.alter(resource)
54
+ def alter(*resources)
55
+ operate_on_resources(
56
+ :incremental_alter_configs,
57
+ resources
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ # @param action [Symbol] runs given action via Rdkafka Admin
64
+ # @param resources [Array<Resource>] resources on which we want to operate
65
+ def operate_on_resources(action, resources)
66
+ resources = Array(resources).flatten
67
+
68
+ result = with_admin_wait do |admin|
69
+ admin.public_send(
70
+ action,
71
+ resources.map(&:to_native_hash)
72
+ )
73
+ end
74
+
75
+ result.resources.map do |rd_kafka_resource|
76
+ # Create back a resource
77
+ resource = Resource.new(
78
+ name: rd_kafka_resource.name,
79
+ type: rd_kafka_resource.type
80
+ )
81
+
82
+ rd_kafka_resource.configs.each do |rd_kafka_config|
83
+ resource.configs << Config.from_rd_kafka(rd_kafka_config)
84
+ end
85
+
86
+ resource.configs.sort_by!(&:name)
87
+ resource.configs.freeze
88
+
89
+ resource
90
+ end
91
+ end
92
+
93
+ # Yields admin instance, allows to run Acl operations and awaits on the final result
94
+ # Makes sure that admin is closed afterwards.
95
+ def with_admin_wait
96
+ Admin.with_admin do |admin|
97
+ yield(admin).wait(max_wait_timeout: Karafka::App.config.admin.max_wait_time)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
data/lib/karafka/admin.rb CHANGED
@@ -132,7 +132,7 @@ module Karafka
132
132
 
133
133
  with_re_wait(
134
134
  -> { handler.wait(max_wait_timeout: app_config.admin.max_wait_time) },
135
- -> { topic(name).fetch(:partition_count) >= partitions }
135
+ -> { topic_info(name).fetch(:partition_count) >= partitions }
136
136
  )
137
137
  end
138
138
  end
@@ -168,7 +168,7 @@ module Karafka
168
168
  if partitions_with_offsets.is_a?(Hash)
169
169
  tpl_base[topic] = partitions_with_offsets
170
170
  else
171
- topic(topic)[:partition_count].times do |partition|
171
+ topic_info(topic)[:partition_count].times do |partition|
172
172
  tpl_base[topic][partition] = partitions_with_offsets
173
173
  end
174
174
  end
@@ -183,66 +183,62 @@ module Karafka
183
183
  tpl_base.each do |topic, partitions_with_offsets|
184
184
  partitions_with_offsets.each do |partition, offset|
185
185
  target = offset.is_a?(Time) ? time_tpl : tpl
186
- target.add_topic_and_partitions_with_offsets(topic, [[partition, offset]])
186
+ # We reverse and uniq to make sure that potentially duplicated references are removed
187
+ # in such a way that the newest stays
188
+ target.to_h[topic] ||= []
189
+ target.to_h[topic] << Rdkafka::Consumer::Partition.new(partition, offset)
190
+ target.to_h[topic].reverse!
191
+ target.to_h[topic].uniq!(&:partition)
192
+ target.to_h[topic].reverse!
187
193
  end
188
194
  end
189
195
 
190
- # We set this that way so we can impersonate this consumer group and seek where we want
191
- mapped_consumer_group_id = app_config.consumer_mapper.call(consumer_group_id)
192
- settings = { 'group.id': mapped_consumer_group_id }
193
-
194
- # This error can occur when we query a broker that is not a coordinator because something
195
- # was changing in the cluster. We should be able to safely restart our seeking request
196
- # when this happens without any issues
197
- #
198
- # We wrap the consumer creation, so we retry with a new consumer instance
199
- with_rdkafka_retry(codes: %i[not_coordinator]) do
200
- with_consumer(settings) do |consumer|
201
- # If we have any time based stuff to resolve, we need to do it prior to commits
202
- unless time_tpl.empty?
203
- real_offsets = consumer.offsets_for_times(time_tpl)
204
-
205
- real_offsets.to_h.each do |name, results|
206
- results.each do |result|
207
- raise(Errors::InvalidTimeBasedOffsetError) unless result
208
-
209
- partition = result.partition
210
-
211
- # Negative offset means we're beyond last message and we need to query for the
212
- # high watermark offset to get the most recent offset and move there
213
- if result.offset.negative?
214
- _, offset = consumer.query_watermark_offsets(name, result.partition)
215
- else
216
- # If we get an offset, it means there existed a message close to this time
217
- # location
218
- offset = result.offset
219
- end
220
-
221
- # Since now we have proper offsets, we can add this to the final tpl for commit
222
- tpl.add_topic_and_partitions_with_offsets(name, [[partition, offset]])
196
+ settings = { 'group.id': consumer_group_id }
197
+
198
+ with_consumer(settings) do |consumer|
199
+ # If we have any time based stuff to resolve, we need to do it prior to commits
200
+ unless time_tpl.empty?
201
+ real_offsets = consumer.offsets_for_times(time_tpl)
202
+
203
+ real_offsets.to_h.each do |name, results|
204
+ results.each do |result|
205
+ raise(Errors::InvalidTimeBasedOffsetError) unless result
206
+
207
+ partition = result.partition
208
+
209
+ # Negative offset means we're beyond last message and we need to query for the
210
+ # high watermark offset to get the most recent offset and move there
211
+ if result.offset.negative?
212
+ _, offset = consumer.query_watermark_offsets(name, result.partition)
213
+ else
214
+ # If we get an offset, it means there existed a message close to this time
215
+ # location
216
+ offset = result.offset
223
217
  end
218
+
219
+ # Since now we have proper offsets, we can add this to the final tpl for commit
220
+ tpl.to_h[name] ||= []
221
+ tpl.to_h[name] << Rdkafka::Consumer::Partition.new(partition, offset)
222
+ tpl.to_h[name].reverse!
223
+ tpl.to_h[name].uniq!(&:partition)
224
+ tpl.to_h[name].reverse!
224
225
  end
225
226
  end
226
-
227
- consumer.commit(tpl, false)
228
227
  end
228
+
229
+ consumer.commit_offsets(tpl, async: false)
229
230
  end
230
231
  end
231
232
 
232
233
  # Removes given consumer group (if exists)
233
234
  #
234
- # @param consumer_group_id [String] consumer group name without the mapper name (if any used)
235
- #
236
- # @note Please note, Karafka will apply the consumer group mapper on the provided consumer
237
- # group.
235
+ # @param consumer_group_id [String] consumer group name
238
236
  #
239
237
  # @note This method should not be used on a running consumer group as it will not yield any
240
238
  # results.
241
239
  def delete_consumer_group(consumer_group_id)
242
- mapped_consumer_group_id = app_config.consumer_mapper.call(consumer_group_id)
243
-
244
240
  with_admin do |admin|
245
- handler = admin.delete_group(mapped_consumer_group_id)
241
+ handler = admin.delete_group(consumer_group_id)
246
242
  handler.wait(max_wait_timeout: app_config.admin.max_wait_time)
247
243
  end
248
244
  end
@@ -254,13 +250,118 @@ module Karafka
254
250
  # @return [Array<Integer, Integer>] low watermark offset and high watermark offset
255
251
  def read_watermark_offsets(name, partition)
256
252
  with_consumer do |consumer|
257
- # For newly created topics or in cases where we're trying to get them but there is no
258
- # leader, this can fail. It happens more often for new topics under KRaft, however we
259
- # still want to make sure things operate as expected even then
260
- with_rdkafka_retry(codes: %i[not_leader_for_partition]) do
261
- consumer.query_watermark_offsets(name, partition)
253
+ consumer.query_watermark_offsets(name, partition)
254
+ end
255
+ end
256
+
257
+ # Reads lags and offsets for given topics in the context of consumer groups defined in the
258
+ # routing
259
+ # @param consumer_groups_with_topics [Hash<String, Array<String>>] hash with consumer groups
260
+ # names with array of topics to query per consumer group inside
261
+ # @param active_topics_only [Boolean] if set to false, when we use routing topics, will
262
+ # select also topics that are marked as inactive in routing
263
+ # @return [Hash<String, Hash<Integer, <Hash<Integer>>>>] hash where the top level keys are
264
+ # the consumer groups and values are hashes with topics and inside partitions with lags
265
+ # and offsets
266
+ #
267
+ # @note For topics that do not exist, topic details will be set to an empty hash
268
+ #
269
+ # @note For topics that exist but were never consumed by a given CG we set `-1` as lag and
270
+ # the offset on each of the partitions that were not consumed.
271
+ #
272
+ # @note This lag reporting is for committed lags and is "Kafka-centric", meaning that this
273
+ # represents lags from Kafka perspective and not the consumer. They may differ.
274
+ def read_lags_with_offsets(consumer_groups_with_topics = {}, active_topics_only: true)
275
+ # We first fetch all the topics with partitions count that exist in the cluster so we
276
+ # do not query for topics that do not exist and so we can get partitions count for all
277
+ # the topics we may need. The non-existent and not consumed will be filled at the end
278
+ existing_topics = cluster_info.topics.map do |topic|
279
+ [topic[:topic_name], topic[:partition_count]]
280
+ end.to_h.freeze
281
+
282
+ # If no expected CGs, we use all from routing that have active topics
283
+ if consumer_groups_with_topics.empty?
284
+ consumer_groups_with_topics = Karafka::App.routes.map do |cg|
285
+ cg_topics = cg.topics.select do |cg_topic|
286
+ active_topics_only ? cg_topic.active? : true
287
+ end
288
+
289
+ [cg.id, cg_topics.map(&:name)]
290
+ end.to_h
291
+ end
292
+
293
+ # We make a copy because we will remove once with non-existing topics
294
+ # We keep original requested consumer groups with topics for later backfilling
295
+ cgs_with_topics = consumer_groups_with_topics.dup
296
+ cgs_with_topics.transform_values!(&:dup)
297
+
298
+ # We can query only topics that do exist, this is why we are cleaning those that do not
299
+ # exist
300
+ cgs_with_topics.each_value do |requested_topics|
301
+ requested_topics.delete_if { |topic| !existing_topics.include?(topic) }
302
+ end
303
+
304
+ groups_lags = Hash.new { |h, k| h[k] = {} }
305
+ groups_offs = Hash.new { |h, k| h[k] = {} }
306
+
307
+ cgs_with_topics.each do |cg, topics|
308
+ # Do not add to tpl topics that do not exist
309
+ next if topics.empty?
310
+
311
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
312
+
313
+ with_consumer('group.id': cg) do |consumer|
314
+ topics.each { |topic| tpl.add_topic(topic, existing_topics[topic]) }
315
+
316
+ commit_offsets = consumer.committed(tpl)
317
+
318
+ commit_offsets.to_h.each do |topic, partitions|
319
+ groups_offs[cg][topic] = {}
320
+
321
+ partitions.each do |partition|
322
+ # -1 when no offset is stored
323
+ groups_offs[cg][topic][partition.partition] = partition.offset || -1
324
+ end
325
+ end
326
+
327
+ consumer.lag(commit_offsets).each do |topic, partitions_lags|
328
+ groups_lags[cg][topic] = partitions_lags
329
+ end
330
+ end
331
+ end
332
+
333
+ consumer_groups_with_topics.each do |cg, topics|
334
+ groups_lags[cg]
335
+
336
+ topics.each do |topic|
337
+ groups_lags[cg][topic] ||= {}
338
+
339
+ next unless existing_topics.key?(topic)
340
+
341
+ # We backfill because there is a case where our consumer group would consume for
342
+ # example only one partition out of 20, rest needs to get -1
343
+ existing_topics[topic].times do |partition_id|
344
+ groups_lags[cg][topic][partition_id] ||= -1
345
+ end
346
+ end
347
+ end
348
+
349
+ merged = Hash.new { |h, k| h[k] = {} }
350
+
351
+ groups_lags.each do |cg, topics|
352
+ topics.each do |topic, partitions|
353
+ merged[cg][topic] = {}
354
+
355
+ partitions.each do |partition, lag|
356
+ merged[cg][topic][partition] = {
357
+ offset: groups_offs.fetch(cg).fetch(topic).fetch(partition),
358
+ lag: lag
359
+ }
360
+ end
262
361
  end
263
362
  end
363
+
364
+ merged
264
365
  end
265
366
 
266
367
  # @return [Rdkafka::Metadata] cluster metadata info
@@ -268,6 +369,24 @@ module Karafka
268
369
  with_admin(&:metadata)
269
370
  end
270
371
 
372
+ # Returns basic topic metadata
373
+ #
374
+ # @param topic_name [String] name of the topic we're interested in
375
+ # @return [Hash] topic metadata info hash
376
+ # @raise [Rdkafka::RdkafkaError] `unknown_topic_or_part` if requested topic is not found
377
+ #
378
+ # @note This query is much more efficient than doing a full `#cluster_info` + topic lookup
379
+ # because it does not have to query for all the topics data but just the topic we're
380
+ # interested in
381
+ def topic_info(topic_name)
382
+ with_admin do |admin|
383
+ admin
384
+ .metadata(topic_name)
385
+ .topics
386
+ .find { |topic| topic[:topic_name] == topic_name }
387
+ end
388
+ end
389
+
271
390
  # Creates consumer instance and yields it. After usage it closes the consumer instance
272
391
  # This API can be used in other pieces of code and allows for low-level consumer usage
273
392
  #
@@ -276,7 +395,12 @@ module Karafka
276
395
  # @note We always ship and yield a proxied consumer because admin API performance is not
277
396
  # that relevant. That is, there are no high frequency calls that would have to be delegated
278
397
  def with_consumer(settings = {})
279
- consumer = config(:consumer, settings).consumer
398
+ bind_id = SecureRandom.uuid
399
+
400
+ consumer = config(:consumer, settings).consumer(native_kafka_auto_start: false)
401
+ bind_oauth(bind_id, consumer)
402
+
403
+ consumer.start
280
404
  proxy = ::Karafka::Connection::Proxy.new(consumer)
281
405
  yield(proxy)
282
406
  ensure
@@ -291,30 +415,56 @@ module Karafka
291
415
  end
292
416
 
293
417
  consumer&.close
418
+
419
+ unbind_oauth(bind_id)
294
420
  end
295
421
 
296
422
  # Creates admin instance and yields it. After usage it closes the admin instance
297
423
  def with_admin
298
- admin = config(:producer, {}).admin
299
- yield(admin)
424
+ bind_id = SecureRandom.uuid
425
+
426
+ admin = config(:producer, {}).admin(native_kafka_auto_start: false)
427
+ bind_oauth(bind_id, admin)
428
+
429
+ admin.start
430
+ proxy = ::Karafka::Connection::Proxy.new(admin)
431
+ yield(proxy)
300
432
  ensure
301
433
  admin&.close
434
+
435
+ unbind_oauth(bind_id)
302
436
  end
303
437
 
304
438
  private
305
439
 
440
+ # Adds a new callback for given rdkafka instance for oauth token refresh (if needed)
441
+ #
442
+ # @param id [String, Symbol] unique (for the lifetime of instance) id that we use for
443
+ # callback referencing
444
+ # @param instance [Rdkafka::Consumer, Rdkafka::Admin] rdkafka instance to be used to set
445
+ # appropriate oauth token when needed
446
+ def bind_oauth(id, instance)
447
+ ::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.add(
448
+ id,
449
+ Instrumentation::Callbacks::OauthbearerTokenRefresh.new(
450
+ instance
451
+ )
452
+ )
453
+ end
454
+
455
+ # Removes the callback from no longer used instance
456
+ #
457
+ # @param id [String, Symbol] unique (for the lifetime of instance) id that we use for
458
+ # callback referencing
459
+ def unbind_oauth(id)
460
+ ::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.delete(id)
461
+ end
462
+
306
463
  # @return [Array<String>] topics names
307
464
  def topics_names
308
465
  cluster_info.topics.map { |topic| topic.fetch(:topic_name) }
309
466
  end
310
467
 
311
- # Finds details about given topic
312
- # @param name [String] topic name
313
- # @return [Hash] topic details
314
- def topic(name)
315
- cluster_info.topics.find { |topic| topic[:topic_name] == name }
316
- end
317
-
318
468
  # There are some cases where rdkafka admin operations finish successfully but without the
319
469
  # callback being triggered to materialize the post-promise object. Until this is fixed we
320
470
  # can figure out, that operation we wanted to do finished successfully by checking that the
@@ -341,44 +491,15 @@ module Karafka
341
491
  raise
342
492
  end
343
493
 
344
- # Handles retries for rdkafka related errors that we specify in `:codes`.
345
- #
346
- # Some operations temporarily fail, especially for cases where we changed something fast
347
- # like topic creation or repartitioning. In cases like this it is ok to retry operations that
348
- # do not change the state as it will usually recover.
349
- #
350
- # @param codes [Array<Symbol>] librdkafka error codes on which we want to retry
351
- # @param max_attempts [Integer] number of attempts (including initial) after which we should
352
- # give up
353
- #
354
- # @note This code implements a simple backoff that increases with each attempt.
355
- def with_rdkafka_retry(codes:, max_attempts: 5)
356
- attempt ||= 0
357
- attempt += 1
358
-
359
- yield
360
- rescue Rdkafka::RdkafkaError => e
361
- raise unless codes.include?(e.code)
362
- raise if attempt >= max_attempts
363
-
364
- sleep(max_attempts)
365
-
366
- retry
367
- end
368
-
369
494
  # @param type [Symbol] type of config we want
370
495
  # @param settings [Hash] extra settings for config (if needed)
371
496
  # @return [::Rdkafka::Config] rdkafka config
372
497
  def config(type, settings)
373
- mapped_admin_group_id = app_config.consumer_mapper.call(
374
- app_config.admin.group_id
375
- )
376
-
377
498
  app_config
378
499
  .kafka
379
500
  .then(&:dup)
380
501
  .merge(app_config.admin.kafka)
381
- .tap { |config| config[:'group.id'] = mapped_admin_group_id }
502
+ .tap { |config| config[:'group.id'] = app_config.admin.group_id }
382
503
  # We merge after setting the group id so it can be altered if needed
383
504
  # In general in admin we only should alter it when we need to impersonate a given
384
505
  # consumer group or do something similar
@@ -217,7 +217,7 @@ module Karafka
217
217
  subscription_group: topic.subscription_group,
218
218
  offset: offset,
219
219
  timeout: coordinator.pause_tracker.current_timeout,
220
- attempt: coordinator.pause_tracker.attempt
220
+ attempt: attempt
221
221
  )
222
222
  end
223
223
 
@@ -297,7 +297,7 @@ module Karafka
297
297
  partition: partition,
298
298
  offset: coordinator.seek_offset,
299
299
  timeout: coordinator.pause_tracker.current_timeout,
300
- attempt: coordinator.pause_tracker.attempt
300
+ attempt: attempt
301
301
  )
302
302
  end
303
303
  end
@@ -5,6 +5,12 @@ module Karafka
5
5
  class Cli
6
6
  # Info Karafka Cli action
7
7
  class Info < Base
8
+ include Helpers::ConfigImporter.new(
9
+ concurrency: %i[concurrency],
10
+ license: %i[license],
11
+ client_id: %i[client_id]
12
+ )
13
+
8
14
  desc 'Prints configuration details and other options of your application'
9
15
 
10
16
  # Nice karafka banner
@@ -29,8 +35,6 @@ module Karafka
29
35
 
30
36
  # @return [Array<String>] core framework related info
31
37
  def core_info
32
- config = Karafka::App.config
33
-
34
38
  postfix = Karafka.pro? ? ' + Pro' : ''
35
39
 
36
40
  [
@@ -39,8 +43,8 @@ module Karafka
39
43
  "Rdkafka version: #{::Rdkafka::VERSION}",
40
44
  "Consumer groups count: #{Karafka::App.consumer_groups.size}",
41
45
  "Subscription groups count: #{Karafka::App.subscription_groups.values.flatten.size}",
42
- "Workers count: #{Karafka::App.config.concurrency}",
43
- "Application client id: #{config.client_id}",
46
+ "Workers count: #{concurrency}",
47
+ "Application client id: #{client_id}",
44
48
  "Boot file: #{Karafka.boot_file}",
45
49
  "Environment: #{Karafka.env}"
46
50
  ]
@@ -48,12 +52,10 @@ module Karafka
48
52
 
49
53
  # @return [Array<String>] license related info
50
54
  def license_info
51
- config = Karafka::App.config
52
-
53
55
  if Karafka.pro?
54
56
  [
55
57
  'License: Commercial',
56
- "License entity: #{config.license.entity}"
58
+ "License entity: #{license.entity}"
57
59
  ]
58
60
  else
59
61
  [
@@ -5,6 +5,10 @@ module Karafka
5
5
  class Cli
6
6
  # Server Karafka Cli action
7
7
  class Server < Base
8
+ include Helpers::ConfigImporter.new(
9
+ activity_manager: %i[internal routing activity_manager]
10
+ )
11
+
8
12
  # Types of things we can include / exclude from the routing via the CLI options
9
13
  SUPPORTED_TYPES = ::Karafka::Routing::ActivityManager::SUPPORTED_TYPES
10
14
 
@@ -90,23 +94,19 @@ module Karafka
90
94
 
91
95
  # Registers things we want to include (if defined)
92
96
  def register_inclusions
93
- activities = ::Karafka::App.config.internal.routing.activity_manager
94
-
95
97
  SUPPORTED_TYPES.each do |type|
96
98
  names = options[type] || []
97
99
 
98
- names.each { |name| activities.include(type, name) }
100
+ names.each { |name| activity_manager.include(type, name) }
99
101
  end
100
102
  end
101
103
 
102
104
  # Registers things we want to exclude (if defined)
103
105
  def register_exclusions
104
- activities = ::Karafka::App.config.internal.routing.activity_manager
105
-
106
- activities.class::SUPPORTED_TYPES.each do |type|
106
+ activity_manager.class::SUPPORTED_TYPES.each do |type|
107
107
  names = options[:"exclude_#{type}"] || []
108
108
 
109
- names.each { |name| activities.exclude(type, name) }
109
+ names.each { |name| activity_manager.exclude(type, name) }
110
110
  end
111
111
  end
112
112
  end