karafka 2.3.3 → 2.4.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +12 -38
- data/CHANGELOG.md +59 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +29 -27
- 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 +211 -90
- 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/constraints.rb +3 -3
- 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,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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
settings
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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'] =
|
|
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:
|
|
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
|