ruby-kafka 0.7.4 → 1.1.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 +168 -3
- data/.github/workflows/stale.yml +19 -0
- data/CHANGELOG.md +48 -0
- data/README.md +59 -0
- data/lib/kafka/async_producer.rb +30 -9
- data/lib/kafka/broker.rb +13 -1
- data/lib/kafka/broker_pool.rb +1 -1
- data/lib/kafka/client.rb +63 -6
- data/lib/kafka/cluster.rb +53 -1
- data/lib/kafka/compression.rb +13 -11
- data/lib/kafka/compressor.rb +1 -0
- data/lib/kafka/connection.rb +7 -1
- data/lib/kafka/connection_builder.rb +1 -1
- data/lib/kafka/consumer.rb +98 -17
- data/lib/kafka/consumer_group.rb +20 -2
- data/lib/kafka/datadog.rb +32 -12
- data/lib/kafka/fetch_operation.rb +1 -1
- data/lib/kafka/fetched_batch.rb +5 -1
- data/lib/kafka/fetched_batch_generator.rb +5 -2
- data/lib/kafka/fetched_message.rb +1 -0
- data/lib/kafka/fetched_offset_resolver.rb +1 -1
- data/lib/kafka/fetcher.rb +13 -6
- data/lib/kafka/gzip_codec.rb +4 -0
- data/lib/kafka/heartbeat.rb +8 -3
- data/lib/kafka/lz4_codec.rb +4 -0
- data/lib/kafka/offset_manager.rb +13 -2
- data/lib/kafka/produce_operation.rb +1 -1
- data/lib/kafka/producer.rb +33 -8
- data/lib/kafka/prometheus.rb +316 -0
- data/lib/kafka/protocol/add_offsets_to_txn_request.rb +29 -0
- data/lib/kafka/protocol/add_offsets_to_txn_response.rb +19 -0
- data/lib/kafka/protocol/join_group_request.rb +8 -2
- data/lib/kafka/protocol/metadata_response.rb +1 -1
- data/lib/kafka/protocol/offset_fetch_request.rb +3 -1
- data/lib/kafka/protocol/produce_request.rb +3 -1
- data/lib/kafka/protocol/record_batch.rb +7 -4
- data/lib/kafka/protocol/sasl_handshake_request.rb +1 -1
- data/lib/kafka/protocol/txn_offset_commit_request.rb +46 -0
- data/lib/kafka/protocol/txn_offset_commit_response.rb +18 -0
- data/lib/kafka/protocol.rb +8 -0
- data/lib/kafka/round_robin_assignment_strategy.rb +10 -7
- data/lib/kafka/sasl/gssapi.rb +1 -1
- data/lib/kafka/sasl/oauth.rb +64 -0
- data/lib/kafka/sasl/plain.rb +1 -1
- data/lib/kafka/sasl/scram.rb +16 -13
- data/lib/kafka/sasl_authenticator.rb +10 -3
- data/lib/kafka/snappy_codec.rb +4 -0
- data/lib/kafka/ssl_context.rb +5 -1
- data/lib/kafka/ssl_socket_with_timeout.rb +1 -0
- data/lib/kafka/statsd.rb +10 -1
- data/lib/kafka/tagged_logger.rb +77 -0
- data/lib/kafka/transaction_manager.rb +26 -1
- data/lib/kafka/transaction_state_machine.rb +1 -1
- data/lib/kafka/version.rb +1 -1
- data/lib/kafka/zstd_codec.rb +27 -0
- data/lib/kafka.rb +4 -0
- data/ruby-kafka.gemspec +5 -3
- metadata +50 -7
data/lib/kafka/cluster.rb
CHANGED
@@ -23,7 +23,7 @@ module Kafka
|
|
23
23
|
raise ArgumentError, "At least one seed broker must be configured"
|
24
24
|
end
|
25
25
|
|
26
|
-
@logger = logger
|
26
|
+
@logger = TaggedLogger.new(logger)
|
27
27
|
@seed_brokers = seed_brokers
|
28
28
|
@broker_pool = broker_pool
|
29
29
|
@cluster_info = nil
|
@@ -45,6 +45,10 @@ module Kafka
|
|
45
45
|
new_topics = topics - @target_topics
|
46
46
|
|
47
47
|
unless new_topics.empty?
|
48
|
+
if new_topics.any? { |topic| topic.nil? or topic.empty? }
|
49
|
+
raise ArgumentError, "Topic must not be nil or empty"
|
50
|
+
end
|
51
|
+
|
48
52
|
@logger.info "New topics added to target list: #{new_topics.to_a.join(', ')}"
|
49
53
|
|
50
54
|
@target_topics.merge(new_topics)
|
@@ -139,6 +143,40 @@ module Kafka
|
|
139
143
|
end
|
140
144
|
end
|
141
145
|
|
146
|
+
def describe_configs(broker_id, configs = [])
|
147
|
+
options = {
|
148
|
+
resources: [[Kafka::Protocol::RESOURCE_TYPE_CLUSTER, broker_id.to_s, configs]]
|
149
|
+
}
|
150
|
+
|
151
|
+
info = cluster_info.brokers.find {|broker| broker.node_id == broker_id }
|
152
|
+
broker = @broker_pool.connect(info.host, info.port, node_id: info.node_id)
|
153
|
+
|
154
|
+
response = broker.describe_configs(**options)
|
155
|
+
|
156
|
+
response.resources.each do |resource|
|
157
|
+
Protocol.handle_error(resource.error_code, resource.error_message)
|
158
|
+
end
|
159
|
+
|
160
|
+
response.resources.first.configs
|
161
|
+
end
|
162
|
+
|
163
|
+
def alter_configs(broker_id, configs = [])
|
164
|
+
options = {
|
165
|
+
resources: [[Kafka::Protocol::RESOURCE_TYPE_CLUSTER, broker_id.to_s, configs]]
|
166
|
+
}
|
167
|
+
|
168
|
+
info = cluster_info.brokers.find {|broker| broker.node_id == broker_id }
|
169
|
+
broker = @broker_pool.connect(info.host, info.port, node_id: info.node_id)
|
170
|
+
|
171
|
+
response = broker.alter_configs(**options)
|
172
|
+
|
173
|
+
response.resources.each do |resource|
|
174
|
+
Protocol.handle_error(resource.error_code, resource.error_message)
|
175
|
+
end
|
176
|
+
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
|
142
180
|
def partitions_for(topic)
|
143
181
|
add_target_topics([topic])
|
144
182
|
refresh_metadata_if_necessary!
|
@@ -252,6 +290,20 @@ module Kafka
|
|
252
290
|
group
|
253
291
|
end
|
254
292
|
|
293
|
+
def fetch_group_offsets(group_id)
|
294
|
+
topics = get_group_coordinator(group_id: group_id)
|
295
|
+
.fetch_offsets(group_id: group_id, topics: nil)
|
296
|
+
.topics
|
297
|
+
|
298
|
+
topics.each do |_, partitions|
|
299
|
+
partitions.each do |_, response|
|
300
|
+
Protocol.handle_error(response.error_code)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
topics
|
305
|
+
end
|
306
|
+
|
255
307
|
def create_partitions_for(name, num_partitions:, timeout:)
|
256
308
|
options = {
|
257
309
|
topics: [[name, num_partitions, nil]],
|
data/lib/kafka/compression.rb
CHANGED
@@ -3,27 +3,27 @@
|
|
3
3
|
require "kafka/snappy_codec"
|
4
4
|
require "kafka/gzip_codec"
|
5
5
|
require "kafka/lz4_codec"
|
6
|
+
require "kafka/zstd_codec"
|
6
7
|
|
7
8
|
module Kafka
|
8
9
|
module Compression
|
9
|
-
|
10
|
-
1 => :gzip,
|
11
|
-
2 => :snappy,
|
12
|
-
3 => :lz4,
|
13
|
-
}.freeze
|
14
|
-
|
15
|
-
CODECS = {
|
10
|
+
CODECS_BY_NAME = {
|
16
11
|
:gzip => GzipCodec.new,
|
17
12
|
:snappy => SnappyCodec.new,
|
18
13
|
:lz4 => LZ4Codec.new,
|
14
|
+
:zstd => ZstdCodec.new,
|
19
15
|
}.freeze
|
20
16
|
|
17
|
+
CODECS_BY_ID = CODECS_BY_NAME.each_with_object({}) do |(_, codec), hash|
|
18
|
+
hash[codec.codec_id] = codec
|
19
|
+
end.freeze
|
20
|
+
|
21
21
|
def self.codecs
|
22
|
-
|
22
|
+
CODECS_BY_NAME.keys
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.find_codec(name)
|
26
|
-
codec =
|
26
|
+
codec = CODECS_BY_NAME.fetch(name) do
|
27
27
|
raise "Unknown compression codec #{name}"
|
28
28
|
end
|
29
29
|
|
@@ -33,11 +33,13 @@ module Kafka
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def self.find_codec_by_id(codec_id)
|
36
|
-
|
36
|
+
codec = CODECS_BY_ID.fetch(codec_id) do
|
37
37
|
raise "Unknown codec id #{codec_id}"
|
38
38
|
end
|
39
39
|
|
40
|
-
|
40
|
+
codec.load
|
41
|
+
|
42
|
+
codec
|
41
43
|
end
|
42
44
|
end
|
43
45
|
end
|
data/lib/kafka/compressor.rb
CHANGED
data/lib/kafka/connection.rb
CHANGED
@@ -52,12 +52,15 @@ module Kafka
|
|
52
52
|
# @return [Connection] a new connection.
|
53
53
|
def initialize(host:, port:, client_id:, logger:, instrumenter:, connect_timeout: nil, socket_timeout: nil, ssl_context: nil)
|
54
54
|
@host, @port, @client_id = host, port, client_id
|
55
|
-
@logger = logger
|
55
|
+
@logger = TaggedLogger.new(logger)
|
56
56
|
@instrumenter = instrumenter
|
57
57
|
|
58
58
|
@connect_timeout = connect_timeout || CONNECT_TIMEOUT
|
59
59
|
@socket_timeout = socket_timeout || SOCKET_TIMEOUT
|
60
60
|
@ssl_context = ssl_context
|
61
|
+
|
62
|
+
@socket = nil
|
63
|
+
@last_request = nil
|
61
64
|
end
|
62
65
|
|
63
66
|
def to_s
|
@@ -93,6 +96,7 @@ module Kafka
|
|
93
96
|
|
94
97
|
raise IdleConnection if idle?
|
95
98
|
|
99
|
+
@logger.push_tags(api_name)
|
96
100
|
@instrumenter.instrument("request.connection", notification) do
|
97
101
|
open unless open?
|
98
102
|
|
@@ -113,6 +117,8 @@ module Kafka
|
|
113
117
|
close
|
114
118
|
|
115
119
|
raise ConnectionError, "Connection error #{e.class}: #{e}"
|
120
|
+
ensure
|
121
|
+
@logger.pop_tags
|
116
122
|
end
|
117
123
|
|
118
124
|
private
|
@@ -4,7 +4,7 @@ module Kafka
|
|
4
4
|
class ConnectionBuilder
|
5
5
|
def initialize(client_id:, logger:, instrumenter:, connect_timeout:, socket_timeout:, ssl_context:, sasl_authenticator:)
|
6
6
|
@client_id = client_id
|
7
|
-
@logger = logger
|
7
|
+
@logger = TaggedLogger.new(logger)
|
8
8
|
@instrumenter = instrumenter
|
9
9
|
@connect_timeout = connect_timeout
|
10
10
|
@socket_timeout = socket_timeout
|
data/lib/kafka/consumer.rb
CHANGED
@@ -44,15 +44,16 @@ module Kafka
|
|
44
44
|
#
|
45
45
|
class Consumer
|
46
46
|
|
47
|
-
def initialize(cluster:, logger:, instrumenter:, group:, fetcher:, offset_manager:, session_timeout:, heartbeat:)
|
47
|
+
def initialize(cluster:, logger:, instrumenter:, group:, fetcher:, offset_manager:, session_timeout:, heartbeat:, refresh_topic_interval: 0)
|
48
48
|
@cluster = cluster
|
49
|
-
@logger = logger
|
49
|
+
@logger = TaggedLogger.new(logger)
|
50
50
|
@instrumenter = instrumenter
|
51
51
|
@group = group
|
52
52
|
@offset_manager = offset_manager
|
53
53
|
@session_timeout = session_timeout
|
54
54
|
@fetcher = fetcher
|
55
55
|
@heartbeat = heartbeat
|
56
|
+
@refresh_topic_interval = refresh_topic_interval
|
56
57
|
|
57
58
|
@pauses = Hash.new {|h, k|
|
58
59
|
h[k] = Hash.new {|h2, k2|
|
@@ -73,6 +74,15 @@ module Kafka
|
|
73
74
|
# when user commits message other than last in a batch, this would make ruby-kafka refetch
|
74
75
|
# some already consumed messages
|
75
76
|
@current_offsets = Hash.new { |h, k| h[k] = {} }
|
77
|
+
|
78
|
+
# Map storing subscribed topics with their configuration
|
79
|
+
@subscribed_topics = Concurrent::Map.new
|
80
|
+
|
81
|
+
# Set storing topics that matched topics in @subscribed_topics
|
82
|
+
@matched_topics = Set.new
|
83
|
+
|
84
|
+
# Whether join_group must be executed again because new topics are added
|
85
|
+
@join_group_for_new_topics = false
|
76
86
|
end
|
77
87
|
|
78
88
|
# Subscribes the consumer to a topic.
|
@@ -82,7 +92,8 @@ module Kafka
|
|
82
92
|
# messages to be written. In the former case, set `start_from_beginning`
|
83
93
|
# to true (the default); in the latter, set it to false.
|
84
94
|
#
|
85
|
-
# @param
|
95
|
+
# @param topic_or_regex [String, Regexp] subscribe to single topic with a string
|
96
|
+
# or multiple topics matching a regex.
|
86
97
|
# @param default_offset [Symbol] whether to start from the beginning or the
|
87
98
|
# end of the topic's partitions. Deprecated.
|
88
99
|
# @param start_from_beginning [Boolean] whether to start from the beginning
|
@@ -93,12 +104,15 @@ module Kafka
|
|
93
104
|
# @param max_bytes_per_partition [Integer] the maximum amount of data fetched
|
94
105
|
# from a single partition at a time.
|
95
106
|
# @return [nil]
|
96
|
-
def subscribe(
|
107
|
+
def subscribe(topic_or_regex, default_offset: nil, start_from_beginning: true, max_bytes_per_partition: 1048576)
|
97
108
|
default_offset ||= start_from_beginning ? :earliest : :latest
|
98
109
|
|
99
|
-
@
|
100
|
-
|
101
|
-
|
110
|
+
@subscribed_topics[topic_or_regex] = {
|
111
|
+
default_offset: default_offset,
|
112
|
+
start_from_beginning: start_from_beginning,
|
113
|
+
max_bytes_per_partition: max_bytes_per_partition
|
114
|
+
}
|
115
|
+
scan_for_subscribing
|
102
116
|
|
103
117
|
nil
|
104
118
|
end
|
@@ -111,7 +125,6 @@ module Kafka
|
|
111
125
|
def stop
|
112
126
|
@running = false
|
113
127
|
@fetcher.stop
|
114
|
-
@cluster.disconnect
|
115
128
|
end
|
116
129
|
|
117
130
|
# Pause processing of a specific topic partition.
|
@@ -241,7 +254,7 @@ module Kafka
|
|
241
254
|
|
242
255
|
trigger_heartbeat
|
243
256
|
|
244
|
-
return if
|
257
|
+
return if shutting_down?
|
245
258
|
end
|
246
259
|
|
247
260
|
# We've successfully processed a batch from the partition, so we can clear
|
@@ -280,6 +293,9 @@ module Kafka
|
|
280
293
|
# without an exception. Once marked successful, the offsets of processed
|
281
294
|
# messages can be committed to Kafka.
|
282
295
|
# @yieldparam batch [Kafka::FetchedBatch] a message batch fetched from Kafka.
|
296
|
+
# @raise [Kafka::ProcessingError] if there was an error processing a batch.
|
297
|
+
# The original exception will be returned by calling `#cause` on the
|
298
|
+
# {Kafka::ProcessingError} instance.
|
283
299
|
# @return [nil]
|
284
300
|
def each_batch(min_bytes: 1, max_bytes: 10485760, max_wait_time: 1, automatically_mark_as_processed: true)
|
285
301
|
@fetcher.configure(
|
@@ -300,6 +316,7 @@ module Kafka
|
|
300
316
|
topic: batch.topic,
|
301
317
|
partition: batch.partition,
|
302
318
|
last_offset: batch.last_offset,
|
319
|
+
last_create_time: batch.messages.last.try(:create_time),
|
303
320
|
offset_lag: batch.offset_lag,
|
304
321
|
highwater_mark_offset: batch.highwater_mark_offset,
|
305
322
|
message_count: batch.messages.count,
|
@@ -336,7 +353,7 @@ module Kafka
|
|
336
353
|
|
337
354
|
trigger_heartbeat
|
338
355
|
|
339
|
-
return if
|
356
|
+
return if shutting_down?
|
340
357
|
end
|
341
358
|
|
342
359
|
# We may not have received any messages, but it's still a good idea to
|
@@ -386,22 +403,24 @@ module Kafka
|
|
386
403
|
|
387
404
|
def consumer_loop
|
388
405
|
@running = true
|
406
|
+
@logger.push_tags(@group.to_s)
|
389
407
|
|
390
408
|
@fetcher.start
|
391
409
|
|
392
|
-
while
|
410
|
+
while running?
|
393
411
|
begin
|
394
412
|
@instrumenter.instrument("loop.consumer") do
|
413
|
+
refresh_topic_list_if_enabled
|
395
414
|
yield
|
396
415
|
end
|
397
416
|
rescue HeartbeatError
|
398
417
|
make_final_offsets_commit!
|
399
|
-
join_group
|
418
|
+
join_group if running?
|
400
419
|
rescue OffsetCommitError
|
401
|
-
join_group
|
420
|
+
join_group if running?
|
402
421
|
rescue RebalanceInProgress
|
403
422
|
@logger.warn "Group rebalance in progress, re-joining..."
|
404
|
-
join_group
|
423
|
+
join_group if running?
|
405
424
|
rescue FetchError, NotLeaderForPartition, UnknownTopicOrPartition
|
406
425
|
@cluster.mark_as_stale!
|
407
426
|
rescue LeaderNotAvailable => e
|
@@ -423,7 +442,9 @@ module Kafka
|
|
423
442
|
# important that members explicitly tell Kafka when they're leaving.
|
424
443
|
make_final_offsets_commit!
|
425
444
|
@group.leave rescue nil
|
445
|
+
@cluster.disconnect
|
426
446
|
@running = false
|
447
|
+
@logger.pop_tags
|
427
448
|
end
|
428
449
|
|
429
450
|
def make_final_offsets_commit!(attempts = 3)
|
@@ -442,6 +463,8 @@ module Kafka
|
|
442
463
|
end
|
443
464
|
|
444
465
|
def join_group
|
466
|
+
@join_group_for_new_topics = false
|
467
|
+
|
445
468
|
old_generation_id = @group.generation_id
|
446
469
|
|
447
470
|
@group.join
|
@@ -503,11 +526,19 @@ module Kafka
|
|
503
526
|
end
|
504
527
|
end
|
505
528
|
|
529
|
+
def refresh_topic_list_if_enabled
|
530
|
+
return if @refresh_topic_interval <= 0
|
531
|
+
return if @refreshed_at && @refreshed_at + @refresh_topic_interval > Time.now
|
532
|
+
|
533
|
+
scan_for_subscribing
|
534
|
+
@refreshed_at = Time.now
|
535
|
+
end
|
536
|
+
|
506
537
|
def fetch_batches
|
507
538
|
# Return early if the consumer has been stopped.
|
508
|
-
return [] if
|
539
|
+
return [] if shutting_down?
|
509
540
|
|
510
|
-
join_group
|
541
|
+
join_group if !@group.member? || @join_group_for_new_topics
|
511
542
|
|
512
543
|
trigger_heartbeat
|
513
544
|
|
@@ -515,7 +546,7 @@ module Kafka
|
|
515
546
|
|
516
547
|
if !@fetcher.data?
|
517
548
|
@logger.debug "No batches to process"
|
518
|
-
sleep 2
|
549
|
+
sleep(@fetcher.max_wait_time || 2)
|
519
550
|
[]
|
520
551
|
else
|
521
552
|
tag, message = @fetcher.poll
|
@@ -545,6 +576,14 @@ module Kafka
|
|
545
576
|
@pauses[topic][partition]
|
546
577
|
end
|
547
578
|
|
579
|
+
def running?
|
580
|
+
@running
|
581
|
+
end
|
582
|
+
|
583
|
+
def shutting_down?
|
584
|
+
!running?
|
585
|
+
end
|
586
|
+
|
548
587
|
def clear_current_offsets(excluding: {})
|
549
588
|
@current_offsets.each do |topic, partitions|
|
550
589
|
partitions.keep_if do |partition, _|
|
@@ -552,5 +591,47 @@ module Kafka
|
|
552
591
|
end
|
553
592
|
end
|
554
593
|
end
|
594
|
+
|
595
|
+
def scan_for_subscribing
|
596
|
+
@subscribed_topics.each do |topic_or_regex, config|
|
597
|
+
default_offset = config.fetch(:default_offset)
|
598
|
+
start_from_beginning = config.fetch(:start_from_beginning)
|
599
|
+
max_bytes_per_partition = config.fetch(:max_bytes_per_partition)
|
600
|
+
if topic_or_regex.is_a?(Regexp)
|
601
|
+
subscribe_to_regex(topic_or_regex, default_offset, start_from_beginning, max_bytes_per_partition)
|
602
|
+
else
|
603
|
+
subscribe_to_topic(topic_or_regex, default_offset, start_from_beginning, max_bytes_per_partition)
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
def subscribe_to_regex(topic_regex, default_offset, start_from_beginning, max_bytes_per_partition)
|
609
|
+
cluster_topics.select { |topic| topic =~ topic_regex }.each do |topic|
|
610
|
+
subscribe_to_topic(topic, default_offset, start_from_beginning, max_bytes_per_partition)
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def subscribe_to_topic(topic, default_offset, start_from_beginning, max_bytes_per_partition)
|
615
|
+
return if @matched_topics.include?(topic)
|
616
|
+
@matched_topics.add(topic)
|
617
|
+
@join_group_for_new_topics = true
|
618
|
+
|
619
|
+
@group.subscribe(topic)
|
620
|
+
@offset_manager.set_default_offset(topic, default_offset)
|
621
|
+
@fetcher.subscribe(topic, max_bytes_per_partition: max_bytes_per_partition)
|
622
|
+
@cluster.mark_as_stale!
|
623
|
+
end
|
624
|
+
|
625
|
+
def cluster_topics
|
626
|
+
attempts = 0
|
627
|
+
begin
|
628
|
+
attempts += 1
|
629
|
+
@cluster.list_topics
|
630
|
+
rescue Kafka::ConnectionError
|
631
|
+
@cluster.mark_as_stale!
|
632
|
+
retry unless attempts > 1
|
633
|
+
raise
|
634
|
+
end
|
635
|
+
end
|
555
636
|
end
|
556
637
|
end
|
data/lib/kafka/consumer_group.rb
CHANGED
@@ -7,11 +7,12 @@ module Kafka
|
|
7
7
|
class ConsumerGroup
|
8
8
|
attr_reader :assigned_partitions, :generation_id, :group_id
|
9
9
|
|
10
|
-
def initialize(cluster:, logger:, group_id:, session_timeout:, retention_time:, instrumenter:)
|
10
|
+
def initialize(cluster:, logger:, group_id:, session_timeout:, rebalance_timeout:, retention_time:, instrumenter:)
|
11
11
|
@cluster = cluster
|
12
|
-
@logger = logger
|
12
|
+
@logger = TaggedLogger.new(logger)
|
13
13
|
@group_id = group_id
|
14
14
|
@session_timeout = session_timeout
|
15
|
+
@rebalance_timeout = rebalance_timeout
|
15
16
|
@instrumenter = instrumenter
|
16
17
|
@member_id = ""
|
17
18
|
@generation_id = nil
|
@@ -122,6 +123,15 @@ module Kafka
|
|
122
123
|
retry
|
123
124
|
end
|
124
125
|
|
126
|
+
def to_s
|
127
|
+
"[#{@group_id}] {" + assigned_partitions.map { |topic, partitions|
|
128
|
+
partition_str = partitions.size > 5 ?
|
129
|
+
"#{partitions[0..4].join(', ')}..." :
|
130
|
+
partitions.join(', ')
|
131
|
+
"#{topic}: #{partition_str}"
|
132
|
+
}.join('; ') + '}:'
|
133
|
+
end
|
134
|
+
|
125
135
|
private
|
126
136
|
|
127
137
|
def join_group
|
@@ -131,7 +141,9 @@ module Kafka
|
|
131
141
|
response = coordinator.join_group(
|
132
142
|
group_id: @group_id,
|
133
143
|
session_timeout: @session_timeout,
|
144
|
+
rebalance_timeout: @rebalance_timeout,
|
134
145
|
member_id: @member_id,
|
146
|
+
topics: @topics,
|
135
147
|
)
|
136
148
|
|
137
149
|
Protocol.handle_error(response.error_code)
|
@@ -149,6 +161,12 @@ module Kafka
|
|
149
161
|
@member_id = ""
|
150
162
|
sleep 1
|
151
163
|
|
164
|
+
retry
|
165
|
+
rescue CoordinatorLoadInProgress
|
166
|
+
@logger.error "Coordinator broker still loading, retrying in 1s..."
|
167
|
+
|
168
|
+
sleep 1
|
169
|
+
|
152
170
|
retry
|
153
171
|
end
|
154
172
|
|
data/lib/kafka/datadog.rb
CHANGED
@@ -31,7 +31,7 @@ module Kafka
|
|
31
31
|
|
32
32
|
class << self
|
33
33
|
def statsd
|
34
|
-
@statsd ||= ::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags)
|
34
|
+
@statsd ||= ::Datadog::Statsd.new(host, port, namespace: namespace, tags: tags, socket_path: socket_path)
|
35
35
|
end
|
36
36
|
|
37
37
|
def statsd=(statsd)
|
@@ -40,7 +40,7 @@ module Kafka
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def host
|
43
|
-
@host
|
43
|
+
@host
|
44
44
|
end
|
45
45
|
|
46
46
|
def host=(host)
|
@@ -49,7 +49,7 @@ module Kafka
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def port
|
52
|
-
@port
|
52
|
+
@port
|
53
53
|
end
|
54
54
|
|
55
55
|
def port=(port)
|
@@ -57,6 +57,15 @@ module Kafka
|
|
57
57
|
clear
|
58
58
|
end
|
59
59
|
|
60
|
+
def socket_path
|
61
|
+
@socket_path
|
62
|
+
end
|
63
|
+
|
64
|
+
def socket_path=(socket_path)
|
65
|
+
@socket_path = socket_path
|
66
|
+
clear
|
67
|
+
end
|
68
|
+
|
60
69
|
def namespace
|
61
70
|
@namespace ||= STATSD_NAMESPACE
|
62
71
|
end
|
@@ -77,14 +86,6 @@ module Kafka
|
|
77
86
|
|
78
87
|
private
|
79
88
|
|
80
|
-
def default_host
|
81
|
-
::Datadog::Statsd.const_defined?(:Connection) ? ::Datadog::Statsd::Connection::DEFAULT_HOST : ::Datadog::Statsd::DEFAULT_HOST
|
82
|
-
end
|
83
|
-
|
84
|
-
def default_port
|
85
|
-
::Datadog::Statsd.const_defined?(:Connection) ? ::Datadog::Statsd::Connection::DEFAULT_PORT : ::Datadog::Statsd::DEFAULT_PORT
|
86
|
-
end
|
87
|
-
|
88
89
|
def clear
|
89
90
|
@statsd && @statsd.close
|
90
91
|
@statsd = nil
|
@@ -167,8 +168,9 @@ module Kafka
|
|
167
168
|
|
168
169
|
def process_batch(event)
|
169
170
|
offset = event.payload.fetch(:last_offset)
|
170
|
-
lag = event.payload.fetch(:offset_lag)
|
171
171
|
messages = event.payload.fetch(:message_count)
|
172
|
+
create_time = event.payload.fetch(:last_create_time)
|
173
|
+
time_lag = create_time && ((Time.now - create_time) * 1000).to_i
|
172
174
|
|
173
175
|
tags = {
|
174
176
|
client: event.payload.fetch(:client_id),
|
@@ -185,6 +187,24 @@ module Kafka
|
|
185
187
|
end
|
186
188
|
|
187
189
|
gauge("consumer.offset", offset, tags: tags)
|
190
|
+
|
191
|
+
if time_lag
|
192
|
+
gauge("consumer.time_lag", time_lag, tags: tags)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def fetch_batch(event)
|
197
|
+
lag = event.payload.fetch(:offset_lag)
|
198
|
+
batch_size = event.payload.fetch(:message_count)
|
199
|
+
|
200
|
+
tags = {
|
201
|
+
client: event.payload.fetch(:client_id),
|
202
|
+
group_id: event.payload.fetch(:group_id),
|
203
|
+
topic: event.payload.fetch(:topic),
|
204
|
+
partition: event.payload.fetch(:partition),
|
205
|
+
}
|
206
|
+
|
207
|
+
histogram("consumer.batch_size", batch_size, tags: tags)
|
188
208
|
gauge("consumer.lag", lag, tags: tags)
|
189
209
|
end
|
190
210
|
|
@@ -23,7 +23,7 @@ module Kafka
|
|
23
23
|
class FetchOperation
|
24
24
|
def initialize(cluster:, logger:, min_bytes: 1, max_bytes: 10485760, max_wait_time: 5)
|
25
25
|
@cluster = cluster
|
26
|
-
@logger = logger
|
26
|
+
@logger = TaggedLogger.new(logger)
|
27
27
|
@min_bytes = min_bytes
|
28
28
|
@max_bytes = max_bytes
|
29
29
|
@max_wait_time = max_wait_time
|
data/lib/kafka/fetched_batch.rb
CHANGED
@@ -13,18 +13,22 @@ module Kafka
|
|
13
13
|
# @return [Integer]
|
14
14
|
attr_reader :last_offset
|
15
15
|
|
16
|
+
# @return [Integer]
|
17
|
+
attr_reader :leader_epoch
|
18
|
+
|
16
19
|
# @return [Integer] the offset of the most recent message in the partition.
|
17
20
|
attr_reader :highwater_mark_offset
|
18
21
|
|
19
22
|
# @return [Array<Kafka::FetchedMessage>]
|
20
23
|
attr_accessor :messages
|
21
24
|
|
22
|
-
def initialize(topic:, partition:, highwater_mark_offset:, messages:, last_offset: nil)
|
25
|
+
def initialize(topic:, partition:, highwater_mark_offset:, messages:, last_offset: nil, leader_epoch: nil)
|
23
26
|
@topic = topic
|
24
27
|
@partition = partition
|
25
28
|
@highwater_mark_offset = highwater_mark_offset
|
26
29
|
@messages = messages
|
27
30
|
@last_offset = last_offset
|
31
|
+
@leader_epoch = leader_epoch
|
28
32
|
end
|
29
33
|
|
30
34
|
def empty?
|
@@ -10,7 +10,7 @@ module Kafka
|
|
10
10
|
def initialize(topic, fetched_partition, offset, logger:)
|
11
11
|
@topic = topic
|
12
12
|
@fetched_partition = fetched_partition
|
13
|
-
@logger = logger
|
13
|
+
@logger = TaggedLogger.new(logger)
|
14
14
|
@offset = offset
|
15
15
|
end
|
16
16
|
|
@@ -48,7 +48,7 @@ module Kafka
|
|
48
48
|
partition: @fetched_partition.partition
|
49
49
|
)
|
50
50
|
end
|
51
|
-
end
|
51
|
+
end.compact
|
52
52
|
end
|
53
53
|
FetchedBatch.new(
|
54
54
|
topic: @topic,
|
@@ -62,11 +62,13 @@ module Kafka
|
|
62
62
|
def extract_records
|
63
63
|
records = []
|
64
64
|
last_offset = nil
|
65
|
+
leader_epoch = nil
|
65
66
|
aborted_transactions = @fetched_partition.aborted_transactions.sort_by(&:first_offset)
|
66
67
|
aborted_producer_ids = {}
|
67
68
|
|
68
69
|
@fetched_partition.messages.each do |record_batch|
|
69
70
|
last_offset = record_batch.last_offset if last_offset.nil? || last_offset < record_batch.last_offset
|
71
|
+
leader_epoch = record_batch.partition_leader_epoch if leader_epoch.nil? || leader_epoch < record_batch.partition_leader_epoch
|
70
72
|
# Find the list of aborted producer IDs less than current offset
|
71
73
|
unless aborted_transactions.empty?
|
72
74
|
if aborted_transactions.first.first_offset <= record_batch.last_offset
|
@@ -99,6 +101,7 @@ module Kafka
|
|
99
101
|
topic: @topic,
|
100
102
|
partition: @fetched_partition.partition,
|
101
103
|
last_offset: last_offset,
|
104
|
+
leader_epoch: leader_epoch,
|
102
105
|
highwater_mark_offset: @fetched_partition.highwater_mark_offset,
|
103
106
|
messages: records
|
104
107
|
)
|