karafka 2.3.2 → 2.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) 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 +65 -0
  5. data/Gemfile +6 -3
  6. data/Gemfile.lock +25 -23
  7. data/README.md +2 -2
  8. data/bin/integrations +1 -1
  9. data/config/locales/errors.yml +24 -2
  10. data/config/locales/pro_errors.yml +19 -0
  11. data/karafka.gemspec +4 -2
  12. data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
  13. data/lib/karafka/admin/configs/config.rb +81 -0
  14. data/lib/karafka/admin/configs/resource.rb +88 -0
  15. data/lib/karafka/admin/configs.rb +103 -0
  16. data/lib/karafka/admin.rb +200 -89
  17. data/lib/karafka/base_consumer.rb +2 -2
  18. data/lib/karafka/cli/info.rb +9 -7
  19. data/lib/karafka/cli/server.rb +7 -7
  20. data/lib/karafka/cli/topics/align.rb +109 -0
  21. data/lib/karafka/cli/topics/base.rb +66 -0
  22. data/lib/karafka/cli/topics/create.rb +35 -0
  23. data/lib/karafka/cli/topics/delete.rb +30 -0
  24. data/lib/karafka/cli/topics/migrate.rb +31 -0
  25. data/lib/karafka/cli/topics/plan.rb +169 -0
  26. data/lib/karafka/cli/topics/repartition.rb +41 -0
  27. data/lib/karafka/cli/topics/reset.rb +18 -0
  28. data/lib/karafka/cli/topics.rb +13 -123
  29. data/lib/karafka/connection/client.rb +62 -37
  30. data/lib/karafka/connection/listener.rb +22 -17
  31. data/lib/karafka/connection/proxy.rb +93 -4
  32. data/lib/karafka/connection/status.rb +14 -2
  33. data/lib/karafka/contracts/config.rb +36 -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/datadog/metrics_listener.rb +34 -4
  46. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
  47. data/lib/karafka/messages/batch_metadata.rb +1 -1
  48. data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
  49. data/lib/karafka/messages/builders/message.rb +10 -6
  50. data/lib/karafka/messages/message.rb +2 -1
  51. data/lib/karafka/messages/metadata.rb +20 -4
  52. data/lib/karafka/messages/parser.rb +1 -1
  53. data/lib/karafka/pro/base_consumer.rb +12 -23
  54. data/lib/karafka/pro/encryption/cipher.rb +7 -3
  55. data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
  56. data/lib/karafka/pro/encryption/errors.rb +4 -1
  57. data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
  58. data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
  59. data/lib/karafka/pro/encryption/setup/config.rb +5 -0
  60. data/lib/karafka/pro/iterator/expander.rb +2 -1
  61. data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
  62. data/lib/karafka/pro/iterator.rb +28 -2
  63. data/lib/karafka/pro/loader.rb +3 -0
  64. data/lib/karafka/pro/processing/coordinator.rb +15 -2
  65. data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
  66. data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
  67. data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
  68. data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
  69. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
  70. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
  71. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
  72. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
  73. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
  74. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
  75. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
  76. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
  77. data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
  78. data/lib/karafka/pro/processing/strategies/default.rb +5 -1
  79. data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
  80. data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
  81. data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
  82. data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
  83. data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
  84. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
  85. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
  86. data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
  87. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
  88. data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
  89. data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
  90. data/lib/karafka/pro/routing/features/swarm/config.rb +31 -0
  91. data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
  92. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +78 -0
  93. data/lib/karafka/pro/routing/features/swarm/topic.rb +77 -0
  94. data/lib/karafka/pro/routing/features/swarm.rb +36 -0
  95. data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
  96. data/lib/karafka/processing/coordinator.rb +17 -8
  97. data/lib/karafka/processing/coordinators_buffer.rb +5 -2
  98. data/lib/karafka/processing/executor.rb +6 -2
  99. data/lib/karafka/processing/executors_buffer.rb +5 -2
  100. data/lib/karafka/processing/jobs_queue.rb +9 -4
  101. data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
  102. data/lib/karafka/processing/strategies/default.rb +7 -1
  103. data/lib/karafka/processing/strategies/dlq.rb +17 -2
  104. data/lib/karafka/processing/workers_batch.rb +4 -1
  105. data/lib/karafka/routing/builder.rb +6 -2
  106. data/lib/karafka/routing/consumer_group.rb +2 -1
  107. data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
  108. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
  109. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
  110. data/lib/karafka/routing/features/deserializers/config.rb +18 -0
  111. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
  112. data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
  113. data/lib/karafka/routing/features/deserializers.rb +11 -0
  114. data/lib/karafka/routing/proxy.rb +9 -14
  115. data/lib/karafka/routing/router.rb +11 -2
  116. data/lib/karafka/routing/subscription_group.rb +22 -1
  117. data/lib/karafka/routing/topic.rb +0 -1
  118. data/lib/karafka/runner.rb +1 -1
  119. data/lib/karafka/setup/config.rb +51 -10
  120. data/lib/karafka/status.rb +7 -8
  121. data/lib/karafka/swarm/manager.rb +15 -3
  122. data/lib/karafka/swarm/node.rb +3 -3
  123. data/lib/karafka/swarm/pidfd.rb +20 -4
  124. data/lib/karafka/swarm/supervisor.rb +25 -8
  125. data/lib/karafka/templates/karafka.rb.erb +28 -1
  126. data/lib/karafka/version.rb +1 -1
  127. data.tar.gz.sig +0 -0
  128. metadata +42 -12
  129. metadata.gz.sig +0 -0
  130. data/lib/karafka/routing/consumer_mapper.rb +0 -23
  131. data/lib/karafka/serialization/json/deserializer.rb +0 -19
  132. data/lib/karafka/time_trackers/partition_usage.rb +0 -56
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Admin
5
+ module Configs
6
+ # Represents a single resource in the context of configuration management
7
+ class Resource
8
+ # Types of resources that have workable configs.
9
+ RESOURCE_TYPES_MAP = {
10
+ # use when you want to assign acl to a given topic
11
+ topic: Rdkafka::Bindings::RD_KAFKA_RESOURCE_TOPIC,
12
+ # use when you want to assign acl to a given broker
13
+ broker: Rdkafka::Bindings::RD_KAFKA_RESOURCE_BROKER
14
+ }.freeze
15
+
16
+ # Map for operations we may perform on the resource configs
17
+ OPERATIONS_TYPES_MAP = {
18
+ set: Rdkafka::Bindings::RD_KAFKA_ALTER_CONFIG_OP_TYPE_SET,
19
+ delete: Rdkafka::Bindings::RD_KAFKA_ALTER_CONFIG_OP_TYPE_DELETE,
20
+ append: Rdkafka::Bindings::RD_KAFKA_ALTER_CONFIG_OP_TYPE_APPEND,
21
+ subtract: Rdkafka::Bindings::RD_KAFKA_ALTER_CONFIG_OP_TYPE_SUBTRACT
22
+ }.freeze
23
+
24
+ private_constant :RESOURCE_TYPES_MAP, :OPERATIONS_TYPES_MAP
25
+
26
+ attr_reader :type, :name, :configs
27
+
28
+ # @param type [Symbol, Integer] type of resource as a symbol for mapping or integer
29
+ # @param name [String] name of the resource. It's the broker id or topic name
30
+ # @return [Resource]
31
+ def initialize(type:, name:)
32
+ @type = map_type(type)
33
+ @name = name.to_s
34
+ @configs = []
35
+ @operations = Hash.new { |h, k| h[k] = [] }
36
+
37
+ freeze
38
+ end
39
+
40
+ OPERATIONS_TYPES_MAP.each do |op_name, op_value|
41
+ # Adds an outgoing operation to a given resource of a given type
42
+ # Useful since we alter in batches and not one at a time
43
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
44
+ # @param name [String] name of the config to alter
45
+ # @param value [String] value of the config
46
+ def #{op_name}(name, value #{op_name == :delete ? ' = nil' : ''})
47
+ @operations[#{op_value}] << Config.new(name: name, value: value.to_s)
48
+ end
49
+ RUBY
50
+ end
51
+
52
+ # @return [Hash] resource converted to a hash that rdkafka can work with
53
+ # @note Configs include the operation type and are expected to be used only for the
54
+ # incremental alter API.
55
+ def to_native_hash
56
+ configs_with_operations = []
57
+
58
+ @operations.each do |op_type, configs|
59
+ configs.each do |config|
60
+ configs_with_operations << config.to_native_hash.merge(op_type: op_type)
61
+ end
62
+ end
63
+
64
+ {
65
+ resource_type: RESOURCE_TYPES_MAP.fetch(type),
66
+ resource_name: name,
67
+ configs: configs_with_operations
68
+ }.freeze
69
+ end
70
+
71
+ private
72
+
73
+ # Recognizes whether the type is provided and remaps it to a symbol representation if
74
+ # needed
75
+ #
76
+ # @param type [Symbol, Integer]
77
+ # @return [Symbol]
78
+ def map_type(type)
79
+ inverted = RESOURCE_TYPES_MAP.invert
80
+
81
+ return inverted[type] if inverted.key?(type)
82
+
83
+ RESOURCE_TYPES_MAP.fetch(type) ? type : nil
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -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
@@ -187,62 +187,48 @@ module Karafka
187
187
  end
188
188
  end
189
189
 
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]])
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
223
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]])
224
215
  end
225
216
  end
226
-
227
- consumer.commit(tpl, false)
228
217
  end
218
+
219
+ consumer.commit_offsets(tpl, async: false)
229
220
  end
230
221
  end
231
222
 
232
223
  # Removes given consumer group (if exists)
233
224
  #
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.
225
+ # @param consumer_group_id [String] consumer group name
238
226
  #
239
227
  # @note This method should not be used on a running consumer group as it will not yield any
240
228
  # results.
241
229
  def delete_consumer_group(consumer_group_id)
242
- mapped_consumer_group_id = app_config.consumer_mapper.call(consumer_group_id)
243
-
244
230
  with_admin do |admin|
245
- handler = admin.delete_group(mapped_consumer_group_id)
231
+ handler = admin.delete_group(consumer_group_id)
246
232
  handler.wait(max_wait_timeout: app_config.admin.max_wait_time)
247
233
  end
248
234
  end
@@ -254,13 +240,118 @@ module Karafka
254
240
  # @return [Array<Integer, Integer>] low watermark offset and high watermark offset
255
241
  def read_watermark_offsets(name, partition)
256
242
  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)
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
262
351
  end
263
352
  end
353
+
354
+ merged
264
355
  end
265
356
 
266
357
  # @return [Rdkafka::Metadata] cluster metadata info
@@ -268,6 +359,24 @@ module Karafka
268
359
  with_admin(&:metadata)
269
360
  end
270
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
+
271
380
  # Creates consumer instance and yields it. After usage it closes the consumer instance
272
381
  # This API can be used in other pieces of code and allows for low-level consumer usage
273
382
  #
@@ -276,7 +385,12 @@ module Karafka
276
385
  # @note We always ship and yield a proxied consumer because admin API performance is not
277
386
  # that relevant. That is, there are no high frequency calls that would have to be delegated
278
387
  def with_consumer(settings = {})
279
- 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
280
394
  proxy = ::Karafka::Connection::Proxy.new(consumer)
281
395
  yield(proxy)
282
396
  ensure
@@ -291,30 +405,56 @@ module Karafka
291
405
  end
292
406
 
293
407
  consumer&.close
408
+
409
+ unbind_oauth(bind_id)
294
410
  end
295
411
 
296
412
  # Creates admin instance and yields it. After usage it closes the admin instance
297
413
  def with_admin
298
- admin = config(:producer, {}).admin
299
- 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)
300
422
  ensure
301
423
  admin&.close
424
+
425
+ unbind_oauth(bind_id)
302
426
  end
303
427
 
304
428
  private
305
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
+
306
453
  # @return [Array<String>] topics names
307
454
  def topics_names
308
455
  cluster_info.topics.map { |topic| topic.fetch(:topic_name) }
309
456
  end
310
457
 
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
458
  # There are some cases where rdkafka admin operations finish successfully but without the
319
459
  # callback being triggered to materialize the post-promise object. Until this is fixed we
320
460
  # can figure out, that operation we wanted to do finished successfully by checking that the
@@ -341,44 +481,15 @@ module Karafka
341
481
  raise
342
482
  end
343
483
 
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
484
  # @param type [Symbol] type of config we want
370
485
  # @param settings [Hash] extra settings for config (if needed)
371
486
  # @return [::Rdkafka::Config] rdkafka config
372
487
  def config(type, settings)
373
- mapped_admin_group_id = app_config.consumer_mapper.call(
374
- app_config.admin.group_id
375
- )
376
-
377
488
  app_config
378
489
  .kafka
379
490
  .then(&:dup)
380
491
  .merge(app_config.admin.kafka)
381
- .tap { |config| config[:'group.id'] = mapped_admin_group_id }
492
+ .tap { |config| config[:'group.id'] = app_config.admin.group_id }
382
493
  # We merge after setting the group id so it can be altered if needed
383
494
  # In general in admin we only should alter it when we need to impersonate a given
384
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