karafka 2.5.0 → 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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{ci.yml → ci_linux_ubuntu_x86_64_gnu.yml} +54 -30
  3. data/.github/workflows/ci_macos_arm64.yml +148 -0
  4. data/.github/workflows/push.yml +2 -2
  5. data/.github/workflows/trigger-wiki-refresh.yml +30 -0
  6. data/.github/workflows/verify-action-pins.yml +1 -1
  7. data/.ruby-version +1 -1
  8. data/CHANGELOG.md +28 -1
  9. data/Gemfile +2 -1
  10. data/Gemfile.lock +55 -26
  11. data/README.md +2 -2
  12. data/bin/integrations +3 -1
  13. data/bin/verify_kafka_warnings +2 -1
  14. data/config/locales/errors.yml +153 -152
  15. data/config/locales/pro_errors.yml +135 -134
  16. data/karafka.gemspec +3 -3
  17. data/lib/active_job/queue_adapters/karafka_adapter.rb +30 -1
  18. data/lib/karafka/active_job/dispatcher.rb +19 -9
  19. data/lib/karafka/admin/acl.rb +7 -8
  20. data/lib/karafka/admin/configs/config.rb +2 -2
  21. data/lib/karafka/admin/configs/resource.rb +2 -2
  22. data/lib/karafka/admin/configs.rb +3 -7
  23. data/lib/karafka/admin/consumer_groups.rb +351 -0
  24. data/lib/karafka/admin/topics.rb +206 -0
  25. data/lib/karafka/admin.rb +42 -451
  26. data/lib/karafka/base_consumer.rb +22 -0
  27. data/lib/karafka/{pro/contracts/server_cli_options.rb → cli/contracts/server.rb} +4 -12
  28. data/lib/karafka/cli/info.rb +1 -1
  29. data/lib/karafka/cli/install.rb +0 -2
  30. data/lib/karafka/connection/client.rb +8 -0
  31. data/lib/karafka/connection/listener.rb +5 -1
  32. data/lib/karafka/connection/status.rb +12 -9
  33. data/lib/karafka/errors.rb +0 -8
  34. data/lib/karafka/instrumentation/assignments_tracker.rb +16 -0
  35. data/lib/karafka/instrumentation/logger_listener.rb +109 -50
  36. data/lib/karafka/pro/active_job/dispatcher.rb +5 -0
  37. data/lib/karafka/pro/cleaner/messages/messages.rb +18 -8
  38. data/lib/karafka/pro/cli/contracts/server.rb +106 -0
  39. data/lib/karafka/pro/encryption/contracts/config.rb +1 -1
  40. data/lib/karafka/pro/loader.rb +1 -1
  41. data/lib/karafka/pro/recurring_tasks/contracts/config.rb +1 -1
  42. data/lib/karafka/pro/routing/features/adaptive_iterator/contracts/topic.rb +1 -1
  43. data/lib/karafka/pro/routing/features/adaptive_iterator/topic.rb +9 -0
  44. data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
  45. data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +9 -0
  46. data/lib/karafka/pro/routing/features/delaying/contracts/topic.rb +1 -1
  47. data/lib/karafka/pro/routing/features/delaying/topic.rb +9 -0
  48. data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +1 -1
  49. data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +1 -1
  50. data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +9 -0
  51. data/lib/karafka/pro/routing/features/expiring/contracts/topic.rb +1 -1
  52. data/lib/karafka/pro/routing/features/expiring/topic.rb +9 -0
  53. data/lib/karafka/pro/routing/features/filtering/contracts/topic.rb +1 -1
  54. data/lib/karafka/pro/routing/features/filtering/topic.rb +9 -0
  55. data/lib/karafka/pro/routing/features/inline_insights/contracts/topic.rb +1 -1
  56. data/lib/karafka/pro/routing/features/inline_insights/topic.rb +9 -0
  57. data/lib/karafka/pro/routing/features/long_running_job/contracts/topic.rb +1 -1
  58. data/lib/karafka/pro/routing/features/long_running_job/topic.rb +9 -0
  59. data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +1 -1
  60. data/lib/karafka/pro/routing/features/multiplexing.rb +1 -1
  61. data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +1 -1
  62. data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +9 -0
  63. data/lib/karafka/pro/routing/features/parallel_segments/contracts/consumer_group.rb +1 -1
  64. data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +1 -1
  65. data/lib/karafka/pro/routing/features/patterns/contracts/topic.rb +1 -1
  66. data/lib/karafka/pro/routing/features/patterns/topic.rb +9 -0
  67. data/lib/karafka/pro/routing/features/pausing/contracts/topic.rb +1 -1
  68. data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +1 -1
  69. data/lib/karafka/pro/routing/features/periodic_job/topic.rb +9 -0
  70. data/lib/karafka/pro/routing/features/recurring_tasks/contracts/topic.rb +1 -1
  71. data/lib/karafka/pro/routing/features/recurring_tasks/topic.rb +9 -0
  72. data/lib/karafka/pro/routing/features/scheduled_messages/contracts/topic.rb +1 -1
  73. data/lib/karafka/pro/routing/features/scheduled_messages/topic.rb +9 -0
  74. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +1 -1
  75. data/lib/karafka/pro/routing/features/swarm/topic.rb +9 -0
  76. data/lib/karafka/pro/routing/features/throttling/contracts/topic.rb +1 -1
  77. data/lib/karafka/pro/routing/features/throttling/topic.rb +9 -0
  78. data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -1
  79. data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +9 -0
  80. data/lib/karafka/pro/scheduled_messages/contracts/config.rb +1 -1
  81. data/lib/karafka/pro/scheduled_messages/daily_buffer.rb +9 -3
  82. data/lib/karafka/pro/swarm/liveness_listener.rb +17 -2
  83. data/lib/karafka/processing/executor.rb +1 -1
  84. data/lib/karafka/routing/builder.rb +0 -3
  85. data/lib/karafka/routing/consumer_group.rb +1 -4
  86. data/lib/karafka/routing/contracts/consumer_group.rb +84 -0
  87. data/lib/karafka/routing/contracts/routing.rb +61 -0
  88. data/lib/karafka/routing/contracts/topic.rb +83 -0
  89. data/lib/karafka/routing/features/active_job/contracts/topic.rb +1 -1
  90. data/lib/karafka/routing/features/active_job/topic.rb +9 -0
  91. data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -1
  92. data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -0
  93. data/lib/karafka/routing/features/declaratives/contracts/topic.rb +1 -1
  94. data/lib/karafka/routing/features/declaratives/topic.rb +9 -0
  95. data/lib/karafka/routing/features/deserializers/contracts/topic.rb +1 -1
  96. data/lib/karafka/routing/features/deserializers/topic.rb +9 -0
  97. data/lib/karafka/routing/features/eofed/contracts/topic.rb +1 -1
  98. data/lib/karafka/routing/features/eofed/topic.rb +9 -0
  99. data/lib/karafka/routing/features/inline_insights/contracts/topic.rb +1 -1
  100. data/lib/karafka/routing/features/inline_insights/topic.rb +9 -0
  101. data/lib/karafka/routing/features/manual_offset_management/contracts/topic.rb +1 -1
  102. data/lib/karafka/routing/features/manual_offset_management/topic.rb +9 -0
  103. data/lib/karafka/routing/subscription_group.rb +1 -10
  104. data/lib/karafka/routing/topic.rb +9 -1
  105. data/lib/karafka/server.rb +2 -7
  106. data/lib/karafka/setup/attributes_map.rb +36 -0
  107. data/lib/karafka/setup/config.rb +6 -7
  108. data/lib/karafka/setup/contracts/config.rb +217 -0
  109. data/lib/karafka/setup/defaults_injector.rb +3 -1
  110. data/lib/karafka/swarm/node.rb +66 -6
  111. data/lib/karafka/swarm.rb +2 -2
  112. data/lib/karafka/templates/karafka.rb.erb +2 -7
  113. data/lib/karafka/version.rb +1 -1
  114. data/lib/karafka.rb +17 -18
  115. metadata +18 -15
  116. data/lib/karafka/contracts/config.rb +0 -210
  117. data/lib/karafka/contracts/consumer_group.rb +0 -81
  118. data/lib/karafka/contracts/routing.rb +0 -59
  119. data/lib/karafka/contracts/server_cli_options.rb +0 -92
  120. data/lib/karafka/contracts/topic.rb +0 -81
  121. 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
- module Admin
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
- # Allows us to read messages from the topic
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. If -1 is provided
43
- # (default) we will start from the latest offset. If time is provided, the appropriate
44
- # offset will be resolved. If negative beyond -1 is provided, we move backwards more.
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
- messages = []
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 as described here:
122
- # https://kafka.apache.org/documentation/#topicconfigs
43
+ # @param topic_config [Hash] topic config details
44
+ # @see Topics.create
123
45
  def create_topic(name, partitions, replication_factor, topic_config = {})
124
- with_admin do |admin|
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
- with_admin do |admin|
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
- with_admin do |admin|
154
- handler = admin.create_partitions(name, partitions)
59
+ Topics.create_partitions(name, partitions)
60
+ end
155
61
 
156
- with_re_wait(
157
- -> { handler.wait(max_wait_timeout: max_wait_time_seconds) },
158
- -> { topic_info(name).fetch(:partition_count) >= partitions }
159
- )
160
- end
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 to
168
- # where to move given consumer. It allows us to move particular partitions or whole topics
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
- tpl_base = {}
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
- remap = Hash.new { |h, k| h[k] = {} }
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
- copy_result = copy_consumer_group(previous_name, new_name, topics)
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
- with_admin do |admin|
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
- # We first fetch all the topics with partitions count that exist in the cluster so we
402
- # do not query for topics that do not exist and so we can get partitions count for all
403
- # the topics we may need. The non-existent and not consumed will be filled at the end
404
- existing_topics = cluster_info.topics.map do |topic|
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
- module Pro
4
+ class Cli
5
+ # CLI related contracts
8
6
  module Contracts
9
7
  # Contract for validating correctness of the server cli command options.
10
- # It differs slightly from the OSS one because it is aware of the routing patterns
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('server_cli_options')
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
@@ -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
- "Application client id: #{client_id}",
47
+ "Instance client id: #{client_id}",
48
48
  "Boot file: #{Karafka.boot_file}",
49
49
  "Environment: #{Karafka.env}"
50
50
  ]
@@ -14,9 +14,7 @@ module Karafka
14
14
  # Directories created by default
15
15
  INSTALL_DIRS = %w[
16
16
  app/consumers
17
- config
18
17
  log
19
- lib
20
18
  ].freeze
21
19
 
22
20
  # Where should we map proper files from templates