karafka-rdkafka 0.27.1-aarch64-linux-gnu → 0.28.0-aarch64-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 +36 -0
- data/README.md +1 -0
- data/docker-compose-ssl.yml +1 -1
- data/docker-compose.yml +1 -1
- data/ext/librdkafka.so +0 -0
- data/karafka-rdkafka.gemspec +1 -0
- data/lib/rdkafka/abstract_handle.rb +37 -4
- data/lib/rdkafka/admin/config_binding_result.rb +1 -1
- data/lib/rdkafka/admin/create_acl_handle.rb +2 -8
- data/lib/rdkafka/admin/create_partitions_handle.rb +1 -20
- data/lib/rdkafka/admin/create_topic_handle.rb +1 -20
- data/lib/rdkafka/admin/delete_acl_handle.rb +2 -10
- data/lib/rdkafka/admin/delete_groups_handle.rb +3 -11
- data/lib/rdkafka/admin/delete_topic_handle.rb +1 -20
- data/lib/rdkafka/admin/describe_acl_handle.rb +2 -10
- data/lib/rdkafka/admin/describe_configs_handle.rb +2 -13
- data/lib/rdkafka/admin/describe_configs_report.rb +5 -6
- data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +2 -13
- data/lib/rdkafka/admin/incremental_alter_configs_report.rb +5 -6
- data/lib/rdkafka/admin/list_offsets_handle.rb +2 -13
- data/lib/rdkafka/admin/list_offsets_report.rb +5 -2
- data/lib/rdkafka/admin.rb +56 -137
- data/lib/rdkafka/bindings.rb +12 -3
- 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 +56 -244
- data/lib/rdkafka/config.rb +50 -33
- data/lib/rdkafka/consumer/headers.rb +19 -5
- data/lib/rdkafka/consumer/topic_partition_list.rb +43 -33
- data/lib/rdkafka/consumer.rb +203 -39
- data/lib/rdkafka/defaults.rb +21 -1
- data/lib/rdkafka/error.rb +12 -4
- data/lib/rdkafka/helpers/list_offsets.rb +127 -0
- data/lib/rdkafka/helpers/metadata.rb +29 -0
- data/lib/rdkafka/metadata.rb +84 -19
- data/lib/rdkafka/producer/delivery_handle.rb +3 -3
- data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
- data/lib/rdkafka/producer.rb +72 -51
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +13 -0
- data/package-lock.json +6 -6
- data/renovate.json +13 -2
- metadata +15 -5
- data/Gemfile +0 -13
- data/Gemfile.lint +0 -14
- data/Gemfile.lint.lock +0 -123
|
@@ -136,51 +136,61 @@ module Rdkafka
|
|
|
136
136
|
|
|
137
137
|
# Create a native tpl with the contents of this object added.
|
|
138
138
|
#
|
|
139
|
-
# The pointer
|
|
139
|
+
# The returned pointer is caller-owned and must be released with
|
|
140
|
+
# `rd_kafka_topic_partition_list_destroy`. It is not cleaned up by GC.
|
|
140
141
|
#
|
|
141
142
|
# @private
|
|
142
143
|
# @return [FFI::Pointer]
|
|
143
144
|
def to_native_tpl
|
|
144
145
|
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
partitions
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
topic,
|
|
152
|
-
p.partition
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Remove the respond to check after karafka 2.3.0 is released
|
|
156
|
-
if p.respond_to?(:metadata) && p.metadata
|
|
157
|
-
part = Rdkafka::Bindings::TopicPartition.new(ref)
|
|
158
|
-
str_ptr = FFI::MemoryPointer.from_string(p.metadata)
|
|
159
|
-
# released here:
|
|
160
|
-
# https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
|
|
161
|
-
str_ptr.autorelease = false
|
|
162
|
-
part[:metadata] = str_ptr
|
|
163
|
-
part[:metadata_size] = p.metadata.bytesize
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
if p.offset
|
|
167
|
-
offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
|
|
168
|
-
|
|
169
|
-
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
|
|
147
|
+
begin
|
|
148
|
+
@data.each do |topic, partitions|
|
|
149
|
+
if partitions
|
|
150
|
+
partitions.each do |p|
|
|
151
|
+
ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
170
152
|
tpl,
|
|
171
153
|
topic,
|
|
172
|
-
p.partition
|
|
173
|
-
offset
|
|
154
|
+
p.partition
|
|
174
155
|
)
|
|
156
|
+
|
|
157
|
+
# Remove the respond to check after karafka 2.3.0 is released
|
|
158
|
+
if p.respond_to?(:metadata) && p.metadata
|
|
159
|
+
part = Rdkafka::Bindings::TopicPartition.new(ref)
|
|
160
|
+
str_ptr = FFI::MemoryPointer.from_string(p.metadata)
|
|
161
|
+
# released here:
|
|
162
|
+
# https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
|
|
163
|
+
str_ptr.autorelease = false
|
|
164
|
+
part[:metadata] = str_ptr
|
|
165
|
+
part[:metadata_size] = p.metadata.bytesize
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if p.offset
|
|
169
|
+
offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
|
|
170
|
+
|
|
171
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
|
|
172
|
+
tpl,
|
|
173
|
+
topic,
|
|
174
|
+
p.partition,
|
|
175
|
+
offset
|
|
176
|
+
)
|
|
177
|
+
end
|
|
175
178
|
end
|
|
179
|
+
else
|
|
180
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
181
|
+
tpl,
|
|
182
|
+
topic,
|
|
183
|
+
Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
184
|
+
)
|
|
176
185
|
end
|
|
177
|
-
else
|
|
178
|
-
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
179
|
-
tpl,
|
|
180
|
-
topic,
|
|
181
|
-
Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
182
|
-
)
|
|
183
186
|
end
|
|
187
|
+
rescue Exception
|
|
188
|
+
# The native list is caller-owned and only handed back on success. If population raises
|
|
189
|
+
# partway (e.g. non-string metadata, or an offset FFI can't coerce to int64) we must
|
|
190
|
+
# destroy it here, otherwise the half-built list leaks.
|
|
191
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
192
|
+
|
|
193
|
+
raise
|
|
184
194
|
end
|
|
185
195
|
|
|
186
196
|
tpl
|
data/lib/rdkafka/consumer.rb
CHANGED
|
@@ -14,14 +14,46 @@ module Rdkafka
|
|
|
14
14
|
include Enumerable
|
|
15
15
|
include Helpers::Time
|
|
16
16
|
include Helpers::OAuth
|
|
17
|
+
include Helpers::Metadata
|
|
18
|
+
include Helpers::ListOffsets
|
|
17
19
|
|
|
18
20
|
# @private
|
|
19
21
|
# @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
|
|
20
22
|
def initialize(native_kafka)
|
|
21
23
|
@native_kafka = native_kafka
|
|
24
|
+
# Single-element holder shared with the GC finalizer so it can destroy the lazily created
|
|
25
|
+
# consumer queue without capturing `self` (capturing the consumer in its own finalizer would
|
|
26
|
+
# pin it and prevent it from ever being collected).
|
|
27
|
+
@consumer_queue_holder = []
|
|
28
|
+
|
|
29
|
+
# Makes sure the consumer is closed (consumer queue destroyed and native client destroyed)
|
|
30
|
+
# before it gets GCed by Ruby.
|
|
31
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(native_kafka, @consumer_queue_holder))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Builds the GC finalizer for a consumer. It mirrors {#close}: close the consumer, destroy the
|
|
35
|
+
# consumer-queue reference, then destroy the native client. The default `NativeKafka#finalizer`
|
|
36
|
+
# went straight to `rd_kafka_destroy`, leaving the consumer-queue reference (from
|
|
37
|
+
# `rd_kafka_queue_get_consumer`, taken by `poll_batch`) dangling - which can make
|
|
38
|
+
# `rd_kafka_destroy` block inside the finalizer (process hang at GC/shutdown) or leak the handle.
|
|
39
|
+
#
|
|
40
|
+
# @private
|
|
41
|
+
# @param native_kafka [NativeKafka] the wrapped native client
|
|
42
|
+
# @param queue_holder [Array] single-element holder carrying the consumer queue pointer (or empty)
|
|
43
|
+
# @return [Proc] finalizer proc that must not reference the consumer instance
|
|
44
|
+
def self.finalizer(native_kafka, queue_holder)
|
|
45
|
+
proc do
|
|
46
|
+
next if native_kafka.closed?
|
|
47
|
+
|
|
48
|
+
native_kafka.synchronize do |inner|
|
|
49
|
+
Rdkafka::Bindings.rd_kafka_consumer_close(inner)
|
|
50
|
+
|
|
51
|
+
queue = queue_holder[0]
|
|
52
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue) if queue
|
|
53
|
+
end
|
|
22
54
|
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
native_kafka.close
|
|
56
|
+
end
|
|
25
57
|
end
|
|
26
58
|
|
|
27
59
|
# Starts the native Kafka polling thread and kicks off the init polling
|
|
@@ -187,6 +219,7 @@ module Rdkafka
|
|
|
187
219
|
if @consumer_queue
|
|
188
220
|
Rdkafka::Bindings.rd_kafka_queue_destroy(@consumer_queue)
|
|
189
221
|
@consumer_queue = nil
|
|
222
|
+
@consumer_queue_holder[0] = nil
|
|
190
223
|
end
|
|
191
224
|
end
|
|
192
225
|
|
|
@@ -412,6 +445,8 @@ module Rdkafka
|
|
|
412
445
|
#
|
|
413
446
|
# @raise [RdkafkaError] When getting the positions fails.
|
|
414
447
|
def position(list = nil)
|
|
448
|
+
closed_consumer_check(__method__)
|
|
449
|
+
|
|
415
450
|
if list.nil?
|
|
416
451
|
list = assignment
|
|
417
452
|
elsif !list.is_a?(TopicPartitionList)
|
|
@@ -466,39 +501,77 @@ module Rdkafka
|
|
|
466
501
|
# possible to create one yourself, in this case you have to provide a list that
|
|
467
502
|
# already contains all the partitions you need the lag for.
|
|
468
503
|
#
|
|
504
|
+
# The end offsets of all requested partitions are fetched in a single batched
|
|
505
|
+
# {#list_offsets} query - librdkafka fans it out to the involved partition leaders
|
|
506
|
+
# internally and concurrently - instead of one blocking {#query_watermark_offsets}
|
|
507
|
+
# broker roundtrip per partition.
|
|
508
|
+
#
|
|
469
509
|
# @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
|
|
470
|
-
# @param watermark_timeout_ms [Integer] The timeout for
|
|
510
|
+
# @param watermark_timeout_ms [Integer] The timeout for the batched end-offsets query.
|
|
471
511
|
# @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
|
|
472
512
|
# per partition
|
|
513
|
+
# @raise [ClosedConsumerError] when the consumer is closed
|
|
473
514
|
# @raise [RdkafkaError] When querying the broker fails.
|
|
474
515
|
def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
|
|
516
|
+
closed_consumer_check(__method__)
|
|
517
|
+
|
|
475
518
|
out = {}
|
|
519
|
+
request = {}
|
|
520
|
+
partitions_by_topic = topic_partition_list.to_h
|
|
521
|
+
|
|
522
|
+
partitions_by_topic.each do |topic, partitions|
|
|
523
|
+
out[topic] = {}
|
|
524
|
+
|
|
525
|
+
partitions.each do |p|
|
|
526
|
+
next if p.offset.nil?
|
|
527
|
+
|
|
528
|
+
(request[topic] ||= []) << { partition: p.partition, offset: :latest }
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
return out if request.empty?
|
|
533
|
+
|
|
534
|
+
report = begin
|
|
535
|
+
# The isolation level is forwarded so the end offsets match what the old per-partition
|
|
536
|
+
# watermark query returned: librdkafka resolves that query with the consumer's configured
|
|
537
|
+
# `isolation.level` (LSO for the default read_committed), while the admin-style batched
|
|
538
|
+
# query would otherwise default to read_uncommitted (true high watermark).
|
|
539
|
+
list_offsets(request, isolation_level: isolation_level)
|
|
540
|
+
.wait(max_wait_timeout_ms: watermark_timeout_ms)
|
|
541
|
+
rescue AbstractHandle::WaitTimeoutError
|
|
542
|
+
# Keep the pre-batching contract: a slow broker surfaced as a timed-out RdkafkaError
|
|
543
|
+
# from the per-partition watermark query, not as a handle wait timeout.
|
|
544
|
+
raise RdkafkaError.new(
|
|
545
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR__TIMED_OUT,
|
|
546
|
+
"Error querying watermark offsets of '#{request.keys.join(", ")}'"
|
|
547
|
+
)
|
|
548
|
+
end
|
|
476
549
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
550
|
+
end_offsets = {}
|
|
551
|
+
report.offsets.each do |result|
|
|
552
|
+
(end_offsets[result[:topic]] ||= {})[result[:partition]] = result[:offset]
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
partitions_by_topic.each do |topic, partitions|
|
|
481
556
|
partitions.each do |p|
|
|
482
557
|
next if p.offset.nil?
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
p.partition,
|
|
486
|
-
watermark_timeout_ms
|
|
487
|
-
)
|
|
488
|
-
topic_out[p.partition] = high - p.offset
|
|
558
|
+
|
|
559
|
+
out[topic][p.partition] = end_offsets.fetch(topic).fetch(p.partition) - p.offset
|
|
489
560
|
end
|
|
490
|
-
out[topic] = topic_out
|
|
491
561
|
end
|
|
562
|
+
|
|
492
563
|
out
|
|
493
564
|
end
|
|
494
565
|
|
|
495
566
|
# Returns the ClusterId as reported in broker metadata.
|
|
496
567
|
#
|
|
497
|
-
# @
|
|
498
|
-
|
|
568
|
+
# @param timeout_ms [Integer] how long to block waiting for metadata when the cluster id is
|
|
569
|
+
# not already cached (0 for a non-blocking call)
|
|
570
|
+
# @return [String, nil] the cluster id, or nil if it could not be retrieved in time
|
|
571
|
+
def cluster_id(timeout_ms = Defaults::CONSUMER_CLUSTER_ID_TIMEOUT_MS)
|
|
499
572
|
closed_consumer_check(__method__)
|
|
500
573
|
@native_kafka.with_inner do |inner|
|
|
501
|
-
Rdkafka::Bindings.rd_kafka_clusterid(inner)
|
|
574
|
+
read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_clusterid(inner, timeout_ms))
|
|
502
575
|
end
|
|
503
576
|
end
|
|
504
577
|
|
|
@@ -510,7 +583,7 @@ module Rdkafka
|
|
|
510
583
|
def member_id
|
|
511
584
|
closed_consumer_check(__method__)
|
|
512
585
|
@native_kafka.with_inner do |inner|
|
|
513
|
-
Rdkafka::Bindings.rd_kafka_memberid(inner)
|
|
586
|
+
read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_memberid(inner))
|
|
514
587
|
end
|
|
515
588
|
end
|
|
516
589
|
|
|
@@ -801,16 +874,21 @@ module Rdkafka
|
|
|
801
874
|
# is available, librdkafka fills the buffer with whatever is immediately ready and
|
|
802
875
|
# returns without further waiting.
|
|
803
876
|
#
|
|
877
|
+
# Error events (e.g. `:partition_eof`) are returned inline as {RdkafkaError} objects
|
|
878
|
+
# rather than raised, so callers receive the complete batch — both messages and errors —
|
|
879
|
+
# and can decide how to handle each. This is particularly useful when multiple partitions
|
|
880
|
+
# signal EOF simultaneously: all signals appear in the returned array rather than only
|
|
881
|
+
# the first one being raised and the rest silently discarded.
|
|
882
|
+
#
|
|
804
883
|
# @param timeout_ms [Integer] Timeout waiting for the first message (-1 for infinite)
|
|
805
884
|
# @param max_items [Integer] Maximum number of messages to return per call
|
|
806
|
-
# @return [Array<Message>]
|
|
807
|
-
# @raise [RdkafkaError] When a consumed message contains an error
|
|
885
|
+
# @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
|
|
808
886
|
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
809
887
|
def poll_batch(timeout_ms, max_items: 100)
|
|
810
888
|
closed_consumer_check(__method__)
|
|
811
889
|
|
|
812
890
|
buffer = batch_buffer(max_items)
|
|
813
|
-
|
|
891
|
+
results = []
|
|
814
892
|
|
|
815
893
|
count = @native_kafka.with_inner do |_inner|
|
|
816
894
|
Rdkafka::Bindings.rd_kafka_consume_batch_queue(
|
|
@@ -821,7 +899,7 @@ module Rdkafka
|
|
|
821
899
|
)
|
|
822
900
|
end
|
|
823
901
|
|
|
824
|
-
return
|
|
902
|
+
return results if count <= 0
|
|
825
903
|
|
|
826
904
|
i = 0
|
|
827
905
|
begin
|
|
@@ -836,13 +914,23 @@ module Rdkafka
|
|
|
836
914
|
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
837
915
|
|
|
838
916
|
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
917
|
+
results << build_batch_error(native_message)
|
|
918
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
919
|
+
i += 1
|
|
920
|
+
next
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
begin
|
|
924
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
925
|
+
rescue Rdkafka::RdkafkaError => e
|
|
926
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
927
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
928
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
929
|
+
results << e
|
|
930
|
+
ensure
|
|
931
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
842
932
|
end
|
|
843
933
|
|
|
844
|
-
messages << Rdkafka::Consumer::Message.new(native_message)
|
|
845
|
-
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
846
934
|
i += 1
|
|
847
935
|
end
|
|
848
936
|
ensure
|
|
@@ -853,7 +941,7 @@ module Rdkafka
|
|
|
853
941
|
end
|
|
854
942
|
end
|
|
855
943
|
|
|
856
|
-
|
|
944
|
+
results
|
|
857
945
|
end
|
|
858
946
|
|
|
859
947
|
# Poll for a batch of messages without releasing the GVL (Global VM Lock).
|
|
@@ -865,16 +953,17 @@ module Rdkafka
|
|
|
865
953
|
# @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
|
|
866
954
|
# threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
|
|
867
955
|
#
|
|
956
|
+
# Error events are returned inline as {RdkafkaError} objects; see {#poll_batch} for details.
|
|
957
|
+
#
|
|
868
958
|
# @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
|
|
869
959
|
# @param max_items [Integer] Maximum number of messages to return per call
|
|
870
|
-
# @return [Array<Message>]
|
|
871
|
-
# @raise [RdkafkaError] When a consumed message contains an error
|
|
960
|
+
# @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
|
|
872
961
|
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
873
962
|
def poll_batch_nb(timeout_ms = 0, max_items: 100)
|
|
874
963
|
closed_consumer_check(__method__)
|
|
875
964
|
|
|
876
965
|
buffer = batch_buffer(max_items)
|
|
877
|
-
|
|
966
|
+
results = []
|
|
878
967
|
|
|
879
968
|
count = @native_kafka.with_inner do |_inner|
|
|
880
969
|
Rdkafka::Bindings.rd_kafka_consume_batch_queue_nb(
|
|
@@ -885,7 +974,7 @@ module Rdkafka
|
|
|
885
974
|
)
|
|
886
975
|
end
|
|
887
976
|
|
|
888
|
-
return
|
|
977
|
+
return results if count <= 0
|
|
889
978
|
|
|
890
979
|
i = 0
|
|
891
980
|
begin
|
|
@@ -900,13 +989,23 @@ module Rdkafka
|
|
|
900
989
|
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
901
990
|
|
|
902
991
|
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
992
|
+
results << build_batch_error(native_message)
|
|
993
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
994
|
+
i += 1
|
|
995
|
+
next
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
begin
|
|
999
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
1000
|
+
rescue Rdkafka::RdkafkaError => e
|
|
1001
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
1002
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
1003
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
1004
|
+
results << e
|
|
1005
|
+
ensure
|
|
1006
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
906
1007
|
end
|
|
907
1008
|
|
|
908
|
-
messages << Rdkafka::Consumer::Message.new(native_message)
|
|
909
|
-
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
910
1009
|
i += 1
|
|
911
1010
|
end
|
|
912
1011
|
ensure
|
|
@@ -917,7 +1016,7 @@ module Rdkafka
|
|
|
917
1016
|
end
|
|
918
1017
|
end
|
|
919
1018
|
|
|
920
|
-
|
|
1019
|
+
results
|
|
921
1020
|
end
|
|
922
1021
|
|
|
923
1022
|
# Poll for new messages and yield for each received one. Iteration
|
|
@@ -986,18 +1085,83 @@ module Rdkafka
|
|
|
986
1085
|
|
|
987
1086
|
private
|
|
988
1087
|
|
|
1088
|
+
# Copies a librdkafka-allocated C string into a Ruby string and frees the native buffer.
|
|
1089
|
+
#
|
|
1090
|
+
# `rd_kafka_memberid`/`rd_kafka_clusterid` return a string the caller owns and must release
|
|
1091
|
+
# with `rd_kafka_mem_free`; without this the buffer leaks on every call.
|
|
1092
|
+
#
|
|
1093
|
+
# @param inner [FFI::Pointer] the native client handle (needed to free the buffer)
|
|
1094
|
+
# @param ptr [FFI::Pointer] the librdkafka-allocated string, possibly NULL
|
|
1095
|
+
# @return [String, nil] the copied string, or nil when the pointer was NULL
|
|
1096
|
+
def read_and_free_native_string(inner, ptr)
|
|
1097
|
+
return nil if ptr.null?
|
|
1098
|
+
|
|
1099
|
+
ptr.read_string
|
|
1100
|
+
ensure
|
|
1101
|
+
Rdkafka::Bindings.rd_kafka_mem_free(inner, ptr) unless ptr.null?
|
|
1102
|
+
end
|
|
1103
|
+
|
|
989
1104
|
# Checks if the consumer is closed and raises an error if so
|
|
990
1105
|
# @param method [Symbol] name of the calling method for error context
|
|
991
1106
|
# @raise [ClosedConsumerError] when the consumer is closed
|
|
992
1107
|
def closed_consumer_check(method)
|
|
993
1108
|
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
|
994
1109
|
end
|
|
1110
|
+
alias_method :closed_check, :closed_consumer_check
|
|
1111
|
+
|
|
1112
|
+
# Reads this consumer's effective `isolation.level` from the live librdkafka configuration
|
|
1113
|
+
# and maps it to the numeric isolation level constant. Memoized: the value cannot change
|
|
1114
|
+
# after client creation.
|
|
1115
|
+
#
|
|
1116
|
+
# @return [Integer] `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` or
|
|
1117
|
+
# `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED`
|
|
1118
|
+
# @raise [Rdkafka::Config::ConfigError] when the property cannot be read
|
|
1119
|
+
def isolation_level
|
|
1120
|
+
@isolation_level ||= @native_kafka.with_inner do |inner|
|
|
1121
|
+
conf = Rdkafka::Bindings.rd_kafka_conf(inner)
|
|
1122
|
+
|
|
1123
|
+
size_ptr = Rdkafka::Bindings::SizePtr.new
|
|
1124
|
+
size_ptr[:value] = 64
|
|
1125
|
+
value_ptr = FFI::MemoryPointer.new(:char, 64)
|
|
1126
|
+
|
|
1127
|
+
result = Rdkafka::Bindings.rd_kafka_conf_get(conf, "isolation.level", value_ptr, size_ptr)
|
|
1128
|
+
|
|
1129
|
+
if result != :config_ok
|
|
1130
|
+
raise Rdkafka::Config::ConfigError.new("Could not read isolation.level: #{result}")
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
if value_ptr.read_string == "read_committed"
|
|
1134
|
+
Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED
|
|
1135
|
+
else
|
|
1136
|
+
Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED
|
|
1137
|
+
end
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
995
1140
|
|
|
996
1141
|
# Returns the consumer queue pointer, lazily initialized
|
|
997
1142
|
# @return [FFI::Pointer] consumer queue handle
|
|
998
1143
|
def consumer_queue
|
|
999
1144
|
@consumer_queue ||= @native_kafka.with_inner do |inner|
|
|
1000
|
-
Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
1145
|
+
queue = Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
1146
|
+
# Share the pointer with the finalizer so it is destroyed even if the consumer is GC'd
|
|
1147
|
+
# without an explicit close.
|
|
1148
|
+
@consumer_queue_holder[0] = queue
|
|
1149
|
+
queue
|
|
1150
|
+
end
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
# Builds an RdkafkaError from a native message, promoting to a fatal error when
|
|
1154
|
+
# librdkafka flagged a fatal condition on the underlying client handle.
|
|
1155
|
+
# @param native_message [Rdkafka::Bindings::Message] the errored native message
|
|
1156
|
+
# @return [RdkafkaError]
|
|
1157
|
+
def build_batch_error(native_message)
|
|
1158
|
+
err = Rdkafka::RdkafkaError.build(native_message)
|
|
1159
|
+
if err && err.rdkafka_response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR__FATAL
|
|
1160
|
+
@native_kafka.with_inner do |inner|
|
|
1161
|
+
Rdkafka::RdkafkaError.build_fatal(inner, fallback_error_code: err.rdkafka_response)
|
|
1162
|
+
end
|
|
1163
|
+
else
|
|
1164
|
+
err
|
|
1001
1165
|
end
|
|
1002
1166
|
end
|
|
1003
1167
|
|
data/lib/rdkafka/defaults.rb
CHANGED
|
@@ -22,6 +22,10 @@ module Rdkafka
|
|
|
22
22
|
# @see Consumer#offsets_for_times
|
|
23
23
|
CONSUMER_OFFSETS_FOR_TIMES_TIMEOUT_MS = 1_000
|
|
24
24
|
|
|
25
|
+
# Consumer timeout for fetching the cluster id when it is not already cached from metadata
|
|
26
|
+
# @see Consumer#cluster_id
|
|
27
|
+
CONSUMER_CLUSTER_ID_TIMEOUT_MS = 1_000
|
|
28
|
+
|
|
25
29
|
# Consumer timeout for poll operations (used in each iteration)
|
|
26
30
|
# @see Consumer#each
|
|
27
31
|
CONSUMER_POLL_TIMEOUT_MS = 250
|
|
@@ -55,14 +59,30 @@ module Rdkafka
|
|
|
55
59
|
# @see Admin#metadata
|
|
56
60
|
METADATA_TIMEOUT_MS = 2_000
|
|
57
61
|
|
|
58
|
-
#
|
|
62
|
+
# Hard ceiling on metadata fetch attempts on transient errors (backstop; the retry budget
|
|
63
|
+
# normally ends the loop first)
|
|
59
64
|
# @see Metadata#initialize
|
|
60
65
|
METADATA_MAX_RETRIES = 10
|
|
61
66
|
|
|
67
|
+
# Minimum metadata fetch attempts before the retry budget may end the loop, so a slow broker
|
|
68
|
+
# (whose requests each consume the full timeout) still gets a few tries
|
|
69
|
+
# @see Metadata#initialize
|
|
70
|
+
METADATA_MIN_ATTEMPTS = 3
|
|
71
|
+
|
|
72
|
+
# Soft wall-clock budget for the whole metadata retry loop; past it (and past
|
|
73
|
+
# METADATA_MIN_ATTEMPTS) the loop stops so a synchronous fetch cannot block the caller for long
|
|
74
|
+
# @see Metadata#initialize
|
|
75
|
+
METADATA_RETRY_BUDGET_MS = 5_000
|
|
76
|
+
|
|
62
77
|
# Base backoff time for metadata retry (100ms = 0.1s)
|
|
63
78
|
# @see Metadata#initialize
|
|
64
79
|
METADATA_RETRY_BACKOFF_BASE_MS = 100
|
|
65
80
|
|
|
81
|
+
# Maximum backoff time between metadata retries. Caps the exponential backoff so a long retry
|
|
82
|
+
# sequence against an unhealthy cluster cannot block the calling thread for minutes.
|
|
83
|
+
# @see Metadata#initialize
|
|
84
|
+
METADATA_RETRY_BACKOFF_MAX_MS = 1_000
|
|
85
|
+
|
|
66
86
|
# Default wait timeout for operation handles
|
|
67
87
|
# @see AbstractHandle#wait
|
|
68
88
|
HANDLE_WAIT_TIMEOUT_MS = 60_000
|
data/lib/rdkafka/error.rb
CHANGED
|
@@ -35,8 +35,9 @@ module Rdkafka
|
|
|
35
35
|
# @param response_ptr [FFI::Pointer] Pointer to rd_kafka_error_t
|
|
36
36
|
# @param message_prefix [String, nil] Optional prefix for the error message
|
|
37
37
|
# @param broker_message [String, nil] Optional broker error message
|
|
38
|
+
# @param instance_name [String, nil] Optional name of the rdkafka instance
|
|
38
39
|
# @return [RdkafkaError, false] Error instance or false if no error
|
|
39
|
-
def build_from_c(response_ptr, message_prefix = nil, broker_message: nil)
|
|
40
|
+
def build_from_c(response_ptr, message_prefix = nil, broker_message: nil, instance_name: nil)
|
|
40
41
|
code = Rdkafka::Bindings.rd_kafka_error_code(response_ptr)
|
|
41
42
|
|
|
42
43
|
return false if code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
@@ -54,7 +55,8 @@ module Rdkafka
|
|
|
54
55
|
broker_message: message,
|
|
55
56
|
fatal: fatal,
|
|
56
57
|
retryable: retryable,
|
|
57
|
-
abortable: abortable
|
|
58
|
+
abortable: abortable,
|
|
59
|
+
instance_name: instance_name
|
|
58
60
|
)
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -92,10 +94,16 @@ module Rdkafka
|
|
|
92
94
|
response_ptr_or_code[:err],
|
|
93
95
|
message_prefix,
|
|
94
96
|
broker_message: broker_message,
|
|
95
|
-
details: details
|
|
97
|
+
details: details,
|
|
98
|
+
instance_name: instance_name
|
|
96
99
|
)
|
|
97
100
|
else
|
|
98
|
-
build_from_c(
|
|
101
|
+
build_from_c(
|
|
102
|
+
response_ptr_or_code,
|
|
103
|
+
message_prefix,
|
|
104
|
+
broker_message: broker_message,
|
|
105
|
+
instance_name: instance_name
|
|
106
|
+
)
|
|
99
107
|
end
|
|
100
108
|
end
|
|
101
109
|
|