rdkafka 0.27.0-x86_64-linux-gnu → 0.29.0-x86_64-linux-gnu
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 +38 -0
- data/README.md +2 -0
- data/docker-compose-ssl.yml +1 -1
- data/docker-compose.yml +1 -1
- data/ext/librdkafka.so +0 -0
- 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 +135 -33
- 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 +6 -6
- data/rdkafka.gemspec +1 -0
- data/renovate.json +13 -2
- 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
|
|
@@ -795,16 +845,21 @@ module Rdkafka
|
|
|
795
845
|
# is available, librdkafka fills the buffer with whatever is immediately ready and
|
|
796
846
|
# returns without further waiting.
|
|
797
847
|
#
|
|
848
|
+
# Error events (e.g. `:partition_eof`) are returned inline as {RdkafkaError} objects
|
|
849
|
+
# rather than raised, so callers receive the complete batch — both messages and errors —
|
|
850
|
+
# and can decide how to handle each. This is particularly useful when multiple partitions
|
|
851
|
+
# signal EOF simultaneously: all signals appear in the returned array rather than only
|
|
852
|
+
# the first one being raised and the rest silently discarded.
|
|
853
|
+
#
|
|
798
854
|
# @param timeout_ms [Integer] Timeout waiting for the first message (-1 for infinite)
|
|
799
855
|
# @param max_items [Integer] Maximum number of messages to return per call
|
|
800
|
-
# @return [Array<Message>]
|
|
801
|
-
# @raise [RdkafkaError] When a consumed message contains an error
|
|
856
|
+
# @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
|
|
802
857
|
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
803
858
|
def poll_batch(timeout_ms, max_items: 100)
|
|
804
859
|
closed_consumer_check(__method__)
|
|
805
860
|
|
|
806
861
|
buffer = batch_buffer(max_items)
|
|
807
|
-
|
|
862
|
+
results = []
|
|
808
863
|
|
|
809
864
|
count = @native_kafka.with_inner do |_inner|
|
|
810
865
|
Rdkafka::Bindings.rd_kafka_consume_batch_queue(
|
|
@@ -815,7 +870,7 @@ module Rdkafka
|
|
|
815
870
|
)
|
|
816
871
|
end
|
|
817
872
|
|
|
818
|
-
return
|
|
873
|
+
return results if count <= 0
|
|
819
874
|
|
|
820
875
|
i = 0
|
|
821
876
|
begin
|
|
@@ -830,11 +885,23 @@ module Rdkafka
|
|
|
830
885
|
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
831
886
|
|
|
832
887
|
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
833
|
-
|
|
888
|
+
results << Rdkafka::RdkafkaError.new(native_message[:err])
|
|
889
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
890
|
+
i += 1
|
|
891
|
+
next
|
|
892
|
+
end
|
|
893
|
+
|
|
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)
|
|
834
903
|
end
|
|
835
904
|
|
|
836
|
-
messages << Rdkafka::Consumer::Message.new(native_message)
|
|
837
|
-
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
838
905
|
i += 1
|
|
839
906
|
end
|
|
840
907
|
ensure
|
|
@@ -845,7 +912,7 @@ module Rdkafka
|
|
|
845
912
|
end
|
|
846
913
|
end
|
|
847
914
|
|
|
848
|
-
|
|
915
|
+
results
|
|
849
916
|
end
|
|
850
917
|
|
|
851
918
|
# Poll for a batch of messages without releasing the GVL (Global VM Lock).
|
|
@@ -857,16 +924,17 @@ module Rdkafka
|
|
|
857
924
|
# @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
|
|
858
925
|
# threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
|
|
859
926
|
#
|
|
927
|
+
# Error events are returned inline as {RdkafkaError} objects; see {#poll_batch} for details.
|
|
928
|
+
#
|
|
860
929
|
# @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
|
|
861
930
|
# @param max_items [Integer] Maximum number of messages to return per call
|
|
862
|
-
# @return [Array<Message>]
|
|
863
|
-
# @raise [RdkafkaError] When a consumed message contains an error
|
|
931
|
+
# @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
|
|
864
932
|
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
865
933
|
def poll_batch_nb(timeout_ms = 0, max_items: 100)
|
|
866
934
|
closed_consumer_check(__method__)
|
|
867
935
|
|
|
868
936
|
buffer = batch_buffer(max_items)
|
|
869
|
-
|
|
937
|
+
results = []
|
|
870
938
|
|
|
871
939
|
count = @native_kafka.with_inner do |_inner|
|
|
872
940
|
Rdkafka::Bindings.rd_kafka_consume_batch_queue_nb(
|
|
@@ -877,7 +945,7 @@ module Rdkafka
|
|
|
877
945
|
)
|
|
878
946
|
end
|
|
879
947
|
|
|
880
|
-
return
|
|
948
|
+
return results if count <= 0
|
|
881
949
|
|
|
882
950
|
i = 0
|
|
883
951
|
begin
|
|
@@ -892,11 +960,23 @@ module Rdkafka
|
|
|
892
960
|
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
893
961
|
|
|
894
962
|
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
895
|
-
|
|
963
|
+
results << Rdkafka::RdkafkaError.new(native_message[:err])
|
|
964
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
965
|
+
i += 1
|
|
966
|
+
next
|
|
967
|
+
end
|
|
968
|
+
|
|
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)
|
|
896
978
|
end
|
|
897
979
|
|
|
898
|
-
messages << Rdkafka::Consumer::Message.new(native_message)
|
|
899
|
-
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
900
980
|
i += 1
|
|
901
981
|
end
|
|
902
982
|
ensure
|
|
@@ -907,7 +987,7 @@ module Rdkafka
|
|
|
907
987
|
end
|
|
908
988
|
end
|
|
909
989
|
|
|
910
|
-
|
|
990
|
+
results
|
|
911
991
|
end
|
|
912
992
|
|
|
913
993
|
# Poll for new messages and yield for each received one. Iteration
|
|
@@ -982,12 +1062,34 @@ module Rdkafka
|
|
|
982
1062
|
def closed_consumer_check(method)
|
|
983
1063
|
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
|
984
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
|
|
985
1083
|
|
|
986
1084
|
# Returns the consumer queue pointer, lazily initialized
|
|
987
1085
|
# @return [FFI::Pointer] consumer queue handle
|
|
988
1086
|
def consumer_queue
|
|
989
1087
|
@consumer_queue ||= @native_kafka.with_inner do |inner|
|
|
990
|
-
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
|
|
991
1093
|
end
|
|
992
1094
|
end
|
|
993
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")
|