karafka 2.3.4 → 2.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) 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 +56 -2
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +25 -23
  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 +201 -100
  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/contracts/config.rb +14 -1
  33. data/lib/karafka/contracts/topic.rb +1 -1
  34. data/lib/karafka/deserializers/headers.rb +15 -0
  35. data/lib/karafka/deserializers/key.rb +15 -0
  36. data/lib/karafka/deserializers/payload.rb +16 -0
  37. data/lib/karafka/embedded.rb +2 -0
  38. data/lib/karafka/helpers/async.rb +5 -2
  39. data/lib/karafka/helpers/colorize.rb +6 -0
  40. data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
  41. data/lib/karafka/instrumentation/logger_listener.rb +23 -3
  42. data/lib/karafka/instrumentation/notifications.rb +10 -0
  43. data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
  44. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  45. data/lib/karafka/messages/batch_metadata.rb +1 -1
  46. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  47. data/lib/karafka/messages/builders/message.rb +10 -6
  48. data/lib/karafka/messages/message.rb +2 -1
  49. data/lib/karafka/messages/metadata.rb +20 -4
  50. data/lib/karafka/messages/parser.rb +1 -1
  51. data/lib/karafka/pro/base_consumer.rb +12 -23
  52. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  53. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  54. data/lib/karafka/pro/encryption/errors.rb +4 -1
  55. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  56. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  57. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  58. data/lib/karafka/pro/iterator/expander.rb +2 -1
  59. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  60. data/lib/karafka/pro/iterator.rb +28 -2
  61. data/lib/karafka/pro/loader.rb +3 -0
  62. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  63. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  64. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  65. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  66. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  67. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  68. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  76. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  77. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  78. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  79. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  80. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  81. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  82. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  87. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  88. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  89. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
  90. data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
  91. data/lib/karafka/pro/routing/features/swarm.rb +11 -0
  92. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  93. data/lib/karafka/processing/coordinator.rb +17 -8
  94. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  95. data/lib/karafka/processing/executor.rb +6 -2
  96. data/lib/karafka/processing/executors_buffer.rb +5 -2
  97. data/lib/karafka/processing/jobs_queue.rb +9 -4
  98. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  99. data/lib/karafka/processing/strategies/default.rb +7 -1
  100. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  101. data/lib/karafka/processing/workers_batch.rb +4 -1
  102. data/lib/karafka/routing/builder.rb +6 -2
  103. data/lib/karafka/routing/consumer_group.rb +2 -1
  104. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  105. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  106. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  107. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  108. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  109. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  110. data/lib/karafka/routing/features/deserializers.rb +11 -0
  111. data/lib/karafka/routing/proxy.rb +9 -14
  112. data/lib/karafka/routing/router.rb +11 -2
  113. data/lib/karafka/routing/subscription_group.rb +9 -1
  114. data/lib/karafka/routing/topic.rb +0 -1
  115. data/lib/karafka/runner.rb +1 -1
  116. data/lib/karafka/setup/config.rb +50 -9
  117. data/lib/karafka/status.rb +7 -8
  118. data/lib/karafka/swarm/supervisor.rb +16 -2
  119. data/lib/karafka/templates/karafka.rb.erb +28 -1
  120. data/lib/karafka/version.rb +1 -1
  121. data.tar.gz.sig +0 -0
  122. metadata +38 -12
  123. metadata.gz.sig +0 -0
  124. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  125. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  126. 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,76 +183,52 @@ 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
- # 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!
186
+ target.add_topic_and_partitions_with_offsets(topic, [[partition, offset]])
193
187
  end
194
188
  end
195
189
 
196
- # We set this that way so we can impersonate this consumer group and seek where we want
197
- mapped_consumer_group_id = app_config.consumer_mapper.call(consumer_group_id)
198
- settings = { 'group.id': mapped_consumer_group_id }
199
-
200
- # This error can occur when we query a broker that is not a coordinator because something
201
- # was changing in the cluster. We should be able to safely restart our seeking request
202
- # when this happens without any issues
203
- #
204
- # We wrap the consumer creation, so we retry with a new consumer instance
205
- with_rdkafka_retry(codes: %i[not_coordinator]) do
206
- with_consumer(settings) do |consumer|
207
- # If we have any time based stuff to resolve, we need to do it prior to commits
208
- unless time_tpl.empty?
209
- real_offsets = consumer.offsets_for_times(time_tpl)
210
-
211
- real_offsets.to_h.each do |name, results|
212
- results.each do |result|
213
- raise(Errors::InvalidTimeBasedOffsetError) unless result
214
-
215
- partition = result.partition
216
-
217
- # Negative offset means we're beyond last message and we need to query for the
218
- # high watermark offset to get the most recent offset and move there
219
- if result.offset.negative?
220
- _, offset = consumer.query_watermark_offsets(name, result.partition)
221
- else
222
- # If we get an offset, it means there existed a message close to this time
223
- # location
224
- offset = result.offset
225
- end
226
-
227
- # Since now we have proper offsets, we can add this to the final tpl for commit
228
- tpl.to_h[name] ||= []
229
- tpl.to_h[name] << Rdkafka::Consumer::Partition.new(partition, offset)
230
- tpl.to_h[name].reverse!
231
- tpl.to_h[name].uniq!(&:partition)
232
- tpl.to_h[name].reverse!
190
+ settings = { 'group.id': consumer_group_id }
191
+
192
+ with_consumer(settings) do |consumer|
193
+ # If we have any time based stuff to resolve, we need to do it prior to commits
194
+ unless time_tpl.empty?
195
+ real_offsets = consumer.offsets_for_times(time_tpl)
196
+
197
+ real_offsets.to_h.each do |name, results|
198
+ results.each do |result|
199
+ raise(Errors::InvalidTimeBasedOffsetError) unless result
200
+
201
+ partition = result.partition
202
+
203
+ # Negative offset means we're beyond last message and we need to query for the
204
+ # high watermark offset to get the most recent offset and move there
205
+ if result.offset.negative?
206
+ _, offset = consumer.query_watermark_offsets(name, result.partition)
207
+ else
208
+ # If we get an offset, it means there existed a message close to this time
209
+ # location
210
+ offset = result.offset
233
211
  end
212
+
213
+ # Since now we have proper offsets, we can add this to the final tpl for commit
214
+ tpl.add_topic_and_partitions_with_offsets(name, [[partition, offset]])
234
215
  end
235
216
  end
236
-
237
- consumer.commit(tpl, false)
238
217
  end
218
+
219
+ consumer.commit_offsets(tpl, async: false)
239
220
  end
240
221
  end
241
222
 
242
223
  # Removes given consumer group (if exists)
243
224
  #
244
- # @param consumer_group_id [String] consumer group name without the mapper name (if any used)
245
- #
246
- # @note Please note, Karafka will apply the consumer group mapper on the provided consumer
247
- # group.
225
+ # @param consumer_group_id [String] consumer group name
248
226
  #
249
227
  # @note This method should not be used on a running consumer group as it will not yield any
250
228
  # results.
251
229
  def delete_consumer_group(consumer_group_id)
252
- mapped_consumer_group_id = app_config.consumer_mapper.call(consumer_group_id)
253
-
254
230
  with_admin do |admin|
255
- handler = admin.delete_group(mapped_consumer_group_id)
231
+ handler = admin.delete_group(consumer_group_id)
256
232
  handler.wait(max_wait_timeout: app_config.admin.max_wait_time)
257
233
  end
258
234
  end
@@ -264,13 +240,118 @@ module Karafka
264
240
  # @return [Array<Integer, Integer>] low watermark offset and high watermark offset
265
241
  def read_watermark_offsets(name, partition)
266
242
  with_consumer do |consumer|
267
- # For newly created topics or in cases where we're trying to get them but there is no
268
- # leader, this can fail. It happens more often for new topics under KRaft, however we
269
- # still want to make sure things operate as expected even then
270
- with_rdkafka_retry(codes: %i[not_leader_for_partition]) do
271
- consumer.query_watermark_offsets(name, partition)
243
+ consumer.query_watermark_offsets(name, partition)
244
+ end
245
+ end
246
+
247
+ # Reads lags and offsets for given topics in the context of consumer groups defined in the
248
+ # routing
249
+ # @param consumer_groups_with_topics [Hash<String, Array<String>>] hash with consumer groups
250
+ # names with array of topics to query per consumer group inside
251
+ # @param active_topics_only [Boolean] if set to false, when we use routing topics, will
252
+ # select also topics that are marked as inactive in routing
253
+ # @return [Hash<String, Hash<Integer, <Hash<Integer>>>>] hash where the top level keys are
254
+ # the consumer groups and values are hashes with topics and inside partitions with lags
255
+ # and offsets
256
+ #
257
+ # @note For topics that do not exist, topic details will be set to an empty hash
258
+ #
259
+ # @note For topics that exist but were never consumed by a given CG we set `-1` as lag and
260
+ # the offset on each of the partitions that were not consumed.
261
+ #
262
+ # @note This lag reporting is for committed lags and is "Kafka-centric", meaning that this
263
+ # represents lags from Kafka perspective and not the consumer. They may differ.
264
+ def read_lags_with_offsets(consumer_groups_with_topics = {}, active_topics_only: true)
265
+ # We first fetch all the topics with partitions count that exist in the cluster so we
266
+ # do not query for topics that do not exist and so we can get partitions count for all
267
+ # the topics we may need. The non-existent and not consumed will be filled at the end
268
+ existing_topics = cluster_info.topics.map do |topic|
269
+ [topic[:topic_name], topic[:partition_count]]
270
+ end.to_h.freeze
271
+
272
+ # If no expected CGs, we use all from routing that have active topics
273
+ if consumer_groups_with_topics.empty?
274
+ consumer_groups_with_topics = Karafka::App.routes.map do |cg|
275
+ cg_topics = cg.topics.select do |cg_topic|
276
+ active_topics_only ? cg_topic.active? : true
277
+ end
278
+
279
+ [cg.id, cg_topics.map(&:name)]
280
+ end.to_h
281
+ end
282
+
283
+ # We make a copy because we will remove once with non-existing topics
284
+ # We keep original requested consumer groups with topics for later backfilling
285
+ cgs_with_topics = consumer_groups_with_topics.dup
286
+ cgs_with_topics.transform_values!(&:dup)
287
+
288
+ # We can query only topics that do exist, this is why we are cleaning those that do not
289
+ # exist
290
+ cgs_with_topics.each_value do |requested_topics|
291
+ requested_topics.delete_if { |topic| !existing_topics.include?(topic) }
292
+ end
293
+
294
+ groups_lags = Hash.new { |h, k| h[k] = {} }
295
+ groups_offs = Hash.new { |h, k| h[k] = {} }
296
+
297
+ cgs_with_topics.each do |cg, topics|
298
+ # Do not add to tpl topics that do not exist
299
+ next if topics.empty?
300
+
301
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
302
+
303
+ with_consumer('group.id': cg) do |consumer|
304
+ topics.each { |topic| tpl.add_topic(topic, existing_topics[topic]) }
305
+
306
+ commit_offsets = consumer.committed(tpl)
307
+
308
+ commit_offsets.to_h.each do |topic, partitions|
309
+ groups_offs[cg][topic] = {}
310
+
311
+ partitions.each do |partition|
312
+ # -1 when no offset is stored
313
+ groups_offs[cg][topic][partition.partition] = partition.offset || -1
314
+ end
315
+ end
316
+
317
+ consumer.lag(commit_offsets).each do |topic, partitions_lags|
318
+ groups_lags[cg][topic] = partitions_lags
319
+ end
320
+ end
321
+ end
322
+
323
+ consumer_groups_with_topics.each do |cg, topics|
324
+ groups_lags[cg]
325
+
326
+ topics.each do |topic|
327
+ groups_lags[cg][topic] ||= {}
328
+
329
+ next unless existing_topics.key?(topic)
330
+
331
+ # We backfill because there is a case where our consumer group would consume for
332
+ # example only one partition out of 20, rest needs to get -1
333
+ existing_topics[topic].times do |partition_id|
334
+ groups_lags[cg][topic][partition_id] ||= -1
335
+ end
336
+ end
337
+ end
338
+
339
+ merged = Hash.new { |h, k| h[k] = {} }
340
+
341
+ groups_lags.each do |cg, topics|
342
+ topics.each do |topic, partitions|
343
+ merged[cg][topic] = {}
344
+
345
+ partitions.each do |partition, lag|
346
+ merged[cg][topic][partition] = {
347
+ offset: groups_offs.fetch(cg).fetch(topic).fetch(partition),
348
+ lag: lag
349
+ }
350
+ end
272
351
  end
273
352
  end
353
+
354
+ merged
274
355
  end
275
356
 
276
357
  # @return [Rdkafka::Metadata] cluster metadata info
@@ -278,6 +359,24 @@ module Karafka
278
359
  with_admin(&:metadata)
279
360
  end
280
361
 
362
+ # Returns basic topic metadata
363
+ #
364
+ # @param topic_name [String] name of the topic we're interested in
365
+ # @return [Hash] topic metadata info hash
366
+ # @raise [Rdkafka::RdkafkaError] `unknown_topic_or_part` if requested topic is not found
367
+ #
368
+ # @note This query is much more efficient than doing a full `#cluster_info` + topic lookup
369
+ # because it does not have to query for all the topics data but just the topic we're
370
+ # interested in
371
+ def topic_info(topic_name)
372
+ with_admin do |admin|
373
+ admin
374
+ .metadata(topic_name)
375
+ .topics
376
+ .find { |topic| topic[:topic_name] == topic_name }
377
+ end
378
+ end
379
+
281
380
  # Creates consumer instance and yields it. After usage it closes the consumer instance
282
381
  # This API can be used in other pieces of code and allows for low-level consumer usage
283
382
  #
@@ -286,7 +385,12 @@ module Karafka
286
385
  # @note We always ship and yield a proxied consumer because admin API performance is not
287
386
  # that relevant. That is, there are no high frequency calls that would have to be delegated
288
387
  def with_consumer(settings = {})
289
- consumer = config(:consumer, settings).consumer
388
+ bind_id = SecureRandom.uuid
389
+
390
+ consumer = config(:consumer, settings).consumer(native_kafka_auto_start: false)
391
+ bind_oauth(bind_id, consumer)
392
+
393
+ consumer.start
290
394
  proxy = ::Karafka::Connection::Proxy.new(consumer)
291
395
  yield(proxy)
292
396
  ensure
@@ -301,30 +405,56 @@ module Karafka
301
405
  end
302
406
 
303
407
  consumer&.close
408
+
409
+ unbind_oauth(bind_id)
304
410
  end
305
411
 
306
412
  # Creates admin instance and yields it. After usage it closes the admin instance
307
413
  def with_admin
308
- admin = config(:producer, {}).admin
309
- yield(admin)
414
+ bind_id = SecureRandom.uuid
415
+
416
+ admin = config(:producer, {}).admin(native_kafka_auto_start: false)
417
+ bind_oauth(bind_id, admin)
418
+
419
+ admin.start
420
+ proxy = ::Karafka::Connection::Proxy.new(admin)
421
+ yield(proxy)
310
422
  ensure
311
423
  admin&.close
424
+
425
+ unbind_oauth(bind_id)
312
426
  end
313
427
 
314
428
  private
315
429
 
430
+ # Adds a new callback for given rdkafka instance for oauth token refresh (if needed)
431
+ #
432
+ # @param id [String, Symbol] unique (for the lifetime of instance) id that we use for
433
+ # callback referencing
434
+ # @param instance [Rdkafka::Consumer, Rdkafka::Admin] rdkafka instance to be used to set
435
+ # appropriate oauth token when needed
436
+ def bind_oauth(id, instance)
437
+ ::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.add(
438
+ id,
439
+ Instrumentation::Callbacks::OauthbearerTokenRefresh.new(
440
+ instance
441
+ )
442
+ )
443
+ end
444
+
445
+ # Removes the callback from no longer used instance
446
+ #
447
+ # @param id [String, Symbol] unique (for the lifetime of instance) id that we use for
448
+ # callback referencing
449
+ def unbind_oauth(id)
450
+ ::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.delete(id)
451
+ end
452
+
316
453
  # @return [Array<String>] topics names
317
454
  def topics_names
318
455
  cluster_info.topics.map { |topic| topic.fetch(:topic_name) }
319
456
  end
320
457
 
321
- # Finds details about given topic
322
- # @param name [String] topic name
323
- # @return [Hash] topic details
324
- def topic(name)
325
- cluster_info.topics.find { |topic| topic[:topic_name] == name }
326
- end
327
-
328
458
  # There are some cases where rdkafka admin operations finish successfully but without the
329
459
  # callback being triggered to materialize the post-promise object. Until this is fixed we
330
460
  # can figure out, that operation we wanted to do finished successfully by checking that the
@@ -351,44 +481,15 @@ module Karafka
351
481
  raise
352
482
  end
353
483
 
354
- # Handles retries for rdkafka related errors that we specify in `:codes`.
355
- #
356
- # Some operations temporarily fail, especially for cases where we changed something fast
357
- # like topic creation or repartitioning. In cases like this it is ok to retry operations that
358
- # do not change the state as it will usually recover.
359
- #
360
- # @param codes [Array<Symbol>] librdkafka error codes on which we want to retry
361
- # @param max_attempts [Integer] number of attempts (including initial) after which we should
362
- # give up
363
- #
364
- # @note This code implements a simple backoff that increases with each attempt.
365
- def with_rdkafka_retry(codes:, max_attempts: 5)
366
- attempt ||= 0
367
- attempt += 1
368
-
369
- yield
370
- rescue Rdkafka::RdkafkaError => e
371
- raise unless codes.include?(e.code)
372
- raise if attempt >= max_attempts
373
-
374
- sleep(max_attempts)
375
-
376
- retry
377
- end
378
-
379
484
  # @param type [Symbol] type of config we want
380
485
  # @param settings [Hash] extra settings for config (if needed)
381
486
  # @return [::Rdkafka::Config] rdkafka config
382
487
  def config(type, settings)
383
- mapped_admin_group_id = app_config.consumer_mapper.call(
384
- app_config.admin.group_id
385
- )
386
-
387
488
  app_config
388
489
  .kafka
389
490
  .then(&:dup)
390
491
  .merge(app_config.admin.kafka)
391
- .tap { |config| config[:'group.id'] = mapped_admin_group_id }
492
+ .tap { |config| config[:'group.id'] = app_config.admin.group_id }
392
493
  # We merge after setting the group id so it can be altered if needed
393
494
  # In general in admin we only should alter it when we need to impersonate a given
394
495
  # 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