ruby-kafka 0.7.10 → 1.5.0
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/.circleci/config.yml +179 -0
- data/.github/workflows/stale.yml +19 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +40 -0
- data/README.md +167 -0
- data/lib/kafka/async_producer.rb +60 -42
- data/lib/kafka/client.rb +92 -6
- data/lib/kafka/cluster.rb +82 -24
- data/lib/kafka/connection.rb +3 -0
- data/lib/kafka/consumer.rb +61 -11
- data/lib/kafka/consumer_group/assignor.rb +63 -0
- data/lib/kafka/consumer_group.rb +29 -6
- data/lib/kafka/crc32_hash.rb +15 -0
- data/lib/kafka/datadog.rb +20 -13
- data/lib/kafka/digest.rb +22 -0
- data/lib/kafka/fetcher.rb +5 -2
- data/lib/kafka/interceptors.rb +33 -0
- data/lib/kafka/murmur2_hash.rb +17 -0
- data/lib/kafka/offset_manager.rb +12 -1
- data/lib/kafka/partitioner.rb +8 -3
- data/lib/kafka/producer.rb +13 -5
- data/lib/kafka/prometheus.rb +78 -79
- data/lib/kafka/protocol/add_offsets_to_txn_response.rb +2 -0
- data/lib/kafka/protocol/encoder.rb +1 -1
- data/lib/kafka/protocol/join_group_request.rb +8 -2
- data/lib/kafka/protocol/join_group_response.rb +9 -1
- data/lib/kafka/protocol/metadata_response.rb +1 -1
- data/lib/kafka/protocol/offset_fetch_request.rb +3 -1
- data/lib/kafka/protocol/record_batch.rb +2 -2
- data/lib/kafka/protocol/sasl_handshake_request.rb +1 -1
- data/lib/kafka/protocol/sync_group_response.rb +5 -2
- data/lib/kafka/protocol/txn_offset_commit_response.rb +34 -5
- data/lib/kafka/round_robin_assignment_strategy.rb +37 -39
- data/lib/kafka/sasl/awsmskiam.rb +133 -0
- data/lib/kafka/sasl_authenticator.rb +15 -2
- data/lib/kafka/ssl_context.rb +6 -5
- data/lib/kafka/tagged_logger.rb +1 -0
- data/lib/kafka/transaction_manager.rb +30 -10
- data/lib/kafka/version.rb +1 -1
- data/ruby-kafka.gemspec +5 -4
- metadata +39 -13
data/lib/kafka/async_producer.rb
CHANGED
@@ -59,8 +59,6 @@ module Kafka
|
|
59
59
|
# producer.shutdown
|
60
60
|
#
|
61
61
|
class AsyncProducer
|
62
|
-
THREAD_MUTEX = Mutex.new
|
63
|
-
|
64
62
|
# Initializes a new AsyncProducer.
|
65
63
|
#
|
66
64
|
# @param sync_producer [Kafka::Producer] the synchronous producer that should
|
@@ -94,6 +92,8 @@ module Kafka
|
|
94
92
|
|
95
93
|
# The timer will no-op if the delivery interval is zero.
|
96
94
|
@timer = Timer.new(queue: @queue, interval: delivery_interval)
|
95
|
+
|
96
|
+
@thread_mutex = Mutex.new
|
97
97
|
end
|
98
98
|
|
99
99
|
# Produces a message to the specified topic.
|
@@ -103,6 +103,9 @@ module Kafka
|
|
103
103
|
# @raise [BufferOverflow] if the message queue is full.
|
104
104
|
# @return [nil]
|
105
105
|
def produce(value, topic:, **options)
|
106
|
+
# We want to fail fast if `topic` isn't a String
|
107
|
+
topic = topic.to_str
|
108
|
+
|
106
109
|
ensure_threads_running!
|
107
110
|
|
108
111
|
if @queue.size >= @max_queue_size
|
@@ -128,6 +131,8 @@ module Kafka
|
|
128
131
|
# @see Kafka::Producer#deliver_messages
|
129
132
|
# @return [nil]
|
130
133
|
def deliver_messages
|
134
|
+
ensure_threads_running!
|
135
|
+
|
131
136
|
@queue << [:deliver_messages, nil]
|
132
137
|
|
133
138
|
nil
|
@@ -139,6 +144,8 @@ module Kafka
|
|
139
144
|
# @see Kafka::Producer#shutdown
|
140
145
|
# @return [nil]
|
141
146
|
def shutdown
|
147
|
+
ensure_threads_running!
|
148
|
+
|
142
149
|
@timer_thread && @timer_thread.exit
|
143
150
|
@queue << [:shutdown, nil]
|
144
151
|
@worker_thread && @worker_thread.join
|
@@ -149,17 +156,22 @@ module Kafka
|
|
149
156
|
private
|
150
157
|
|
151
158
|
def ensure_threads_running!
|
152
|
-
|
153
|
-
@worker_thread = nil unless @worker_thread && @worker_thread.alive?
|
154
|
-
@worker_thread ||= Thread.new { @worker.run }
|
155
|
-
end
|
159
|
+
return if worker_thread_alive? && timer_thread_alive?
|
156
160
|
|
157
|
-
|
158
|
-
@
|
159
|
-
@timer_thread
|
161
|
+
@thread_mutex.synchronize do
|
162
|
+
@worker_thread = Thread.new { @worker.run } unless worker_thread_alive?
|
163
|
+
@timer_thread = Thread.new { @timer.run } unless timer_thread_alive?
|
160
164
|
end
|
161
165
|
end
|
162
166
|
|
167
|
+
def worker_thread_alive?
|
168
|
+
!!@worker_thread && @worker_thread.alive?
|
169
|
+
end
|
170
|
+
|
171
|
+
def timer_thread_alive?
|
172
|
+
!!@timer_thread && @timer_thread.alive?
|
173
|
+
end
|
174
|
+
|
163
175
|
def buffer_overflow(topic, message)
|
164
176
|
@instrumenter.instrument("buffer_overflow.async_producer", {
|
165
177
|
topic: topic,
|
@@ -200,31 +212,45 @@ module Kafka
|
|
200
212
|
@logger.push_tags(@producer.to_s)
|
201
213
|
@logger.info "Starting async producer in the background..."
|
202
214
|
|
215
|
+
do_loop
|
216
|
+
rescue Exception => e
|
217
|
+
@logger.error "Unexpected Kafka error #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
218
|
+
@logger.error "Async producer crashed!"
|
219
|
+
ensure
|
220
|
+
@producer.shutdown
|
221
|
+
@logger.pop_tags
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def do_loop
|
203
227
|
loop do
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
produce
|
209
|
-
|
210
|
-
|
211
|
-
deliver_messages
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
228
|
+
begin
|
229
|
+
operation, payload = @queue.pop
|
230
|
+
|
231
|
+
case operation
|
232
|
+
when :produce
|
233
|
+
produce(payload[0], **payload[1])
|
234
|
+
deliver_messages if threshold_reached?
|
235
|
+
when :deliver_messages
|
236
|
+
deliver_messages
|
237
|
+
when :shutdown
|
238
|
+
begin
|
239
|
+
# Deliver any pending messages first.
|
240
|
+
@producer.deliver_messages
|
241
|
+
rescue Error => e
|
242
|
+
@logger.error("Failed to deliver messages during shutdown: #{e.message}")
|
243
|
+
|
244
|
+
@instrumenter.instrument("drop_messages.async_producer", {
|
245
|
+
message_count: @producer.buffer_size + @queue.size,
|
246
|
+
})
|
247
|
+
end
|
248
|
+
|
249
|
+
# Stop the run loop.
|
250
|
+
break
|
251
|
+
else
|
252
|
+
raise "Unknown operation #{operation.inspect}"
|
222
253
|
end
|
223
|
-
|
224
|
-
# Stop the run loop.
|
225
|
-
break
|
226
|
-
else
|
227
|
-
raise "Unknown operation #{operation.inspect}"
|
228
254
|
end
|
229
255
|
end
|
230
256
|
rescue Kafka::Error => e
|
@@ -233,20 +259,12 @@ module Kafka
|
|
233
259
|
|
234
260
|
sleep 10
|
235
261
|
retry
|
236
|
-
rescue Exception => e
|
237
|
-
@logger.error "Unexpected Kafka error #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
238
|
-
@logger.error "Async producer crashed!"
|
239
|
-
ensure
|
240
|
-
@producer.shutdown
|
241
|
-
@logger.pop_tags
|
242
262
|
end
|
243
263
|
|
244
|
-
|
245
|
-
|
246
|
-
def produce(*args)
|
264
|
+
def produce(value, **kwargs)
|
247
265
|
retries = 0
|
248
266
|
begin
|
249
|
-
@producer.produce(
|
267
|
+
@producer.produce(value, **kwargs)
|
250
268
|
rescue BufferOverflow => e
|
251
269
|
deliver_messages
|
252
270
|
if @max_retries == -1
|
data/lib/kafka/client.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require "kafka/ssl_context"
|
@@ -38,8 +39,8 @@ module Kafka
|
|
38
39
|
# @param ssl_ca_cert [String, Array<String>, nil] a PEM encoded CA cert, or an Array of
|
39
40
|
# PEM encoded CA certs, to use with an SSL connection.
|
40
41
|
#
|
41
|
-
# @param ssl_ca_cert_file_path [String, nil] a path on the filesystem
|
42
|
-
#
|
42
|
+
# @param ssl_ca_cert_file_path [String, Array<String>, nil] a path on the filesystem, or an
|
43
|
+
# Array of paths, to PEM encoded CA cert(s) to use with an SSL connection.
|
43
44
|
#
|
44
45
|
# @param ssl_client_cert [String, nil] a PEM encoded client cert to use with an
|
45
46
|
# SSL connection. Must be used in combination with ssl_client_cert_key.
|
@@ -62,19 +63,38 @@ module Kafka
|
|
62
63
|
#
|
63
64
|
# @param sasl_over_ssl [Boolean] whether to enforce SSL with SASL
|
64
65
|
#
|
66
|
+
# @param ssl_ca_certs_from_system [Boolean] whether to use the CA certs from the
|
67
|
+
# system's default certificate store.
|
68
|
+
#
|
69
|
+
# @param partitioner [Partitioner, nil] the partitioner that should be used by the client.
|
70
|
+
#
|
65
71
|
# @param sasl_oauth_token_provider [Object, nil] OAuthBearer Token Provider instance that
|
66
72
|
# implements method token. See {Sasl::OAuth#initialize}
|
67
73
|
#
|
74
|
+
# @param ssl_verify_hostname [Boolean, true] whether to verify that the host serving
|
75
|
+
# the SSL certificate and the signing chain of the certificate have the correct domains
|
76
|
+
# based on the CA certificate
|
77
|
+
#
|
78
|
+
# @param resolve_seed_brokers [Boolean] whether to resolve each hostname of the seed brokers.
|
79
|
+
# If a broker is resolved to multiple IP addresses, the client tries to connect to each
|
80
|
+
# of the addresses until it can connect.
|
81
|
+
#
|
68
82
|
# @return [Client]
|
69
83
|
def initialize(seed_brokers:, client_id: "ruby-kafka", logger: nil, connect_timeout: nil, socket_timeout: nil,
|
70
84
|
ssl_ca_cert_file_path: nil, ssl_ca_cert: nil, ssl_client_cert: nil, ssl_client_cert_key: nil,
|
71
85
|
ssl_client_cert_key_password: nil, ssl_client_cert_chain: nil, sasl_gssapi_principal: nil,
|
72
86
|
sasl_gssapi_keytab: nil, sasl_plain_authzid: '', sasl_plain_username: nil, sasl_plain_password: nil,
|
73
87
|
sasl_scram_username: nil, sasl_scram_password: nil, sasl_scram_mechanism: nil,
|
74
|
-
|
88
|
+
sasl_aws_msk_iam_access_key_id: nil,
|
89
|
+
sasl_aws_msk_iam_secret_key_id: nil,
|
90
|
+
sasl_aws_msk_iam_aws_region: nil,
|
91
|
+
sasl_aws_msk_iam_session_token: nil,
|
92
|
+
sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true,
|
93
|
+
resolve_seed_brokers: false)
|
75
94
|
@logger = TaggedLogger.new(logger)
|
76
95
|
@instrumenter = Instrumenter.new(client_id: client_id)
|
77
96
|
@seed_brokers = normalize_seed_brokers(seed_brokers)
|
97
|
+
@resolve_seed_brokers = resolve_seed_brokers
|
78
98
|
|
79
99
|
ssl_context = SslContext.build(
|
80
100
|
ca_cert_file_path: ssl_ca_cert_file_path,
|
@@ -96,6 +116,10 @@ module Kafka
|
|
96
116
|
sasl_scram_username: sasl_scram_username,
|
97
117
|
sasl_scram_password: sasl_scram_password,
|
98
118
|
sasl_scram_mechanism: sasl_scram_mechanism,
|
119
|
+
sasl_aws_msk_iam_access_key_id: sasl_aws_msk_iam_access_key_id,
|
120
|
+
sasl_aws_msk_iam_secret_key_id: sasl_aws_msk_iam_secret_key_id,
|
121
|
+
sasl_aws_msk_iam_aws_region: sasl_aws_msk_iam_aws_region,
|
122
|
+
sasl_aws_msk_iam_session_token: sasl_aws_msk_iam_session_token,
|
99
123
|
sasl_oauth_token_provider: sasl_oauth_token_provider,
|
100
124
|
logger: @logger
|
101
125
|
)
|
@@ -115,6 +139,7 @@ module Kafka
|
|
115
139
|
)
|
116
140
|
|
117
141
|
@cluster = initialize_cluster
|
142
|
+
@partitioner = partitioner || Partitioner.new
|
118
143
|
end
|
119
144
|
|
120
145
|
# Delivers a single message to the Kafka cluster.
|
@@ -138,6 +163,9 @@ module Kafka
|
|
138
163
|
def deliver_message(value, key: nil, headers: {}, topic:, partition: nil, partition_key: nil, retries: 1)
|
139
164
|
create_time = Time.now
|
140
165
|
|
166
|
+
# We want to fail fast if `topic` isn't a String
|
167
|
+
topic = topic.to_str
|
168
|
+
|
141
169
|
message = PendingMessage.new(
|
142
170
|
value: value,
|
143
171
|
key: key,
|
@@ -150,7 +178,7 @@ module Kafka
|
|
150
178
|
|
151
179
|
if partition.nil?
|
152
180
|
partition_count = @cluster.partitions_for(topic).count
|
153
|
-
partition =
|
181
|
+
partition = @partitioner.call(partition_count, message)
|
154
182
|
end
|
155
183
|
|
156
184
|
buffer = MessageBuffer.new
|
@@ -191,6 +219,8 @@ module Kafka
|
|
191
219
|
attempt = 1
|
192
220
|
|
193
221
|
begin
|
222
|
+
@cluster.refresh_metadata_if_necessary!
|
223
|
+
|
194
224
|
operation.execute
|
195
225
|
|
196
226
|
unless buffer.empty?
|
@@ -241,6 +271,9 @@ module Kafka
|
|
241
271
|
# be in a message set before it should be compressed. Note that message sets
|
242
272
|
# are per-partition rather than per-topic or per-producer.
|
243
273
|
#
|
274
|
+
# @param interceptors [Array<Object>] a list of producer interceptors the implement
|
275
|
+
# `call(Kafka::PendingMessage)`.
|
276
|
+
#
|
244
277
|
# @return [Kafka::Producer] the Kafka producer.
|
245
278
|
def producer(
|
246
279
|
compression_codec: nil,
|
@@ -254,7 +287,8 @@ module Kafka
|
|
254
287
|
idempotent: false,
|
255
288
|
transactional: false,
|
256
289
|
transactional_id: nil,
|
257
|
-
transactional_timeout: 60
|
290
|
+
transactional_timeout: 60,
|
291
|
+
interceptors: []
|
258
292
|
)
|
259
293
|
cluster = initialize_cluster
|
260
294
|
compressor = Compressor.new(
|
@@ -284,6 +318,8 @@ module Kafka
|
|
284
318
|
retry_backoff: retry_backoff,
|
285
319
|
max_buffer_size: max_buffer_size,
|
286
320
|
max_buffer_bytesize: max_buffer_bytesize,
|
321
|
+
partitioner: @partitioner,
|
322
|
+
interceptors: interceptors
|
287
323
|
)
|
288
324
|
end
|
289
325
|
|
@@ -333,15 +369,26 @@ module Kafka
|
|
333
369
|
# @param fetcher_max_queue_size [Integer] max number of items in the fetch queue that
|
334
370
|
# are stored for further processing. Note, that each item in the queue represents a
|
335
371
|
# response from a single broker.
|
372
|
+
# @param refresh_topic_interval [Integer] interval of refreshing the topic list.
|
373
|
+
# If it is 0, the topic list won't be refreshed (default)
|
374
|
+
# If it is n (n > 0), the topic list will be refreshed every n seconds
|
375
|
+
# @param interceptors [Array<Object>] a list of consumer interceptors that implement
|
376
|
+
# `call(Kafka::FetchedBatch)`.
|
377
|
+
# @param assignment_strategy [Object] a partition assignment strategy that
|
378
|
+
# implements `protocol_type()`, `user_data()`, and `assign(members:, partitions:)`
|
336
379
|
# @return [Consumer]
|
337
380
|
def consumer(
|
338
381
|
group_id:,
|
339
382
|
session_timeout: 30,
|
383
|
+
rebalance_timeout: 60,
|
340
384
|
offset_commit_interval: 10,
|
341
385
|
offset_commit_threshold: 0,
|
342
386
|
heartbeat_interval: 10,
|
343
387
|
offset_retention_time: nil,
|
344
|
-
fetcher_max_queue_size: 100
|
388
|
+
fetcher_max_queue_size: 100,
|
389
|
+
refresh_topic_interval: 0,
|
390
|
+
interceptors: [],
|
391
|
+
assignment_strategy: nil
|
345
392
|
)
|
346
393
|
cluster = initialize_cluster
|
347
394
|
|
@@ -357,8 +404,10 @@ module Kafka
|
|
357
404
|
logger: @logger,
|
358
405
|
group_id: group_id,
|
359
406
|
session_timeout: session_timeout,
|
407
|
+
rebalance_timeout: rebalance_timeout,
|
360
408
|
retention_time: retention_time,
|
361
409
|
instrumenter: instrumenter,
|
410
|
+
assignment_strategy: assignment_strategy
|
362
411
|
)
|
363
412
|
|
364
413
|
fetcher = Fetcher.new(
|
@@ -394,6 +443,8 @@ module Kafka
|
|
394
443
|
fetcher: fetcher,
|
395
444
|
session_timeout: session_timeout,
|
396
445
|
heartbeat: heartbeat,
|
446
|
+
refresh_topic_interval: refresh_topic_interval,
|
447
|
+
interceptors: interceptors
|
397
448
|
)
|
398
449
|
end
|
399
450
|
|
@@ -530,6 +581,24 @@ module Kafka
|
|
530
581
|
end
|
531
582
|
end
|
532
583
|
|
584
|
+
# Describe broker configs
|
585
|
+
#
|
586
|
+
# @param broker_id [int] the id of the broker
|
587
|
+
# @param configs [Array] array of config keys.
|
588
|
+
# @return [Array<Kafka::Protocol::DescribeConfigsResponse::ConfigEntry>]
|
589
|
+
def describe_configs(broker_id, configs = [])
|
590
|
+
@cluster.describe_configs(broker_id, configs)
|
591
|
+
end
|
592
|
+
|
593
|
+
# Alter broker configs
|
594
|
+
#
|
595
|
+
# @param broker_id [int] the id of the broker
|
596
|
+
# @param configs [Array] array of config strings.
|
597
|
+
# @return [nil]
|
598
|
+
def alter_configs(broker_id, configs = [])
|
599
|
+
@cluster.alter_configs(broker_id, configs)
|
600
|
+
end
|
601
|
+
|
533
602
|
# Creates a topic in the cluster.
|
534
603
|
#
|
535
604
|
# @example Creating a topic with log compaction
|
@@ -615,6 +684,14 @@ module Kafka
|
|
615
684
|
@cluster.describe_group(group_id)
|
616
685
|
end
|
617
686
|
|
687
|
+
# Fetch all committed offsets for a consumer group
|
688
|
+
#
|
689
|
+
# @param group_id [String] the id of the consumer group
|
690
|
+
# @return [Hash<String, Hash<Integer, Kafka::Protocol::OffsetFetchResponse::PartitionOffsetInfo>>]
|
691
|
+
def fetch_group_offsets(group_id)
|
692
|
+
@cluster.fetch_group_offsets(group_id)
|
693
|
+
end
|
694
|
+
|
618
695
|
# Create partitions for a topic.
|
619
696
|
#
|
620
697
|
# @param name [String] the name of the topic.
|
@@ -663,6 +740,14 @@ module Kafka
|
|
663
740
|
@cluster.partitions_for(topic).count
|
664
741
|
end
|
665
742
|
|
743
|
+
# Counts the number of replicas for a topic's partition
|
744
|
+
#
|
745
|
+
# @param topic [String]
|
746
|
+
# @return [Integer] the number of replica nodes for the topic's partition
|
747
|
+
def replica_count_for(topic)
|
748
|
+
@cluster.partitions_for(topic).first.replicas.count
|
749
|
+
end
|
750
|
+
|
666
751
|
# Retrieve the offset of the last message in a partition. If there are no
|
667
752
|
# messages in the partition -1 is returned.
|
668
753
|
#
|
@@ -741,6 +826,7 @@ module Kafka
|
|
741
826
|
seed_brokers: @seed_brokers,
|
742
827
|
broker_pool: broker_pool,
|
743
828
|
logger: @logger,
|
829
|
+
resolve_seed_brokers: @resolve_seed_brokers,
|
744
830
|
)
|
745
831
|
end
|
746
832
|
|
data/lib/kafka/cluster.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "kafka/broker_pool"
|
4
|
+
require "resolv"
|
4
5
|
require "set"
|
5
6
|
|
6
7
|
module Kafka
|
@@ -18,7 +19,8 @@ module Kafka
|
|
18
19
|
# @param seed_brokers [Array<URI>]
|
19
20
|
# @param broker_pool [Kafka::BrokerPool]
|
20
21
|
# @param logger [Logger]
|
21
|
-
|
22
|
+
# @param resolve_seed_brokers [Boolean] See {Kafka::Client#initialize}
|
23
|
+
def initialize(seed_brokers:, broker_pool:, logger:, resolve_seed_brokers: false)
|
22
24
|
if seed_brokers.empty?
|
23
25
|
raise ArgumentError, "At least one seed broker must be configured"
|
24
26
|
end
|
@@ -26,6 +28,7 @@ module Kafka
|
|
26
28
|
@logger = TaggedLogger.new(logger)
|
27
29
|
@seed_brokers = seed_brokers
|
28
30
|
@broker_pool = broker_pool
|
31
|
+
@resolve_seed_brokers = resolve_seed_brokers
|
29
32
|
@cluster_info = nil
|
30
33
|
@stale = true
|
31
34
|
|
@@ -45,6 +48,10 @@ module Kafka
|
|
45
48
|
new_topics = topics - @target_topics
|
46
49
|
|
47
50
|
unless new_topics.empty?
|
51
|
+
if new_topics.any? { |topic| topic.nil? or topic.empty? }
|
52
|
+
raise ArgumentError, "Topic must not be nil or empty"
|
53
|
+
end
|
54
|
+
|
48
55
|
@logger.info "New topics added to target list: #{new_topics.to_a.join(', ')}"
|
49
56
|
|
50
57
|
@target_topics.merge(new_topics)
|
@@ -113,7 +120,7 @@ module Kafka
|
|
113
120
|
|
114
121
|
# Finds the broker acting as the coordinator of the given group.
|
115
122
|
#
|
116
|
-
# @param group_id
|
123
|
+
# @param group_id [String]
|
117
124
|
# @return [Broker] the broker that's currently coordinator.
|
118
125
|
def get_group_coordinator(group_id:)
|
119
126
|
@logger.debug "Getting group coordinator for `#{group_id}`"
|
@@ -123,7 +130,7 @@ module Kafka
|
|
123
130
|
|
124
131
|
# Finds the broker acting as the coordinator of the given transaction.
|
125
132
|
#
|
126
|
-
# @param transactional_id
|
133
|
+
# @param transactional_id [String]
|
127
134
|
# @return [Broker] the broker that's currently coordinator.
|
128
135
|
def get_transaction_coordinator(transactional_id:)
|
129
136
|
@logger.debug "Getting transaction coordinator for `#{transactional_id}`"
|
@@ -139,6 +146,40 @@ module Kafka
|
|
139
146
|
end
|
140
147
|
end
|
141
148
|
|
149
|
+
def describe_configs(broker_id, configs = [])
|
150
|
+
options = {
|
151
|
+
resources: [[Kafka::Protocol::RESOURCE_TYPE_CLUSTER, broker_id.to_s, configs]]
|
152
|
+
}
|
153
|
+
|
154
|
+
info = cluster_info.brokers.find {|broker| broker.node_id == broker_id }
|
155
|
+
broker = @broker_pool.connect(info.host, info.port, node_id: info.node_id)
|
156
|
+
|
157
|
+
response = broker.describe_configs(**options)
|
158
|
+
|
159
|
+
response.resources.each do |resource|
|
160
|
+
Protocol.handle_error(resource.error_code, resource.error_message)
|
161
|
+
end
|
162
|
+
|
163
|
+
response.resources.first.configs
|
164
|
+
end
|
165
|
+
|
166
|
+
def alter_configs(broker_id, configs = [])
|
167
|
+
options = {
|
168
|
+
resources: [[Kafka::Protocol::RESOURCE_TYPE_CLUSTER, broker_id.to_s, configs]]
|
169
|
+
}
|
170
|
+
|
171
|
+
info = cluster_info.brokers.find {|broker| broker.node_id == broker_id }
|
172
|
+
broker = @broker_pool.connect(info.host, info.port, node_id: info.node_id)
|
173
|
+
|
174
|
+
response = broker.alter_configs(**options)
|
175
|
+
|
176
|
+
response.resources.each do |resource|
|
177
|
+
Protocol.handle_error(resource.error_code, resource.error_message)
|
178
|
+
end
|
179
|
+
|
180
|
+
nil
|
181
|
+
end
|
182
|
+
|
142
183
|
def partitions_for(topic)
|
143
184
|
add_target_topics([topic])
|
144
185
|
refresh_metadata_if_necessary!
|
@@ -252,6 +293,20 @@ module Kafka
|
|
252
293
|
group
|
253
294
|
end
|
254
295
|
|
296
|
+
def fetch_group_offsets(group_id)
|
297
|
+
topics = get_group_coordinator(group_id: group_id)
|
298
|
+
.fetch_offsets(group_id: group_id, topics: nil)
|
299
|
+
.topics
|
300
|
+
|
301
|
+
topics.each do |_, partitions|
|
302
|
+
partitions.each do |_, response|
|
303
|
+
Protocol.handle_error(response.error_code)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
topics
|
308
|
+
end
|
309
|
+
|
255
310
|
def create_partitions_for(name, num_partitions:, timeout:)
|
256
311
|
options = {
|
257
312
|
topics: [[name, num_partitions, nil]],
|
@@ -366,32 +421,35 @@ module Kafka
|
|
366
421
|
# @return [Protocol::MetadataResponse] the cluster metadata.
|
367
422
|
def fetch_cluster_info
|
368
423
|
errors = []
|
369
|
-
|
370
424
|
@seed_brokers.shuffle.each do |node|
|
371
|
-
@
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
425
|
+
(@resolve_seed_brokers ? Resolv.getaddresses(node.hostname).shuffle : [node.hostname]).each do |hostname_or_ip|
|
426
|
+
node_info = node.to_s
|
427
|
+
node_info << " (#{hostname_or_ip})" if node.hostname != hostname_or_ip
|
428
|
+
@logger.info "Fetching cluster metadata from #{node_info}"
|
429
|
+
|
430
|
+
begin
|
431
|
+
broker = @broker_pool.connect(hostname_or_ip, node.port)
|
432
|
+
cluster_info = broker.fetch_metadata(topics: @target_topics)
|
433
|
+
|
434
|
+
if cluster_info.brokers.empty?
|
435
|
+
@logger.error "No brokers in cluster"
|
436
|
+
else
|
437
|
+
@logger.info "Discovered cluster metadata; nodes: #{cluster_info.brokers.join(', ')}"
|
438
|
+
|
439
|
+
@stale = false
|
440
|
+
|
441
|
+
return cluster_info
|
442
|
+
end
|
443
|
+
rescue Error => e
|
444
|
+
@logger.error "Failed to fetch metadata from #{node_info}: #{e}"
|
445
|
+
errors << [node_info, e]
|
446
|
+
ensure
|
447
|
+
broker.disconnect unless broker.nil?
|
385
448
|
end
|
386
|
-
rescue Error => e
|
387
|
-
@logger.error "Failed to fetch metadata from #{node}: #{e}"
|
388
|
-
errors << [node, e]
|
389
|
-
ensure
|
390
|
-
broker.disconnect unless broker.nil?
|
391
449
|
end
|
392
450
|
end
|
393
451
|
|
394
|
-
error_description = errors.map {|
|
452
|
+
error_description = errors.map {|node_info, exception| "- #{node_info}: #{exception}" }.join("\n")
|
395
453
|
|
396
454
|
raise ConnectionError, "Could not connect to any of the seed brokers:\n#{error_description}"
|
397
455
|
end
|