ruby-kafka 0.7.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
)
|