karafka-rdkafka 0.27.2-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 +33 -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 +164 -23
- 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 +11 -1
- metadata +14 -4
- 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
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
54
|
+
|
|
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] = {}
|
|
476
524
|
|
|
477
|
-
topic_partition_list.to_h.each do |topic, partitions|
|
|
478
|
-
# Query high watermarks for this topic's partitions
|
|
479
|
-
# and compare to the offset in the list.
|
|
480
|
-
topic_out = {}
|
|
481
525
|
partitions.each do |p|
|
|
482
526
|
next if p.offset.nil?
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
p.partition,
|
|
486
|
-
watermark_timeout_ms
|
|
487
|
-
)
|
|
488
|
-
topic_out[p.partition] = high - p.offset
|
|
527
|
+
|
|
528
|
+
(request[topic] ||= []) << { partition: p.partition, offset: :latest }
|
|
489
529
|
end
|
|
490
|
-
out[topic] = topic_out
|
|
491
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
|
|
549
|
+
|
|
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|
|
|
556
|
+
partitions.each do |p|
|
|
557
|
+
next if p.offset.nil?
|
|
558
|
+
|
|
559
|
+
out[topic][p.partition] = end_offsets.fetch(topic).fetch(p.partition) - p.offset
|
|
560
|
+
end
|
|
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
|
|
|
@@ -847,8 +920,17 @@ module Rdkafka
|
|
|
847
920
|
next
|
|
848
921
|
end
|
|
849
922
|
|
|
850
|
-
|
|
851
|
-
|
|
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)
|
|
932
|
+
end
|
|
933
|
+
|
|
852
934
|
i += 1
|
|
853
935
|
end
|
|
854
936
|
ensure
|
|
@@ -913,8 +995,17 @@ module Rdkafka
|
|
|
913
995
|
next
|
|
914
996
|
end
|
|
915
997
|
|
|
916
|
-
|
|
917
|
-
|
|
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)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
918
1009
|
i += 1
|
|
919
1010
|
end
|
|
920
1011
|
ensure
|
|
@@ -994,18 +1085,68 @@ module Rdkafka
|
|
|
994
1085
|
|
|
995
1086
|
private
|
|
996
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
|
+
|
|
997
1104
|
# Checks if the consumer is closed and raises an error if so
|
|
998
1105
|
# @param method [Symbol] name of the calling method for error context
|
|
999
1106
|
# @raise [ClosedConsumerError] when the consumer is closed
|
|
1000
1107
|
def closed_consumer_check(method)
|
|
1001
1108
|
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
|
1002
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
|
|
1003
1140
|
|
|
1004
1141
|
# Returns the consumer queue pointer, lazily initialized
|
|
1005
1142
|
# @return [FFI::Pointer] consumer queue handle
|
|
1006
1143
|
def consumer_queue
|
|
1007
1144
|
@consumer_queue ||= @native_kafka.with_inner do |inner|
|
|
1008
|
-
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
|
|
1009
1150
|
end
|
|
1010
1151
|
end
|
|
1011
1152
|
|
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
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
module Helpers
|
|
5
|
+
# Shared `#list_offsets` implementation for Admin and Consumer.
|
|
6
|
+
#
|
|
7
|
+
# The librdkafka Admin API is handle-agnostic: `rd_kafka_ListOffsets()` can be issued on any
|
|
8
|
+
# `rd_kafka_t` instance regardless of its type, so this Ruby-level implementation is identical
|
|
9
|
+
# for admin and consumer clients. Includers must provide a private `#closed_check(method)`
|
|
10
|
+
# that raises their own `Closed*Error`, and must have the background event callback
|
|
11
|
+
# registered on their native config (see `Config#admin` and `Config#consumer`) so the result
|
|
12
|
+
# event is dispatched back to the returned handle.
|
|
13
|
+
module ListOffsets
|
|
14
|
+
# Queries partition offsets by specification (earliest, latest, max_timestamp, or by
|
|
15
|
+
# timestamp) without requiring a consumer group.
|
|
16
|
+
#
|
|
17
|
+
# The query is batched: all requested partitions are carried in one `ListOffsets` request
|
|
18
|
+
# that librdkafka fans out to the involved partition leaders internally and concurrently.
|
|
19
|
+
#
|
|
20
|
+
# @param topic_partition_offsets [Hash{String => Array<Hash>}] hash mapping topic names to
|
|
21
|
+
# arrays of partition offset specifications. Each specification is a hash with:
|
|
22
|
+
# - `:partition` [Integer] partition number
|
|
23
|
+
# - `:offset` [Symbol, Integer] offset specification - `:earliest`, `:latest`,
|
|
24
|
+
# `:max_timestamp`, or an integer timestamp in milliseconds
|
|
25
|
+
# @param isolation_level [Integer, nil] optional isolation level:
|
|
26
|
+
# - `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED` (0) - default
|
|
27
|
+
# - `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` (1)
|
|
28
|
+
#
|
|
29
|
+
# @return [Admin::ListOffsetsHandle] handle that can be used to wait for the result
|
|
30
|
+
#
|
|
31
|
+
# @raise [ClosedAdminError, ClosedConsumerError] when the client is closed
|
|
32
|
+
# @raise [ConfigError] when the background queue is unavailable
|
|
33
|
+
#
|
|
34
|
+
# @example Query earliest and latest offsets
|
|
35
|
+
# handle = client.list_offsets(
|
|
36
|
+
# { "my_topic" => [
|
|
37
|
+
# { partition: 0, offset: :earliest },
|
|
38
|
+
# { partition: 1, offset: :latest }
|
|
39
|
+
# ] }
|
|
40
|
+
# )
|
|
41
|
+
# report = handle.wait(max_wait_timeout_ms: 15_000)
|
|
42
|
+
# report.offsets
|
|
43
|
+
# # => [{ topic: "my_topic", partition: 0, offset: 0, ... }, ...]
|
|
44
|
+
def list_offsets(topic_partition_offsets, isolation_level: nil)
|
|
45
|
+
closed_check(__method__)
|
|
46
|
+
|
|
47
|
+
# Parse and validate every offset spec before allocating the native list, so a missing key
|
|
48
|
+
# or an unknown offset specification raises with nothing to clean up. Previously the
|
|
49
|
+
# ArgumentError (or KeyError) was raised after `rd_kafka_topic_partition_list_new`, leaking
|
|
50
|
+
# the native list.
|
|
51
|
+
parsed = topic_partition_offsets.flat_map do |topic, partitions|
|
|
52
|
+
partitions.map do |spec|
|
|
53
|
+
offset = spec.fetch(:offset)
|
|
54
|
+
|
|
55
|
+
native_offset = case offset
|
|
56
|
+
when :earliest then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_EARLIEST
|
|
57
|
+
when :latest then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_LATEST
|
|
58
|
+
when :max_timestamp then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_MAX_TIMESTAMP
|
|
59
|
+
when Integer then offset
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "Unknown offset specification: #{offset.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
[topic, spec.fetch(:partition), native_offset]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Build native topic partition list
|
|
69
|
+
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(parsed.size)
|
|
70
|
+
|
|
71
|
+
parsed.each do |topic, partition, native_offset|
|
|
72
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, partition)
|
|
73
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(tpl, topic, partition, native_offset)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get a pointer to the queue that our request will be enqueued on
|
|
77
|
+
queue_ptr = @native_kafka.with_inner do |inner|
|
|
78
|
+
Rdkafka::Bindings.rd_kafka_queue_get_background(inner)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if queue_ptr.null?
|
|
82
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
83
|
+
raise Rdkafka::Config::ConfigError.new("rd_kafka_queue_get_background was NULL")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Create and register the handle we will return to the caller
|
|
87
|
+
handle = Admin::ListOffsetsHandle.new
|
|
88
|
+
handle[:pending] = true
|
|
89
|
+
handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
90
|
+
|
|
91
|
+
admin_options_ptr = @native_kafka.with_inner do |inner|
|
|
92
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_new(
|
|
93
|
+
inner,
|
|
94
|
+
Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_LISTOFFSETS
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if isolation_level
|
|
99
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_set_isolation_level(admin_options_ptr, isolation_level)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Admin::ListOffsetsHandle.register(handle)
|
|
103
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, handle.to_ptr)
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
@native_kafka.with_inner do |inner|
|
|
107
|
+
Rdkafka::Bindings.rd_kafka_ListOffsets(
|
|
108
|
+
inner,
|
|
109
|
+
tpl,
|
|
110
|
+
admin_options_ptr,
|
|
111
|
+
queue_ptr
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
rescue Exception
|
|
115
|
+
Admin::ListOffsetsHandle.remove(handle.to_ptr.address)
|
|
116
|
+
raise
|
|
117
|
+
ensure
|
|
118
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_destroy(admin_options_ptr)
|
|
119
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue_ptr)
|
|
120
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
handle
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
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
|