rdkafka 0.28.0 → 0.29.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/CHANGELOG.md +33 -0
- data/README.md +1 -0
- data/dist/{librdkafka-2.14.1.tar.gz → librdkafka-2.14.2.tar.gz} +0 -0
- data/docker-compose-ssl.yml +1 -1
- data/docker-compose.yml +1 -1
- data/lib/rdkafka/abstract_handle.rb +31 -2
- data/lib/rdkafka/admin/config_binding_result.rb +2 -2
- data/lib/rdkafka/admin/create_acl_handle.rb +6 -5
- data/lib/rdkafka/admin/create_partitions_handle.rb +4 -6
- data/lib/rdkafka/admin/create_topic_handle.rb +4 -6
- data/lib/rdkafka/admin/delete_acl_handle.rb +5 -7
- data/lib/rdkafka/admin/delete_groups_handle.rb +4 -6
- data/lib/rdkafka/admin/delete_topic_handle.rb +4 -6
- data/lib/rdkafka/admin/describe_acl_handle.rb +5 -7
- data/lib/rdkafka/admin/describe_configs_handle.rb +4 -10
- data/lib/rdkafka/admin/describe_configs_report.rb +1 -5
- data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +4 -10
- data/lib/rdkafka/admin/incremental_alter_configs_report.rb +1 -5
- data/lib/rdkafka/admin/list_offsets_handle.rb +5 -10
- data/lib/rdkafka/admin.rb +75 -43
- data/lib/rdkafka/bindings.rb +9 -4
- data/lib/rdkafka/callbacks/base_handler.rb +62 -0
- data/lib/rdkafka/callbacks/create_acl_handler.rb +37 -0
- data/lib/rdkafka/callbacks/create_partitions_handler.rb +37 -0
- data/lib/rdkafka/callbacks/create_topic_handler.rb +37 -0
- data/lib/rdkafka/callbacks/delete_acl_handler.rb +42 -0
- data/lib/rdkafka/callbacks/delete_groups_handler.rb +37 -0
- data/lib/rdkafka/callbacks/delete_topic_handler.rb +37 -0
- data/lib/rdkafka/callbacks/describe_acl_handler.rb +35 -0
- data/lib/rdkafka/callbacks/describe_configs_handler.rb +42 -0
- data/lib/rdkafka/callbacks/incremental_alter_configs_handler.rb +42 -0
- data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
- data/lib/rdkafka/callbacks.rb +50 -244
- data/lib/rdkafka/config.rb +45 -33
- data/lib/rdkafka/consumer/headers.rb +19 -5
- data/lib/rdkafka/consumer/partition.rb +8 -1
- data/lib/rdkafka/consumer/topic_partition_list.rb +44 -22
- data/lib/rdkafka/consumer.rb +111 -21
- data/lib/rdkafka/defaults.rb +19 -0
- data/lib/rdkafka/helpers/metadata.rb +29 -0
- data/lib/rdkafka/metadata.rb +86 -19
- data/lib/rdkafka/native_kafka.rb +2 -4
- data/lib/rdkafka/producer/delivery_handle.rb +3 -3
- data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
- data/lib/rdkafka/producer.rb +56 -44
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +12 -0
- data/package-lock.json +3 -3
- data/rdkafka.gemspec +1 -0
- metadata +14 -5
- data/Gemfile +0 -13
- data/Gemfile.lint +0 -14
- data/Gemfile.lint.lock +0 -123
|
@@ -118,7 +118,12 @@ module Rdkafka
|
|
|
118
118
|
else
|
|
119
119
|
elem[:offset]
|
|
120
120
|
end
|
|
121
|
-
partition = Partition.new(
|
|
121
|
+
partition = Partition.new(
|
|
122
|
+
elem[:partition],
|
|
123
|
+
offset,
|
|
124
|
+
elem[:err],
|
|
125
|
+
elem[:metadata].null? ? nil : elem[:metadata].read_string(elem[:metadata_size])
|
|
126
|
+
)
|
|
122
127
|
partitions.push(partition)
|
|
123
128
|
data[elem[:topic]] = partitions
|
|
124
129
|
end
|
|
@@ -137,33 +142,50 @@ module Rdkafka
|
|
|
137
142
|
def to_native_tpl
|
|
138
143
|
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
|
|
139
144
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
partitions
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
topic,
|
|
146
|
-
p.partition
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
if p.offset
|
|
150
|
-
offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
|
|
151
|
-
|
|
152
|
-
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
|
|
145
|
+
begin
|
|
146
|
+
@data.each do |topic, partitions|
|
|
147
|
+
if partitions
|
|
148
|
+
partitions.each do |p|
|
|
149
|
+
ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
153
150
|
tpl,
|
|
154
151
|
topic,
|
|
155
|
-
p.partition
|
|
156
|
-
offset
|
|
152
|
+
p.partition
|
|
157
153
|
)
|
|
154
|
+
|
|
155
|
+
if p.metadata
|
|
156
|
+
part = Rdkafka::Bindings::TopicPartition.new(ref)
|
|
157
|
+
str_ptr = FFI::MemoryPointer.from_string(p.metadata)
|
|
158
|
+
# The metadata string is owned by librdkafka once handed over and released here:
|
|
159
|
+
# https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
|
|
160
|
+
str_ptr.autorelease = false
|
|
161
|
+
part[:metadata] = str_ptr
|
|
162
|
+
part[:metadata_size] = p.metadata.bytesize
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if p.offset
|
|
166
|
+
offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
|
|
167
|
+
|
|
168
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
|
|
169
|
+
tpl,
|
|
170
|
+
topic,
|
|
171
|
+
p.partition,
|
|
172
|
+
offset
|
|
173
|
+
)
|
|
174
|
+
end
|
|
158
175
|
end
|
|
176
|
+
else
|
|
177
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
178
|
+
tpl,
|
|
179
|
+
topic,
|
|
180
|
+
Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
181
|
+
)
|
|
159
182
|
end
|
|
160
|
-
else
|
|
161
|
-
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
162
|
-
tpl,
|
|
163
|
-
topic,
|
|
164
|
-
Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
165
|
-
)
|
|
166
183
|
end
|
|
184
|
+
rescue Exception
|
|
185
|
+
# The native list is caller-owned until it is handed to librdkafka. If population raises
|
|
186
|
+
# partway (e.g. an invalid partition), destroy it here so it is not leaked.
|
|
187
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
188
|
+
raise
|
|
167
189
|
end
|
|
168
190
|
|
|
169
191
|
tpl
|
data/lib/rdkafka/consumer.rb
CHANGED
|
@@ -14,14 +14,45 @@ module Rdkafka
|
|
|
14
14
|
include Enumerable
|
|
15
15
|
include Helpers::Time
|
|
16
16
|
include Helpers::OAuth
|
|
17
|
+
include Helpers::Metadata
|
|
17
18
|
|
|
18
19
|
# @private
|
|
19
20
|
# @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
|
|
20
21
|
def initialize(native_kafka)
|
|
21
22
|
@native_kafka = native_kafka
|
|
23
|
+
# Single-element holder shared with the GC finalizer so it can destroy the lazily created
|
|
24
|
+
# consumer queue without capturing `self` (capturing the consumer in its own finalizer would
|
|
25
|
+
# pin it and prevent it from ever being collected).
|
|
26
|
+
@consumer_queue_holder = []
|
|
27
|
+
|
|
28
|
+
# Makes sure the consumer is closed (consumer queue destroyed and native client destroyed)
|
|
29
|
+
# before it gets GCed by Ruby.
|
|
30
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(native_kafka, @consumer_queue_holder))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Builds the GC finalizer for a consumer. It mirrors {#close}: close the consumer, destroy the
|
|
34
|
+
# consumer-queue reference, then destroy the native client. The default `NativeKafka#finalizer`
|
|
35
|
+
# went straight to `rd_kafka_destroy`, leaving the consumer-queue reference (from
|
|
36
|
+
# `rd_kafka_queue_get_consumer`, taken by `poll_batch`) dangling - which can make
|
|
37
|
+
# `rd_kafka_destroy` block inside the finalizer (process hang at GC/shutdown) or leak the handle.
|
|
38
|
+
#
|
|
39
|
+
# @private
|
|
40
|
+
# @param native_kafka [NativeKafka] the wrapped native client
|
|
41
|
+
# @param queue_holder [Array] single-element holder carrying the consumer queue pointer (or empty)
|
|
42
|
+
# @return [Proc] finalizer proc that must not reference the consumer instance
|
|
43
|
+
def self.finalizer(native_kafka, queue_holder)
|
|
44
|
+
proc do
|
|
45
|
+
next if native_kafka.closed?
|
|
46
|
+
|
|
47
|
+
native_kafka.synchronize do |inner|
|
|
48
|
+
Rdkafka::Bindings.rd_kafka_consumer_close(inner)
|
|
49
|
+
|
|
50
|
+
queue = queue_holder[0]
|
|
51
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue) if queue
|
|
52
|
+
end
|
|
22
53
|
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
native_kafka.close
|
|
55
|
+
end
|
|
25
56
|
end
|
|
26
57
|
|
|
27
58
|
# Starts the native Kafka polling thread and kicks off the init polling
|
|
@@ -187,6 +218,7 @@ module Rdkafka
|
|
|
187
218
|
if @consumer_queue
|
|
188
219
|
Rdkafka::Bindings.rd_kafka_queue_destroy(@consumer_queue)
|
|
189
220
|
@consumer_queue = nil
|
|
221
|
+
@consumer_queue_holder[0] = nil
|
|
190
222
|
end
|
|
191
223
|
end
|
|
192
224
|
|
|
@@ -422,6 +454,8 @@ module Rdkafka
|
|
|
422
454
|
#
|
|
423
455
|
# @raise [RdkafkaError] When getting the positions fails.
|
|
424
456
|
def position(list = nil)
|
|
457
|
+
closed_consumer_check(__method__)
|
|
458
|
+
|
|
425
459
|
if list.nil?
|
|
426
460
|
list = assignment
|
|
427
461
|
elsif !list.is_a?(TopicPartitionList)
|
|
@@ -490,8 +524,7 @@ module Rdkafka
|
|
|
490
524
|
out = {}
|
|
491
525
|
|
|
492
526
|
topic_partition_list.to_h.each do |topic, partitions|
|
|
493
|
-
# Query high watermarks for this topic's partitions
|
|
494
|
-
# and compare to the offset in the list.
|
|
527
|
+
# Query high watermarks for this topic's partitions and compare to the offset in the list.
|
|
495
528
|
topic_out = {}
|
|
496
529
|
partitions.each do |p|
|
|
497
530
|
next if p.offset.nil?
|
|
@@ -509,11 +542,12 @@ module Rdkafka
|
|
|
509
542
|
|
|
510
543
|
# Returns the ClusterId as reported in broker metadata.
|
|
511
544
|
#
|
|
545
|
+
# @param timeout_ms [Integer] timeout in milliseconds to wait for the cluster id
|
|
512
546
|
# @return [String, nil]
|
|
513
|
-
def cluster_id
|
|
547
|
+
def cluster_id(timeout_ms = Defaults::CONSUMER_CLUSTER_ID_TIMEOUT_MS)
|
|
514
548
|
closed_consumer_check(__method__)
|
|
515
549
|
@native_kafka.with_inner do |inner|
|
|
516
|
-
Rdkafka::Bindings.rd_kafka_clusterid(inner)
|
|
550
|
+
read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_clusterid(inner, timeout_ms))
|
|
517
551
|
end
|
|
518
552
|
end
|
|
519
553
|
|
|
@@ -525,7 +559,7 @@ module Rdkafka
|
|
|
525
559
|
def member_id
|
|
526
560
|
closed_consumer_check(__method__)
|
|
527
561
|
@native_kafka.with_inner do |inner|
|
|
528
|
-
Rdkafka::Bindings.rd_kafka_memberid(inner)
|
|
562
|
+
read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_memberid(inner))
|
|
529
563
|
end
|
|
530
564
|
end
|
|
531
565
|
|
|
@@ -534,16 +568,34 @@ module Rdkafka
|
|
|
534
568
|
# When using this `enable.auto.offset.store` should be set to `false` in the config.
|
|
535
569
|
#
|
|
536
570
|
# @param message [Rdkafka::Consumer::Message] The message which offset will be stored
|
|
571
|
+
# @param metadata [String, nil] commit metadata string to store alongside the offset
|
|
537
572
|
# @return [nil]
|
|
538
573
|
# @raise [RdkafkaError] When storing the offset fails
|
|
539
|
-
def store_offset(message)
|
|
574
|
+
def store_offset(message, metadata = nil)
|
|
540
575
|
closed_consumer_check(__method__)
|
|
541
576
|
|
|
542
577
|
list = TopicPartitionList.new
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
578
|
+
|
|
579
|
+
# For metadata aware commits we build the partition reference directly to save on
|
|
580
|
+
# objects allocations
|
|
581
|
+
if metadata
|
|
582
|
+
list.add_topic_and_partitions_with_offsets(
|
|
583
|
+
message.topic,
|
|
584
|
+
[
|
|
585
|
+
Consumer::Partition.new(
|
|
586
|
+
message.partition,
|
|
587
|
+
message.offset + 1,
|
|
588
|
+
0,
|
|
589
|
+
metadata
|
|
590
|
+
)
|
|
591
|
+
]
|
|
592
|
+
)
|
|
593
|
+
else
|
|
594
|
+
list.add_topic_and_partitions_with_offsets(
|
|
595
|
+
message.topic,
|
|
596
|
+
message.partition => message.offset + 1
|
|
597
|
+
)
|
|
598
|
+
end
|
|
547
599
|
|
|
548
600
|
tpl = list.to_native_tpl
|
|
549
601
|
|
|
@@ -572,8 +624,7 @@ module Rdkafka
|
|
|
572
624
|
end
|
|
573
625
|
|
|
574
626
|
# Seek to a particular message by providing the topic, partition and offset.
|
|
575
|
-
# The next poll on the topic/partition will return the
|
|
576
|
-
# message at the given offset.
|
|
627
|
+
# The next poll on the topic/partition will return the message at the given offset.
|
|
577
628
|
#
|
|
578
629
|
# @param topic [String] The topic in which to seek
|
|
579
630
|
# @param partition [Integer] The partition number to seek
|
|
@@ -641,8 +692,7 @@ module Rdkafka
|
|
|
641
692
|
|
|
642
693
|
# Manually commit the current offsets of this consumer.
|
|
643
694
|
#
|
|
644
|
-
# To use this set `enable.auto.commit`to `false` to disable automatic triggering
|
|
645
|
-
# of commits.
|
|
695
|
+
# To use this set `enable.auto.commit`to `false` to disable automatic triggering of commits.
|
|
646
696
|
#
|
|
647
697
|
# If `enable.auto.offset.store` is set to `true` the offset of the last consumed
|
|
648
698
|
# message for every partition is used. If set to `false` you can use {store_offset} to
|
|
@@ -841,8 +891,17 @@ module Rdkafka
|
|
|
841
891
|
next
|
|
842
892
|
end
|
|
843
893
|
|
|
844
|
-
|
|
845
|
-
|
|
894
|
+
begin
|
|
895
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
896
|
+
rescue Rdkafka::RdkafkaError => e
|
|
897
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
898
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
899
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
900
|
+
results << e
|
|
901
|
+
ensure
|
|
902
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
903
|
+
end
|
|
904
|
+
|
|
846
905
|
i += 1
|
|
847
906
|
end
|
|
848
907
|
ensure
|
|
@@ -907,8 +966,17 @@ module Rdkafka
|
|
|
907
966
|
next
|
|
908
967
|
end
|
|
909
968
|
|
|
910
|
-
|
|
911
|
-
|
|
969
|
+
begin
|
|
970
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
971
|
+
rescue Rdkafka::RdkafkaError => e
|
|
972
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
973
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
974
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
975
|
+
results << e
|
|
976
|
+
ensure
|
|
977
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
978
|
+
end
|
|
979
|
+
|
|
912
980
|
i += 1
|
|
913
981
|
end
|
|
914
982
|
ensure
|
|
@@ -994,12 +1062,34 @@ module Rdkafka
|
|
|
994
1062
|
def closed_consumer_check(method)
|
|
995
1063
|
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
|
996
1064
|
end
|
|
1065
|
+
alias_method :closed_check, :closed_consumer_check
|
|
1066
|
+
|
|
1067
|
+
# Reads a librdkafka-allocated string and frees the underlying native buffer.
|
|
1068
|
+
#
|
|
1069
|
+
# librdkafka returns heap-allocated, caller-owned strings from functions like
|
|
1070
|
+
# `rd_kafka_clusterid`/`rd_kafka_memberid`. The buffer must be released with
|
|
1071
|
+
# `rd_kafka_mem_free`, otherwise it leaks on every call.
|
|
1072
|
+
#
|
|
1073
|
+
# @param inner [FFI::Pointer] the native client handle used to allocate the string
|
|
1074
|
+
# @param ptr [FFI::Pointer] the native string pointer (may be null)
|
|
1075
|
+
# @return [String, nil] the copied Ruby string, or nil when the pointer is null
|
|
1076
|
+
def read_and_free_native_string(inner, ptr)
|
|
1077
|
+
return nil if ptr.null?
|
|
1078
|
+
|
|
1079
|
+
ptr.read_string
|
|
1080
|
+
ensure
|
|
1081
|
+
Rdkafka::Bindings.rd_kafka_mem_free(inner, ptr) unless ptr.null?
|
|
1082
|
+
end
|
|
997
1083
|
|
|
998
1084
|
# Returns the consumer queue pointer, lazily initialized
|
|
999
1085
|
# @return [FFI::Pointer] consumer queue handle
|
|
1000
1086
|
def consumer_queue
|
|
1001
1087
|
@consumer_queue ||= @native_kafka.with_inner do |inner|
|
|
1002
|
-
Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
1088
|
+
queue = Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
1089
|
+
# Share the pointer with the finalizer so it is destroyed even if the consumer is GC'd
|
|
1090
|
+
# without an explicit close.
|
|
1091
|
+
@consumer_queue_holder[0] = queue
|
|
1092
|
+
queue
|
|
1003
1093
|
end
|
|
1004
1094
|
end
|
|
1005
1095
|
|
data/lib/rdkafka/defaults.rb
CHANGED
|
@@ -23,6 +23,10 @@ module Rdkafka
|
|
|
23
23
|
# @see Consumer#committed
|
|
24
24
|
CONSUMER_COMMITTED_TIMEOUT_MS = 2_000
|
|
25
25
|
|
|
26
|
+
# Default timeout for fetching the cluster id
|
|
27
|
+
# @see Consumer#cluster_id
|
|
28
|
+
CONSUMER_CLUSTER_ID_TIMEOUT_MS = 1_000
|
|
29
|
+
|
|
26
30
|
# Default timeout for querying watermark offsets
|
|
27
31
|
# @see Consumer#query_watermark_offsets
|
|
28
32
|
CONSUMER_QUERY_WATERMARK_TIMEOUT_MS = 1_000
|
|
@@ -91,6 +95,16 @@ module Rdkafka
|
|
|
91
95
|
# @see Metadata#initialize
|
|
92
96
|
METADATA_RETRY_BACKOFF_BASE_MS = 100
|
|
93
97
|
|
|
98
|
+
# Maximum backoff time between metadata retries. Caps the exponential backoff so a long retry
|
|
99
|
+
# sequence against an unhealthy cluster cannot block the calling thread for minutes.
|
|
100
|
+
# @see Metadata#initialize
|
|
101
|
+
METADATA_RETRY_BACKOFF_MAX_MS = 1_000
|
|
102
|
+
|
|
103
|
+
# Soft wall-clock budget for the whole metadata retry loop; past it (and past
|
|
104
|
+
# METADATA_MIN_ATTEMPTS) the loop stops so a synchronous fetch cannot block the caller for long
|
|
105
|
+
# @see Metadata#initialize
|
|
106
|
+
METADATA_RETRY_BUDGET_MS = 5_000
|
|
107
|
+
|
|
94
108
|
# Cache settings (in milliseconds)
|
|
95
109
|
|
|
96
110
|
# Default time-to-live for cached partition counts
|
|
@@ -102,5 +116,10 @@ module Rdkafka
|
|
|
102
116
|
# Maximum number of metadata fetch retry attempts
|
|
103
117
|
# @see Metadata#initialize
|
|
104
118
|
METADATA_MAX_RETRIES = 10
|
|
119
|
+
|
|
120
|
+
# Minimum metadata fetch attempts before the retry budget may end the loop, so a slow broker
|
|
121
|
+
# (whose requests each consume the full timeout) still gets a few tries
|
|
122
|
+
# @see Metadata#initialize
|
|
123
|
+
METADATA_MIN_ATTEMPTS = 3
|
|
105
124
|
end
|
|
106
125
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
module Helpers
|
|
5
|
+
# Shared `#metadata` implementation for Admin, Consumer and Producer.
|
|
6
|
+
#
|
|
7
|
+
# `rd_kafka_metadata()` is handle-agnostic in librdkafka - it works on any `rd_kafka_t`
|
|
8
|
+
# (consumer, producer or admin) - so this Ruby-level implementation is identical across all
|
|
9
|
+
# three client types. Includers must provide a private `#closed_check(method)` that raises
|
|
10
|
+
# their own `Closed*Error`.
|
|
11
|
+
module Metadata
|
|
12
|
+
# Performs the metadata request using this client
|
|
13
|
+
#
|
|
14
|
+
# @param topic_name [String, nil] metadata about particular topic or all if nil
|
|
15
|
+
# @param timeout_ms [Integer] metadata request timeout
|
|
16
|
+
# @return [Rdkafka::Metadata] requested metadata
|
|
17
|
+
def metadata(topic_name = nil, timeout_ms = Defaults::METADATA_TIMEOUT_MS)
|
|
18
|
+
closed_check(__method__)
|
|
19
|
+
|
|
20
|
+
@native_kafka.with_inner do |inner|
|
|
21
|
+
# Must stay fully qualified: this module is itself named `Metadata`, so a bare
|
|
22
|
+
# `Metadata.new` here would resolve to `Rdkafka::Helpers::Metadata` (no `.new`) instead
|
|
23
|
+
# of this class.
|
|
24
|
+
Rdkafka::Metadata.new(inner, topic_name, timeout_ms)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/rdkafka/metadata.rb
CHANGED
|
@@ -23,12 +23,57 @@ module Rdkafka
|
|
|
23
23
|
# @param timeout_ms [Integer] timeout in milliseconds
|
|
24
24
|
# @raise [RdkafkaError] when metadata fetch fails
|
|
25
25
|
def initialize(native_client, topic_name = nil, timeout_ms = Defaults::METADATA_TIMEOUT_MS)
|
|
26
|
-
attempt
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
attempt = 0
|
|
27
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) +
|
|
28
|
+
Defaults::METADATA_RETRY_BUDGET_MS / 1_000.0
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
attempt += 1
|
|
32
|
+
fetch_metadata(native_client, topic_name, timeout_ms)
|
|
33
|
+
rescue ::Rdkafka::RdkafkaError => e
|
|
34
|
+
raise unless RETRIED_ERRORS.include?(e.code)
|
|
35
|
+
raise if attempt > Defaults::METADATA_MAX_RETRIES
|
|
36
|
+
|
|
37
|
+
# Stop once the wall-clock retry budget is spent, but only after at least
|
|
38
|
+
# METADATA_MIN_ATTEMPTS tries so a slow broker (whose requests each consume the full
|
|
39
|
+
# timeout) still gets a few tries rather than being cut off after one or two.
|
|
40
|
+
raise if attempt >= Defaults::METADATA_MIN_ATTEMPTS &&
|
|
41
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC) >= deadline
|
|
42
|
+
|
|
43
|
+
# Exponential backoff between attempts, capped so a long retry sequence cannot block for
|
|
44
|
+
# minutes. The request timeout (`timeout_ms`) is intentionally left unchanged: it used to be
|
|
45
|
+
# overwritten with the backoff value, which shrank the first retries below the configured
|
|
46
|
+
# timeout (near-guaranteeing another timeout) and then inflated later ones to ~100s.
|
|
47
|
+
backoff_ms = [
|
|
48
|
+
(2**attempt) * Defaults::METADATA_RETRY_BACKOFF_BASE_MS,
|
|
49
|
+
Defaults::METADATA_RETRY_BACKOFF_MAX_MS
|
|
50
|
+
].min
|
|
51
|
+
|
|
52
|
+
sleep(backoff_ms / 1_000.0)
|
|
53
|
+
|
|
54
|
+
retry
|
|
31
55
|
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Performs a single metadata fetch attempt, freeing this attempt's native resources.
|
|
61
|
+
#
|
|
62
|
+
# Kept separate from {#initialize} so each retried attempt frees its own `native_topic` and
|
|
63
|
+
# metadata struct. `retry` re-enters `initialize`'s `begin` without running an `ensure` placed
|
|
64
|
+
# there, so doing the cleanup per attempt here is what prevents the per-retry native leak. The
|
|
65
|
+
# metadata struct is only allocated on success, so it is read and destroyed only after the
|
|
66
|
+
# result has been validated (avoids destroying a NULL/garbage pointer on a failed fetch).
|
|
67
|
+
#
|
|
68
|
+
# @param native_client [FFI::Pointer] pointer to the native Kafka client
|
|
69
|
+
# @param topic_name [String, nil] specific topic to fetch metadata for, or nil for all topics
|
|
70
|
+
# @param timeout_ms [Integer] timeout in milliseconds
|
|
71
|
+
# @raise [RdkafkaError] when the metadata fetch fails
|
|
72
|
+
def fetch_metadata(native_client, topic_name, timeout_ms)
|
|
73
|
+
native_topic = nil
|
|
74
|
+
metadata_ptr = nil
|
|
75
|
+
|
|
76
|
+
native_topic = Rdkafka::Bindings.rd_kafka_topic_new(native_client, topic_name, nil) if topic_name
|
|
32
77
|
|
|
33
78
|
ptr = FFI::MemoryPointer.new(:pointer)
|
|
34
79
|
|
|
@@ -42,24 +87,16 @@ module Rdkafka
|
|
|
42
87
|
# Error Handling
|
|
43
88
|
raise Rdkafka::RdkafkaError.new(result) unless result.zero?
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
raise if attempt > Defaults::METADATA_MAX_RETRIES
|
|
90
|
+
# rd_kafka_metadata only allocates the struct on success, so we read the pointer to destroy
|
|
91
|
+
# only after the result has been confirmed successful.
|
|
92
|
+
metadata_ptr = ptr.read_pointer
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
timeout_ms = backoff_factor * Defaults::METADATA_RETRY_BACKOFF_BASE_MS
|
|
52
|
-
|
|
53
|
-
sleep(timeout_ms / 1000.0)
|
|
54
|
-
|
|
55
|
-
retry
|
|
94
|
+
metadata_from_native(metadata_ptr)
|
|
56
95
|
ensure
|
|
57
|
-
Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if
|
|
58
|
-
Rdkafka::Bindings.rd_kafka_metadata_destroy(
|
|
96
|
+
Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if native_topic
|
|
97
|
+
Rdkafka::Bindings.rd_kafka_metadata_destroy(metadata_ptr) if metadata_ptr && !metadata_ptr.null?
|
|
59
98
|
end
|
|
60
99
|
|
|
61
|
-
private
|
|
62
|
-
|
|
63
100
|
# Extracts metadata from native pointer
|
|
64
101
|
# @param ptr [FFI::Pointer] pointer to native metadata
|
|
65
102
|
def metadata_from_native(ptr)
|
|
@@ -134,6 +171,36 @@ module Rdkafka
|
|
|
134
171
|
:replicas, :pointer,
|
|
135
172
|
:in_sync_replica_brokers, :int,
|
|
136
173
|
:isrs, :pointer
|
|
174
|
+
|
|
175
|
+
# The base `#to_h` skips FFI pointer members, which would drop the replica and in-sync
|
|
176
|
+
# replica assignments entirely. We dereference those pointers here so the partition hash
|
|
177
|
+
# exposes the broker ids backing the partition (needed e.g. to plan replication changes).
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash{Symbol => Integer, Array<Integer>}] partition metadata:
|
|
180
|
+
# * +:partition_id+ (Integer) - partition id
|
|
181
|
+
# * +:leader+ (Integer) - broker id of the partition leader
|
|
182
|
+
# * +:replica_count+ (Integer) - number of assigned replicas
|
|
183
|
+
# * +:in_sync_replica_brokers+ (Integer) - number of in-sync replicas
|
|
184
|
+
# * +:replicas+ (Array<Integer>) - broker ids of the assigned replicas
|
|
185
|
+
# * +:isrs+ (Array<Integer>) - broker ids of the in-sync replicas
|
|
186
|
+
def to_h
|
|
187
|
+
super.merge(
|
|
188
|
+
replicas: read_broker_ids(self[:replicas], self[:replica_count]),
|
|
189
|
+
isrs: read_broker_ids(self[:isrs], self[:in_sync_replica_brokers])
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# Reads `count` broker ids (int32) from a replicas/isrs pointer.
|
|
196
|
+
# @param pointer [FFI::Pointer] pointer to the broker ids array
|
|
197
|
+
# @param count [Integer] number of broker ids to read
|
|
198
|
+
# @return [Array<Integer>] broker ids (empty when there are none)
|
|
199
|
+
def read_broker_ids(pointer, count)
|
|
200
|
+
return [] if count.zero? || pointer.null?
|
|
201
|
+
|
|
202
|
+
pointer.read_array_of_int32(count)
|
|
203
|
+
end
|
|
137
204
|
end
|
|
138
205
|
end
|
|
139
206
|
end
|
data/lib/rdkafka/native_kafka.rb
CHANGED
|
@@ -55,8 +55,7 @@ module Rdkafka
|
|
|
55
55
|
Rdkafka::Bindings.rd_kafka_poll(@inner, 0)
|
|
56
56
|
|
|
57
57
|
if @run_polling_thread
|
|
58
|
-
# Start thread to poll client for delivery callbacks,
|
|
59
|
-
# not used in consumer.
|
|
58
|
+
# Start thread to poll client for delivery callbacks, not used in consumer.
|
|
60
59
|
@polling_thread = Thread.new do
|
|
61
60
|
loop do
|
|
62
61
|
@poll_mutex.synchronize do
|
|
@@ -197,8 +196,7 @@ module Rdkafka
|
|
|
197
196
|
# Indicate to polling thread that we're closing
|
|
198
197
|
@polling_thread[:closing] = true
|
|
199
198
|
|
|
200
|
-
# Wait for the polling thread to finish up,
|
|
201
|
-
# this can be aborted in practice if this
|
|
199
|
+
# Wait for the polling thread to finish up, this can be aborted in practice if this
|
|
202
200
|
# code runs from a finalizer.
|
|
203
201
|
@polling_thread.join
|
|
204
202
|
end
|
|
@@ -8,14 +8,14 @@ module Rdkafka
|
|
|
8
8
|
layout :pending, :bool,
|
|
9
9
|
:response, :int,
|
|
10
10
|
:partition, :int,
|
|
11
|
-
:offset, :int64
|
|
12
|
-
:topic_name, :pointer
|
|
11
|
+
:offset, :int64
|
|
13
12
|
|
|
14
13
|
# @return [Object, nil] label set during message production or nil by default
|
|
15
14
|
attr_accessor :label
|
|
16
15
|
|
|
17
16
|
# @return [String] topic where we are trying to send the message
|
|
18
|
-
#
|
|
17
|
+
# Set in `#produce`, where the topic is known upfront. Keeping it as a Ruby attribute
|
|
18
|
+
# spares a per-message native string copy in the delivery callback.
|
|
19
19
|
attr_accessor :topic
|
|
20
20
|
|
|
21
21
|
# @return [String] the name of the operation (e.g. "delivery")
|
|
@@ -37,12 +37,11 @@ module Rdkafka
|
|
|
37
37
|
# contention in multi-threaded environments while ensuring data consistency.
|
|
38
38
|
#
|
|
39
39
|
# 6. Topic recreation handling
|
|
40
|
-
# If a topic is deleted and recreated with fewer partitions, the cache
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# This also aligns with the previous cache implementation.
|
|
40
|
+
# If a topic is deleted and recreated with fewer partitions, the cache keeps reporting the
|
|
41
|
+
# higher count only until the entry's TTL expires. The first refresh after expiry performs
|
|
42
|
+
# an authoritative metadata read and adopts the lower count. Within the TTL window a lower
|
|
43
|
+
# value is still ignored, so a transient or racy lower read cannot clobber a correct higher
|
|
44
|
+
# count.
|
|
46
45
|
class PartitionsCountCache
|
|
47
46
|
include Helpers::Time
|
|
48
47
|
|
|
@@ -91,28 +90,14 @@ module Rdkafka
|
|
|
91
90
|
current_info = @counts[topic]
|
|
92
91
|
|
|
93
92
|
if current_info.nil? || expired?(current_info[0])
|
|
93
|
+
# The cached entry is missing or expired, so the block performs an authoritative metadata
|
|
94
|
+
# read. We hand it to `set`, which adopts a higher count always and a lower count once the
|
|
95
|
+
# entry has expired (e.g. the topic was recreated with fewer partitions). We then return
|
|
96
|
+
# whatever `set` settled on so a concurrent refresh that wrote a higher value still wins.
|
|
94
97
|
new_count = yield
|
|
98
|
+
set(topic, new_count)
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
# No existing data, create a new entry with mutex
|
|
98
|
-
set(topic, new_count)
|
|
99
|
-
|
|
100
|
-
return new_count
|
|
101
|
-
else
|
|
102
|
-
current_count = current_info[1]
|
|
103
|
-
|
|
104
|
-
if new_count > current_count
|
|
105
|
-
# Higher value needs mutex to update both timestamp and count
|
|
106
|
-
set(topic, new_count)
|
|
107
|
-
|
|
108
|
-
return new_count
|
|
109
|
-
else
|
|
110
|
-
# Same or lower value, just update timestamp without mutex
|
|
111
|
-
refresh_timestamp(topic)
|
|
112
|
-
|
|
113
|
-
return current_count
|
|
114
|
-
end
|
|
115
|
-
end
|
|
100
|
+
return @counts[topic][1]
|
|
116
101
|
end
|
|
117
102
|
|
|
118
103
|
current_info[1]
|
|
@@ -133,8 +118,11 @@ module Rdkafka
|
|
|
133
118
|
# First check outside mutex to avoid unnecessary locking
|
|
134
119
|
current_info = @counts[topic]
|
|
135
120
|
|
|
136
|
-
#
|
|
137
|
-
|
|
121
|
+
# Within the TTL window a lower value is treated as a stale/racy read and ignored, since
|
|
122
|
+
# partition counts only grow during normal operation. Once the entry has expired a lower
|
|
123
|
+
# value is an authoritative refresh (e.g. the topic was recreated with fewer partitions),
|
|
124
|
+
# so we fall through and adopt it below.
|
|
125
|
+
if current_info && new_count < current_info[1] && !expired?(current_info[0])
|
|
138
126
|
refresh_timestamp(topic)
|
|
139
127
|
|
|
140
128
|
return
|
|
@@ -148,17 +136,15 @@ module Rdkafka
|
|
|
148
136
|
if current_info.nil?
|
|
149
137
|
# Create new entry
|
|
150
138
|
@counts[topic] = [monotonic_now_ms, new_count]
|
|
139
|
+
elsif new_count > current_info[1] || expired?(current_info[0])
|
|
140
|
+
# A higher count always wins; a lower count is accepted only when the existing entry
|
|
141
|
+
# has expired, so a concurrent fresh higher value (which reset the timestamp) is never
|
|
142
|
+
# clobbered by a stale lower one.
|
|
143
|
+
current_info[0] = monotonic_now_ms
|
|
144
|
+
current_info[1] = new_count
|
|
151
145
|
else
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if new_count > current_count
|
|
155
|
-
# Update to higher count value
|
|
156
|
-
current_info[0] = monotonic_now_ms
|
|
157
|
-
current_info[1] = new_count
|
|
158
|
-
else
|
|
159
|
-
# Same or lower count, update timestamp only
|
|
160
|
-
current_info[0] = monotonic_now_ms
|
|
161
|
-
end
|
|
146
|
+
# Same or lower count within the TTL window: refresh the timestamp only
|
|
147
|
+
current_info[0] = monotonic_now_ms
|
|
162
148
|
end
|
|
163
149
|
end
|
|
164
150
|
end
|