karafka 2.3.3 → 2.4.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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