karafka 2.5.0.rc2 → 2.5.1.beta1
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
- data/.github/workflows/{ci.yml → ci_linux_ubuntu_x86_64_gnu.yml} +54 -30
- data/.github/workflows/ci_macos_arm64.yml +148 -0
- data/.github/workflows/push.yml +2 -2
- data/.github/workflows/trigger-wiki-refresh.yml +30 -0
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +29 -2
- data/Gemfile +2 -1
- data/Gemfile.lock +56 -27
- data/README.md +2 -2
- data/bin/integrations +3 -1
- data/bin/verify_kafka_warnings +2 -1
- data/config/locales/errors.yml +153 -152
- data/config/locales/pro_errors.yml +135 -134
- data/karafka.gemspec +3 -3
- data/lib/active_job/queue_adapters/karafka_adapter.rb +30 -1
- data/lib/karafka/active_job/dispatcher.rb +19 -9
- data/lib/karafka/admin/acl.rb +7 -8
- data/lib/karafka/admin/configs/config.rb +2 -2
- data/lib/karafka/admin/configs/resource.rb +2 -2
- data/lib/karafka/admin/configs.rb +3 -7
- data/lib/karafka/admin/consumer_groups.rb +351 -0
- data/lib/karafka/admin/topics.rb +206 -0
- data/lib/karafka/admin.rb +42 -451
- data/lib/karafka/base_consumer.rb +22 -0
- data/lib/karafka/{pro/contracts/server_cli_options.rb → cli/contracts/server.rb} +4 -12
- data/lib/karafka/cli/info.rb +1 -1
- data/lib/karafka/cli/install.rb +0 -2
- data/lib/karafka/connection/client.rb +8 -0
- data/lib/karafka/connection/listener.rb +5 -1
- data/lib/karafka/connection/status.rb +12 -9
- data/lib/karafka/errors.rb +0 -8
- data/lib/karafka/instrumentation/assignments_tracker.rb +16 -0
- data/lib/karafka/instrumentation/logger_listener.rb +109 -50
- data/lib/karafka/pro/active_job/dispatcher.rb +5 -0
- data/lib/karafka/pro/cleaner/messages/messages.rb +18 -8
- data/lib/karafka/pro/cli/contracts/server.rb +106 -0
- data/lib/karafka/pro/encryption/contracts/config.rb +1 -1
- data/lib/karafka/pro/loader.rb +1 -1
- data/lib/karafka/pro/recurring_tasks/contracts/config.rb +1 -1
- data/lib/karafka/pro/routing/features/adaptive_iterator/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/adaptive_iterator/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/delaying/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/delaying/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +1 -1
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/expiring/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/expiring/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/filtering/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/filtering/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/inline_insights/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/inline_insights/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/long_running_job/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/long_running_job/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/multiplexing.rb +1 -1
- data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +1 -1
- data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +1 -1
- data/lib/karafka/pro/routing/features/patterns/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/patterns/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/pausing/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/periodic_job/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/recurring_tasks/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/scheduled_messages/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/swarm/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/throttling/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/throttling/topic.rb +9 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -1
- data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +9 -0
- data/lib/karafka/pro/scheduled_messages/contracts/config.rb +1 -1
- data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +9 -3
- data/lib/karafka/pro/swarm/liveness_listener.rb +17 -2
- data/lib/karafka/processing/executor.rb +1 -1
- data/lib/karafka/routing/builder.rb +0 -3
- data/lib/karafka/routing/consumer_group.rb +1 -4
- data/lib/karafka/routing/contracts/consumer_group.rb +84 -0
- data/lib/karafka/routing/contracts/routing.rb +61 -0
- data/lib/karafka/routing/contracts/topic.rb +83 -0
- data/lib/karafka/routing/features/active_job/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/active_job/topic.rb +9 -0
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -0
- data/lib/karafka/routing/features/declaratives/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/declaratives/topic.rb +9 -0
- data/lib/karafka/routing/features/deserializers/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/deserializers/topic.rb +9 -0
- data/lib/karafka/routing/features/eofed/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/eofed/topic.rb +9 -0
- data/lib/karafka/routing/features/inline_insights/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/inline_insights/topic.rb +9 -0
- data/lib/karafka/routing/features/manual_offset_management/contracts/topic.rb +1 -1
- data/lib/karafka/routing/features/manual_offset_management/topic.rb +9 -0
- data/lib/karafka/routing/subscription_group.rb +1 -10
- data/lib/karafka/routing/topic.rb +9 -1
- data/lib/karafka/server.rb +2 -7
- data/lib/karafka/setup/attributes_map.rb +36 -0
- data/lib/karafka/setup/config.rb +6 -7
- data/lib/karafka/setup/contracts/config.rb +217 -0
- data/lib/karafka/setup/defaults_injector.rb +3 -1
- data/lib/karafka/swarm/node.rb +66 -6
- data/lib/karafka/swarm.rb +2 -2
- data/lib/karafka/templates/karafka.rb.erb +2 -7
- data/lib/karafka/version.rb +1 -1
- data/lib/karafka.rb +17 -18
- metadata +18 -15
- data/lib/karafka/contracts/config.rb +0 -210
- data/lib/karafka/contracts/consumer_group.rb +0 -81
- data/lib/karafka/contracts/routing.rb +0 -59
- data/lib/karafka/contracts/server_cli_options.rb +0 -92
- data/lib/karafka/contracts/topic.rb +0 -81
- data/lib/karafka/swarm/pidfd.rb +0 -147
data/lib/karafka/admin.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'admin/consumer_groups'
|
4
|
+
|
3
5
|
module Karafka
|
4
6
|
# Admin actions that we can perform via Karafka on our Kafka cluster
|
5
7
|
#
|
@@ -9,7 +11,7 @@ module Karafka
|
|
9
11
|
# @note It always uses the primary defined cluster and does not support multi-cluster work.
|
10
12
|
# Cluster on which operations are performed can be changed via `admin.kafka` config, however
|
11
13
|
# there is no multi-cluster runtime support.
|
12
|
-
|
14
|
+
class Admin
|
13
15
|
extend Core::Helpers::Time
|
14
16
|
|
15
17
|
extend Helpers::ConfigImporter.new(
|
@@ -22,273 +24,60 @@ module Karafka
|
|
22
24
|
admin_kafka: %i[admin kafka]
|
23
25
|
)
|
24
26
|
|
25
|
-
# 2010-01-01 00:00:00 - way before Kafka was released so no messages should exist prior to
|
26
|
-
# this date
|
27
|
-
# We do not use the explicit -2 librdkafka value here because we resolve this offset without
|
28
|
-
# consuming data
|
29
|
-
LONG_TIME_AGO = Time.at(1_262_300_400)
|
30
|
-
|
31
|
-
# one day in seconds for future time reference
|
32
|
-
DAY_IN_SECONDS = 60 * 60 * 24
|
33
|
-
|
34
|
-
private_constant :LONG_TIME_AGO, :DAY_IN_SECONDS
|
35
|
-
|
36
27
|
class << self
|
37
|
-
#
|
38
|
-
|
28
|
+
# Delegate topic-related operations to Topics class
|
29
|
+
|
39
30
|
# @param name [String, Symbol] topic name
|
40
31
|
# @param partition [Integer] partition
|
41
32
|
# @param count [Integer] how many messages we want to get at most
|
42
|
-
# @param start_offset [Integer, Time] offset from which we should start
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# @param settings [Hash] kafka extra settings (optional)
|
46
|
-
#
|
47
|
-
# @return [Array<Karafka::Messages::Message>] array with messages
|
33
|
+
# @param start_offset [Integer, Time] offset from which we should start
|
34
|
+
# @param settings [Hash] kafka extra settings
|
35
|
+
# @see Topics.read
|
48
36
|
def read_topic(name, partition, count, start_offset = -1, settings = {})
|
49
|
-
|
50
|
-
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
51
|
-
low_offset, high_offset = nil
|
52
|
-
|
53
|
-
with_consumer(settings) do |consumer|
|
54
|
-
# Convert the time offset (if needed)
|
55
|
-
start_offset = resolve_offset(consumer, name.to_s, partition, start_offset)
|
56
|
-
|
57
|
-
low_offset, high_offset = consumer.query_watermark_offsets(name, partition)
|
58
|
-
|
59
|
-
# Select offset dynamically if -1 or less and move backwards with the negative
|
60
|
-
# offset, allowing to start from N messages back from high-watermark
|
61
|
-
start_offset = high_offset - count - start_offset.abs + 1 if start_offset.negative?
|
62
|
-
start_offset = low_offset if start_offset.negative?
|
63
|
-
|
64
|
-
# Build the requested range - since first element is on the start offset we need to
|
65
|
-
# subtract one from requested count to end up with expected number of elements
|
66
|
-
requested_range = (start_offset..start_offset + (count - 1))
|
67
|
-
# Establish theoretical available range. Note, that this does not handle cases related to
|
68
|
-
# log retention or compaction
|
69
|
-
available_range = (low_offset..(high_offset - 1))
|
70
|
-
# Select only offset that we can select. This will remove all the potential offsets that
|
71
|
-
# are below the low watermark offset
|
72
|
-
possible_range = requested_range.select { |offset| available_range.include?(offset) }
|
73
|
-
|
74
|
-
start_offset = possible_range.first
|
75
|
-
count = possible_range.size
|
76
|
-
|
77
|
-
tpl.add_topic_and_partitions_with_offsets(name, partition => start_offset)
|
78
|
-
consumer.assign(tpl)
|
79
|
-
|
80
|
-
# We should poll as long as we don't have all the messages that we need or as long as
|
81
|
-
# we do not read all the messages from the topic
|
82
|
-
loop do
|
83
|
-
# If we've got as many messages as we've wanted stop
|
84
|
-
break if messages.size >= count
|
85
|
-
|
86
|
-
message = consumer.poll(200)
|
87
|
-
|
88
|
-
next unless message
|
89
|
-
|
90
|
-
# If the message we've got is beyond the requested range, stop
|
91
|
-
break unless possible_range.include?(message.offset)
|
92
|
-
|
93
|
-
messages << message
|
94
|
-
rescue Rdkafka::RdkafkaError => e
|
95
|
-
# End of partition
|
96
|
-
break if e.code == :partition_eof
|
97
|
-
|
98
|
-
raise e
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
# Use topic from routes if we can match it or create a dummy one
|
103
|
-
# Dummy one is used in case we cannot match the topic with routes. This can happen
|
104
|
-
# when admin API is used to read topics that are not part of the routing
|
105
|
-
topic = ::Karafka::Routing::Router.find_or_initialize_by_name(name)
|
106
|
-
|
107
|
-
messages.map! do |message|
|
108
|
-
Messages::Builders::Message.call(
|
109
|
-
message,
|
110
|
-
topic,
|
111
|
-
Time.now
|
112
|
-
)
|
113
|
-
end
|
37
|
+
Topics.read(name, partition, count, start_offset, settings)
|
114
38
|
end
|
115
39
|
|
116
|
-
# Creates Kafka topic with given settings
|
117
|
-
#
|
118
40
|
# @param name [String] topic name
|
119
41
|
# @param partitions [Integer] number of partitions we expect
|
120
42
|
# @param replication_factor [Integer] number of replicas
|
121
|
-
# @param topic_config [Hash] topic config details
|
122
|
-
#
|
43
|
+
# @param topic_config [Hash] topic config details
|
44
|
+
# @see Topics.create
|
123
45
|
def create_topic(name, partitions, replication_factor, topic_config = {})
|
124
|
-
|
125
|
-
handler = admin.create_topic(name, partitions, replication_factor, topic_config)
|
126
|
-
|
127
|
-
with_re_wait(
|
128
|
-
-> { handler.wait(max_wait_timeout: max_wait_time_seconds) },
|
129
|
-
-> { topics_names.include?(name) }
|
130
|
-
)
|
131
|
-
end
|
46
|
+
Topics.create(name, partitions, replication_factor, topic_config)
|
132
47
|
end
|
133
48
|
|
134
|
-
# Deleted a given topic
|
135
|
-
#
|
136
49
|
# @param name [String] topic name
|
50
|
+
# @see Topics.delete
|
137
51
|
def delete_topic(name)
|
138
|
-
|
139
|
-
handler = admin.delete_topic(name)
|
140
|
-
|
141
|
-
with_re_wait(
|
142
|
-
-> { handler.wait(max_wait_timeout: max_wait_time_seconds) },
|
143
|
-
-> { !topics_names.include?(name) }
|
144
|
-
)
|
145
|
-
end
|
52
|
+
Topics.delete(name)
|
146
53
|
end
|
147
54
|
|
148
|
-
# Creates more partitions for a given topic
|
149
|
-
#
|
150
55
|
# @param name [String] topic name
|
151
56
|
# @param partitions [Integer] total number of partitions we expect to end up with
|
57
|
+
# @see Topics.create_partitions
|
152
58
|
def create_partitions(name, partitions)
|
153
|
-
|
154
|
-
|
59
|
+
Topics.create_partitions(name, partitions)
|
60
|
+
end
|
155
61
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
62
|
+
# @param name [String, Symbol] topic name
|
63
|
+
# @param partition [Integer] partition
|
64
|
+
# @see Topics.read_watermark_offsets
|
65
|
+
def read_watermark_offsets(name, partition)
|
66
|
+
Topics.read_watermark_offsets(name, partition)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param topic_name [String] name of the topic we're interested in
|
70
|
+
# @see Topics.info
|
71
|
+
def topic_info(topic_name)
|
72
|
+
Topics.info(topic_name)
|
161
73
|
end
|
162
74
|
|
163
|
-
# Moves the offset on a given consumer group and provided topic to the requested location
|
164
|
-
#
|
165
75
|
# @param consumer_group_id [String] id of the consumer group for which we want to move the
|
166
76
|
# existing offset
|
167
|
-
# @param topics_with_partitions_and_offsets [Hash] Hash with list of topics and settings
|
168
|
-
#
|
169
|
-
# if we want to reset all partitions to for example a point in time.
|
170
|
-
#
|
171
|
-
# @note This method should **not** be executed on a running consumer group as it creates a
|
172
|
-
# "fake" consumer and uses it to move offsets.
|
173
|
-
#
|
174
|
-
# @example Move a single topic partition nr 1 offset to 100
|
175
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => { 1 => 100 } })
|
176
|
-
#
|
177
|
-
# @example Move offsets on all partitions of a topic to 100
|
178
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => 100 })
|
179
|
-
#
|
180
|
-
# @example Move offset to 5 seconds ago on partition 2
|
181
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => { 2 => 5.seconds.ago } })
|
182
|
-
#
|
183
|
-
# @example Move to the earliest offset on all the partitions of a topic
|
184
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => 'earliest' })
|
185
|
-
#
|
186
|
-
# @example Move to the latest (high-watermark) offset on all the partitions of a topic
|
187
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => 'latest' })
|
188
|
-
#
|
189
|
-
# @example Move offset of a single partition to earliest
|
190
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => { 1 => 'earliest' } })
|
191
|
-
#
|
192
|
-
# @example Move offset of a single partition to latest
|
193
|
-
# Karafka::Admin.seek_consumer_group('group-id', { 'topic' => { 1 => 'latest' } })
|
77
|
+
# @param topics_with_partitions_and_offsets [Hash] Hash with list of topics and settings
|
78
|
+
# @see ConsumerGroups.seek
|
194
79
|
def seek_consumer_group(consumer_group_id, topics_with_partitions_and_offsets)
|
195
|
-
|
196
|
-
|
197
|
-
# Normalize the data so we always have all partitions and topics in the same format
|
198
|
-
# That is in a format where we have topics and all partitions with their per partition
|
199
|
-
# assigned offsets
|
200
|
-
topics_with_partitions_and_offsets.each do |topic, partitions_with_offsets|
|
201
|
-
tpl_base[topic] = {}
|
202
|
-
|
203
|
-
if partitions_with_offsets.is_a?(Hash)
|
204
|
-
tpl_base[topic] = partitions_with_offsets
|
205
|
-
else
|
206
|
-
topic_info(topic)[:partition_count].times do |partition|
|
207
|
-
tpl_base[topic][partition] = partitions_with_offsets
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
tpl_base.each_value do |partitions|
|
213
|
-
partitions.transform_values! do |position|
|
214
|
-
# Support both symbol and string based references
|
215
|
-
casted_position = position.is_a?(Symbol) ? position.to_s : position
|
216
|
-
|
217
|
-
# This remap allows us to transform some special cases in a reference that can be
|
218
|
-
# understood by Kafka
|
219
|
-
case casted_position
|
220
|
-
# Earliest is not always 0. When compacting/deleting it can be much later, that's why
|
221
|
-
# we fetch the oldest possible offset
|
222
|
-
when 'earliest'
|
223
|
-
LONG_TIME_AGO
|
224
|
-
# Latest will always be the high-watermark offset and we can get it just by getting
|
225
|
-
# a future position
|
226
|
-
when 'latest'
|
227
|
-
Time.now + DAY_IN_SECONDS
|
228
|
-
# Same as `'earliest'`
|
229
|
-
when false
|
230
|
-
LONG_TIME_AGO
|
231
|
-
# Regular offset case
|
232
|
-
else
|
233
|
-
position
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
239
|
-
# In case of time based location, we need to to a pre-resolution, that's why we keep it
|
240
|
-
# separately
|
241
|
-
time_tpl = Rdkafka::Consumer::TopicPartitionList.new
|
242
|
-
|
243
|
-
# Distribute properly the offset type
|
244
|
-
tpl_base.each do |topic, partitions_with_offsets|
|
245
|
-
partitions_with_offsets.each do |partition, offset|
|
246
|
-
target = offset.is_a?(Time) ? time_tpl : tpl
|
247
|
-
# We reverse and uniq to make sure that potentially duplicated references are removed
|
248
|
-
# in such a way that the newest stays
|
249
|
-
target.to_h[topic] ||= []
|
250
|
-
target.to_h[topic] << Rdkafka::Consumer::Partition.new(partition, offset)
|
251
|
-
target.to_h[topic].reverse!
|
252
|
-
target.to_h[topic].uniq!(&:partition)
|
253
|
-
target.to_h[topic].reverse!
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
settings = { 'group.id': consumer_group_id }
|
258
|
-
|
259
|
-
with_consumer(settings) do |consumer|
|
260
|
-
# If we have any time based stuff to resolve, we need to do it prior to commits
|
261
|
-
unless time_tpl.empty?
|
262
|
-
real_offsets = consumer.offsets_for_times(time_tpl)
|
263
|
-
|
264
|
-
real_offsets.to_h.each do |name, results|
|
265
|
-
results.each do |result|
|
266
|
-
raise(Errors::InvalidTimeBasedOffsetError) unless result
|
267
|
-
|
268
|
-
partition = result.partition
|
269
|
-
|
270
|
-
# Negative offset means we're beyond last message and we need to query for the
|
271
|
-
# high watermark offset to get the most recent offset and move there
|
272
|
-
if result.offset.negative?
|
273
|
-
_, offset = consumer.query_watermark_offsets(name, result.partition)
|
274
|
-
else
|
275
|
-
# If we get an offset, it means there existed a message close to this time
|
276
|
-
# location
|
277
|
-
offset = result.offset
|
278
|
-
end
|
279
|
-
|
280
|
-
# Since now we have proper offsets, we can add this to the final tpl for commit
|
281
|
-
tpl.to_h[name] ||= []
|
282
|
-
tpl.to_h[name] << Rdkafka::Consumer::Partition.new(partition, offset)
|
283
|
-
tpl.to_h[name].reverse!
|
284
|
-
tpl.to_h[name].uniq!(&:partition)
|
285
|
-
tpl.to_h[name].reverse!
|
286
|
-
end
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
consumer.commit_offsets(tpl, async: false)
|
291
|
-
end
|
80
|
+
ConsumerGroups.seek(consumer_group_id, topics_with_partitions_and_offsets)
|
292
81
|
end
|
293
82
|
|
294
83
|
# Takes consumer group and its topics and copies all the offsets to a new named group
|
@@ -297,35 +86,9 @@ module Karafka
|
|
297
86
|
# @param new_name [String] new consumer group name
|
298
87
|
# @param topics [Array<String>] topics for which we want to migrate offsets during rename
|
299
88
|
# @return [Boolean] true if anything was migrated, otherwise false
|
300
|
-
#
|
301
|
-
# @note This method should **not** be executed on a running consumer group as it creates a
|
302
|
-
# "fake" consumer and uses it to move offsets.
|
303
|
-
#
|
304
|
-
# @note If new consumer group exists, old offsets will be added to it.
|
89
|
+
# @see ConsumerGroups.copy
|
305
90
|
def copy_consumer_group(previous_name, new_name, topics)
|
306
|
-
|
307
|
-
|
308
|
-
old_lags = read_lags_with_offsets({ previous_name => topics })
|
309
|
-
|
310
|
-
return false if old_lags.empty?
|
311
|
-
return false if old_lags.values.all? { |topic_data| topic_data.values.all?(&:empty?) }
|
312
|
-
|
313
|
-
read_lags_with_offsets({ previous_name => topics })
|
314
|
-
.fetch(previous_name)
|
315
|
-
.each do |topic, partitions|
|
316
|
-
partitions.each do |partition_id, details|
|
317
|
-
offset = details[:offset]
|
318
|
-
|
319
|
-
# No offset on this partition
|
320
|
-
next if offset.negative?
|
321
|
-
|
322
|
-
remap[topic][partition_id] = offset
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
seek_consumer_group(new_name, remap)
|
327
|
-
|
328
|
-
true
|
91
|
+
ConsumerGroups.copy(previous_name, new_name, topics)
|
329
92
|
end
|
330
93
|
|
331
94
|
# Takes consumer group and its topics and migrates all the offsets to a new named group
|
@@ -337,47 +100,17 @@ module Karafka
|
|
337
100
|
# Defaults to true.
|
338
101
|
# @return [Boolean] true if rename (and optionally removal) was ok or false if there was
|
339
102
|
# nothing really to rename
|
340
|
-
#
|
341
|
-
# @note This method should **not** be executed on a running consumer group as it creates a
|
342
|
-
# "fake" consumer and uses it to move offsets.
|
343
|
-
#
|
344
|
-
# @note After migration unless `delete_previous` is set to `false`, old group will be
|
345
|
-
# removed.
|
346
|
-
#
|
347
|
-
# @note If new consumer group exists, old offsets will be added to it.
|
103
|
+
# @see ConsumerGroups.rename
|
348
104
|
def rename_consumer_group(previous_name, new_name, topics, delete_previous: true)
|
349
|
-
|
350
|
-
|
351
|
-
return false unless copy_result
|
352
|
-
return copy_result unless delete_previous
|
353
|
-
|
354
|
-
delete_consumer_group(previous_name)
|
355
|
-
|
356
|
-
true
|
105
|
+
ConsumerGroups.rename(previous_name, new_name, topics, delete_previous: delete_previous)
|
357
106
|
end
|
358
107
|
|
359
108
|
# Removes given consumer group (if exists)
|
360
109
|
#
|
361
110
|
# @param consumer_group_id [String] consumer group name
|
362
|
-
#
|
363
|
-
# @note This method should not be used on a running consumer group as it will not yield any
|
364
|
-
# results.
|
111
|
+
# @see ConsumerGroups.delete
|
365
112
|
def delete_consumer_group(consumer_group_id)
|
366
|
-
|
367
|
-
handler = admin.delete_group(consumer_group_id)
|
368
|
-
handler.wait(max_wait_timeout: max_wait_time_seconds)
|
369
|
-
end
|
370
|
-
end
|
371
|
-
|
372
|
-
# Fetches the watermark offsets for a given topic partition
|
373
|
-
#
|
374
|
-
# @param name [String, Symbol] topic name
|
375
|
-
# @param partition [Integer] partition
|
376
|
-
# @return [Array<Integer, Integer>] low watermark offset and high watermark offset
|
377
|
-
def read_watermark_offsets(name, partition)
|
378
|
-
with_consumer do |consumer|
|
379
|
-
consumer.query_watermark_offsets(name, partition)
|
380
|
-
end
|
113
|
+
ConsumerGroups.delete(consumer_group_id)
|
381
114
|
end
|
382
115
|
|
383
116
|
# Reads lags and offsets for given topics in the context of consumer groups defined in the
|
@@ -389,105 +122,12 @@ module Karafka
|
|
389
122
|
# @return [Hash<String, Hash<Integer, <Hash<Integer>>>>] hash where the top level keys are
|
390
123
|
# the consumer groups and values are hashes with topics and inside partitions with lags
|
391
124
|
# and offsets
|
392
|
-
#
|
393
|
-
# @note For topics that do not exist, topic details will be set to an empty hash
|
394
|
-
#
|
395
|
-
# @note For topics that exist but were never consumed by a given CG we set `-1` as lag and
|
396
|
-
# the offset on each of the partitions that were not consumed.
|
397
|
-
#
|
398
|
-
# @note This lag reporting is for committed lags and is "Kafka-centric", meaning that this
|
399
|
-
# represents lags from Kafka perspective and not the consumer. They may differ.
|
125
|
+
# @see ConsumerGroups.read_lags_with_offsets
|
400
126
|
def read_lags_with_offsets(consumer_groups_with_topics = {}, active_topics_only: true)
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
[topic[:topic_name], topic[:partition_count]]
|
406
|
-
end.to_h.freeze
|
407
|
-
|
408
|
-
# If no expected CGs, we use all from routing that have active topics
|
409
|
-
if consumer_groups_with_topics.empty?
|
410
|
-
consumer_groups_with_topics = Karafka::App.routes.map do |cg|
|
411
|
-
cg_topics = cg.topics.select do |cg_topic|
|
412
|
-
active_topics_only ? cg_topic.active? : true
|
413
|
-
end
|
414
|
-
|
415
|
-
[cg.id, cg_topics.map(&:name)]
|
416
|
-
end.to_h
|
417
|
-
end
|
418
|
-
|
419
|
-
# We make a copy because we will remove once with non-existing topics
|
420
|
-
# We keep original requested consumer groups with topics for later backfilling
|
421
|
-
cgs_with_topics = consumer_groups_with_topics.dup
|
422
|
-
cgs_with_topics.transform_values!(&:dup)
|
423
|
-
|
424
|
-
# We can query only topics that do exist, this is why we are cleaning those that do not
|
425
|
-
# exist
|
426
|
-
cgs_with_topics.each_value do |requested_topics|
|
427
|
-
requested_topics.delete_if { |topic| !existing_topics.include?(topic) }
|
428
|
-
end
|
429
|
-
|
430
|
-
groups_lags = Hash.new { |h, k| h[k] = {} }
|
431
|
-
groups_offs = Hash.new { |h, k| h[k] = {} }
|
432
|
-
|
433
|
-
cgs_with_topics.each do |cg, topics|
|
434
|
-
# Do not add to tpl topics that do not exist
|
435
|
-
next if topics.empty?
|
436
|
-
|
437
|
-
tpl = Rdkafka::Consumer::TopicPartitionList.new
|
438
|
-
|
439
|
-
with_consumer('group.id': cg) do |consumer|
|
440
|
-
topics.each { |topic| tpl.add_topic(topic, existing_topics[topic]) }
|
441
|
-
|
442
|
-
commit_offsets = consumer.committed(tpl)
|
443
|
-
|
444
|
-
commit_offsets.to_h.each do |topic, partitions|
|
445
|
-
groups_offs[cg][topic] = {}
|
446
|
-
|
447
|
-
partitions.each do |partition|
|
448
|
-
# -1 when no offset is stored
|
449
|
-
groups_offs[cg][topic][partition.partition] = partition.offset || -1
|
450
|
-
end
|
451
|
-
end
|
452
|
-
|
453
|
-
consumer.lag(commit_offsets).each do |topic, partitions_lags|
|
454
|
-
groups_lags[cg][topic] = partitions_lags
|
455
|
-
end
|
456
|
-
end
|
457
|
-
end
|
458
|
-
|
459
|
-
consumer_groups_with_topics.each do |cg, topics|
|
460
|
-
groups_lags[cg]
|
461
|
-
|
462
|
-
topics.each do |topic|
|
463
|
-
groups_lags[cg][topic] ||= {}
|
464
|
-
|
465
|
-
next unless existing_topics.key?(topic)
|
466
|
-
|
467
|
-
# We backfill because there is a case where our consumer group would consume for
|
468
|
-
# example only one partition out of 20, rest needs to get -1
|
469
|
-
existing_topics[topic].times do |partition_id|
|
470
|
-
groups_lags[cg][topic][partition_id] ||= -1
|
471
|
-
end
|
472
|
-
end
|
473
|
-
end
|
474
|
-
|
475
|
-
merged = Hash.new { |h, k| h[k] = {} }
|
476
|
-
|
477
|
-
groups_lags.each do |cg, topics|
|
478
|
-
topics.each do |topic, partitions|
|
479
|
-
merged[cg][topic] = {}
|
480
|
-
|
481
|
-
partitions.each do |partition, lag|
|
482
|
-
merged[cg][topic][partition] = {
|
483
|
-
offset: groups_offs.fetch(cg).fetch(topic).fetch(partition),
|
484
|
-
lag: lag
|
485
|
-
}
|
486
|
-
end
|
487
|
-
end
|
488
|
-
end
|
489
|
-
|
490
|
-
merged
|
127
|
+
ConsumerGroups.read_lags_with_offsets(
|
128
|
+
consumer_groups_with_topics,
|
129
|
+
active_topics_only: active_topics_only
|
130
|
+
)
|
491
131
|
end
|
492
132
|
|
493
133
|
# @return [Rdkafka::Metadata] cluster metadata info
|
@@ -495,24 +135,6 @@ module Karafka
|
|
495
135
|
with_admin(&:metadata)
|
496
136
|
end
|
497
137
|
|
498
|
-
# Returns basic topic metadata
|
499
|
-
#
|
500
|
-
# @param topic_name [String] name of the topic we're interested in
|
501
|
-
# @return [Hash] topic metadata info hash
|
502
|
-
# @raise [Rdkafka::RdkafkaError] `unknown_topic_or_part` if requested topic is not found
|
503
|
-
#
|
504
|
-
# @note This query is much more efficient than doing a full `#cluster_info` + topic lookup
|
505
|
-
# because it does not have to query for all the topics data but just the topic we're
|
506
|
-
# interested in
|
507
|
-
def topic_info(topic_name)
|
508
|
-
with_admin do |admin|
|
509
|
-
admin
|
510
|
-
.metadata(topic_name)
|
511
|
-
.topics
|
512
|
-
.find { |topic| topic[:topic_name] == topic_name }
|
513
|
-
end
|
514
|
-
end
|
515
|
-
|
516
138
|
# Creates consumer instance and yields it. After usage it closes the consumer instance
|
517
139
|
# This API can be used in other pieces of code and allows for low-level consumer usage
|
518
140
|
#
|
@@ -596,11 +218,6 @@ module Karafka
|
|
596
218
|
::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.delete(id)
|
597
219
|
end
|
598
220
|
|
599
|
-
# @return [Array<String>] topics names
|
600
|
-
def topics_names
|
601
|
-
cluster_info.topics.map { |topic| topic.fetch(:topic_name) }
|
602
|
-
end
|
603
|
-
|
604
221
|
# There are some cases where rdkafka admin operations finish successfully but without the
|
605
222
|
# callback being triggered to materialize the post-promise object. Until this is fixed we
|
606
223
|
# can figure out, that operation we wanted to do finished successfully by checking that the
|
@@ -645,32 +262,6 @@ module Karafka
|
|
645
262
|
.then { |config| Karafka::Setup::AttributesMap.public_send(type, config) }
|
646
263
|
.then { |config| ::Rdkafka::Config.new(config) }
|
647
264
|
end
|
648
|
-
|
649
|
-
# Resolves the offset if offset is in a time format. Otherwise returns the offset without
|
650
|
-
# resolving.
|
651
|
-
# @param consumer [::Rdkafka::Consumer]
|
652
|
-
# @param name [String, Symbol] expected topic name
|
653
|
-
# @param partition [Integer]
|
654
|
-
# @param offset [Integer, Time]
|
655
|
-
# @return [Integer] expected offset
|
656
|
-
def resolve_offset(consumer, name, partition, offset)
|
657
|
-
if offset.is_a?(Time)
|
658
|
-
tpl = ::Rdkafka::Consumer::TopicPartitionList.new
|
659
|
-
tpl.add_topic_and_partitions_with_offsets(
|
660
|
-
name, partition => offset
|
661
|
-
)
|
662
|
-
|
663
|
-
real_offsets = consumer.offsets_for_times(tpl)
|
664
|
-
detected_offset = real_offsets
|
665
|
-
.to_h
|
666
|
-
.fetch(name)
|
667
|
-
.find { |p_data| p_data.partition == partition }
|
668
|
-
|
669
|
-
detected_offset&.offset || raise(Errors::InvalidTimeBasedOffsetError)
|
670
|
-
else
|
671
|
-
offset
|
672
|
-
end
|
673
|
-
end
|
674
265
|
end
|
675
266
|
end
|
676
267
|
end
|
@@ -210,6 +210,28 @@ module Karafka
|
|
210
210
|
)
|
211
211
|
end
|
212
212
|
|
213
|
+
# Returns a string representation of the consumer instance for debugging purposes.
|
214
|
+
#
|
215
|
+
# This method provides a safe inspection that avoids walking through potentially large
|
216
|
+
# nested objects like messages, client connections, or coordinator state that could
|
217
|
+
# cause performance issues during logging or debugging.
|
218
|
+
#
|
219
|
+
# @return [String] formatted string containing essential consumer information including
|
220
|
+
# consumer ID, topic name, partition number, usage status, message count, and
|
221
|
+
# revocation status
|
222
|
+
def inspect
|
223
|
+
parts = [
|
224
|
+
"id=#{@id}",
|
225
|
+
"topic=#{topic&.name.inspect}",
|
226
|
+
"partition=#{partition}",
|
227
|
+
"used=#{@used}",
|
228
|
+
"messages_count=#{@messages&.count}",
|
229
|
+
"revoked=#{coordinator&.revoked?}"
|
230
|
+
]
|
231
|
+
|
232
|
+
"#<#{self.class.name}:#{format('%#x', object_id)} #{parts.join(' ')}>"
|
233
|
+
end
|
234
|
+
|
213
235
|
private
|
214
236
|
|
215
237
|
# Method called post-initialization of a consumer when all basic things are assigned.
|
@@ -1,20 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# This code is part of Karafka Pro, a commercial component not licensed under LGPL.
|
4
|
-
# See LICENSE for details.
|
5
|
-
|
6
3
|
module Karafka
|
7
|
-
|
4
|
+
class Cli
|
5
|
+
# CLI related contracts
|
8
6
|
module Contracts
|
9
7
|
# Contract for validating correctness of the server cli command options.
|
10
|
-
|
11
|
-
class ServerCliOptions < ::Karafka::Contracts::ServerCliOptions
|
8
|
+
class Server < ::Karafka::Contracts::Base
|
12
9
|
configure do |config|
|
13
10
|
config.error_messages = YAML.safe_load(
|
14
11
|
File.read(
|
15
12
|
File.join(Karafka.gem_root, 'config', 'locales', 'errors.yml')
|
16
13
|
)
|
17
|
-
).fetch('en').fetch('validations').fetch('
|
14
|
+
).fetch('en').fetch('validations').fetch('cli').fetch('server')
|
18
15
|
end
|
19
16
|
|
20
17
|
%i[
|
@@ -79,11 +76,6 @@ module Karafka
|
|
79
76
|
|
80
77
|
next if (value - topics).empty?
|
81
78
|
|
82
|
-
# If there are any patterns defined, we cannot report on topics inclusions because
|
83
|
-
# topics may be added during boot or runtime. We go with simple assumption:
|
84
|
-
# if there are patterns defined, we do not check the inclusions at all
|
85
|
-
next unless Karafka::App.consumer_groups.map(&:patterns).flatten.empty?
|
86
|
-
|
87
79
|
# Found unknown topics
|
88
80
|
[[[:"#{action}_topics"], :topics_inclusion]]
|
89
81
|
end
|
data/lib/karafka/cli/info.rb
CHANGED
@@ -44,7 +44,7 @@ module Karafka
|
|
44
44
|
"Consumer groups count: #{Karafka::App.consumer_groups.size}",
|
45
45
|
"Subscription groups count: #{Karafka::App.subscription_groups.values.flatten.size}",
|
46
46
|
"Workers count: #{concurrency}",
|
47
|
-
"
|
47
|
+
"Instance client id: #{client_id}",
|
48
48
|
"Boot file: #{Karafka.boot_file}",
|
49
49
|
"Environment: #{Karafka.env}"
|
50
50
|
]
|