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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +12 -38
- data/CHANGELOG.md +56 -2
- data/Gemfile +6 -3
- data/Gemfile.lock +25 -23
- data/bin/integrations +1 -1
- data/config/locales/errors.yml +21 -2
- data/config/locales/pro_errors.yml +16 -1
- data/karafka.gemspec +4 -2
- data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
- data/lib/karafka/admin/configs/config.rb +81 -0
- data/lib/karafka/admin/configs/resource.rb +88 -0
- data/lib/karafka/admin/configs.rb +103 -0
- data/lib/karafka/admin.rb +201 -100
- data/lib/karafka/base_consumer.rb +2 -2
- data/lib/karafka/cli/info.rb +9 -7
- data/lib/karafka/cli/server.rb +7 -7
- data/lib/karafka/cli/topics/align.rb +109 -0
- data/lib/karafka/cli/topics/base.rb +66 -0
- data/lib/karafka/cli/topics/create.rb +35 -0
- data/lib/karafka/cli/topics/delete.rb +30 -0
- data/lib/karafka/cli/topics/migrate.rb +31 -0
- data/lib/karafka/cli/topics/plan.rb +169 -0
- data/lib/karafka/cli/topics/repartition.rb +41 -0
- data/lib/karafka/cli/topics/reset.rb +18 -0
- data/lib/karafka/cli/topics.rb +13 -123
- data/lib/karafka/connection/client.rb +55 -37
- data/lib/karafka/connection/listener.rb +22 -17
- data/lib/karafka/connection/proxy.rb +93 -4
- data/lib/karafka/connection/status.rb +14 -2
- data/lib/karafka/contracts/config.rb +14 -1
- data/lib/karafka/contracts/topic.rb +1 -1
- data/lib/karafka/deserializers/headers.rb +15 -0
- data/lib/karafka/deserializers/key.rb +15 -0
- data/lib/karafka/deserializers/payload.rb +16 -0
- data/lib/karafka/embedded.rb +2 -0
- data/lib/karafka/helpers/async.rb +5 -2
- data/lib/karafka/helpers/colorize.rb +6 -0
- data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
- data/lib/karafka/instrumentation/logger_listener.rb +23 -3
- data/lib/karafka/instrumentation/notifications.rb +10 -0
- data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
- data/lib/karafka/messages/batch_metadata.rb +1 -1
- data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
- data/lib/karafka/messages/builders/message.rb +10 -6
- data/lib/karafka/messages/message.rb +2 -1
- data/lib/karafka/messages/metadata.rb +20 -4
- data/lib/karafka/messages/parser.rb +1 -1
- data/lib/karafka/pro/base_consumer.rb +12 -23
- data/lib/karafka/pro/encryption/cipher.rb +7 -3
- data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
- data/lib/karafka/pro/encryption/errors.rb +4 -1
- data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
- data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
- data/lib/karafka/pro/encryption/setup/config.rb +5 -0
- data/lib/karafka/pro/iterator/expander.rb +2 -1
- data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
- data/lib/karafka/pro/iterator.rb +28 -2
- data/lib/karafka/pro/loader.rb +3 -0
- data/lib/karafka/pro/processing/coordinator.rb +15 -2
- data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
- data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
- data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
- data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
- data/lib/karafka/pro/processing/strategies/default.rb +5 -1
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
- data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
- data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
- data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
- data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
- data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
- data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
- data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
- data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
- data/lib/karafka/pro/routing/features/swarm.rb +11 -0
- data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
- data/lib/karafka/processing/coordinator.rb +17 -8
- data/lib/karafka/processing/coordinators_buffer.rb +5 -2
- data/lib/karafka/processing/executor.rb +6 -2
- data/lib/karafka/processing/executors_buffer.rb +5 -2
- data/lib/karafka/processing/jobs_queue.rb +9 -4
- data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
- data/lib/karafka/processing/strategies/default.rb +7 -1
- data/lib/karafka/processing/strategies/dlq.rb +17 -2
- data/lib/karafka/processing/workers_batch.rb +4 -1
- data/lib/karafka/routing/builder.rb +6 -2
- data/lib/karafka/routing/consumer_group.rb +2 -1
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
- data/lib/karafka/routing/features/deserializers/config.rb +18 -0
- data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
- data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
- data/lib/karafka/routing/features/deserializers.rb +11 -0
- data/lib/karafka/routing/proxy.rb +9 -14
- data/lib/karafka/routing/router.rb +11 -2
- data/lib/karafka/routing/subscription_group.rb +9 -1
- data/lib/karafka/routing/topic.rb +0 -1
- data/lib/karafka/runner.rb +1 -1
- data/lib/karafka/setup/config.rb +50 -9
- data/lib/karafka/status.rb +7 -8
- data/lib/karafka/swarm/supervisor.rb +16 -2
- data/lib/karafka/templates/karafka.rb.erb +28 -1
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +38 -12
- metadata.gz.sig +0 -0
- data/lib/karafka/routing/consumer_mapper.rb +0 -23
- data/lib/karafka/serialization/json/deserializer.rb +0 -19
- 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
|
-
-> {
|
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
|
-
|
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
|
-
|
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
|
-
|
197
|
-
|
198
|
-
settings
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
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(
|
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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
-
|
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
|
-
|
309
|
-
|
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'] =
|
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:
|
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:
|
300
|
+
attempt: attempt
|
301
301
|
)
|
302
302
|
end
|
303
303
|
end
|
data/lib/karafka/cli/info.rb
CHANGED
@@ -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: #{
|
43
|
-
"Application 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: #{
|
58
|
+
"License entity: #{license.entity}"
|
57
59
|
]
|
58
60
|
else
|
59
61
|
[
|
data/lib/karafka/cli/server.rb
CHANGED
@@ -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|
|
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
|
-
|
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|
|
109
|
+
names.each { |name| activity_manager.exclude(type, name) }
|
110
110
|
end
|
111
111
|
end
|
112
112
|
end
|