karafka-rdkafka 0.27.2-aarch64-linux-gnu → 0.28.1-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 +41 -1
- data/README.md +4 -3
- 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_acl_report.rb +2 -1
- 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_records_handle.rb +31 -0
- data/lib/rdkafka/admin/delete_records_report.rb +25 -0
- 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_acl_report.rb +2 -1
- 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_consumer_groups_handle.rb +31 -0
- data/lib/rdkafka/admin/list_consumer_groups_report.rb +83 -0
- 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 +233 -145
- data/lib/rdkafka/bindings.rb +63 -16
- 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_records_handler.rb +38 -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_consumer_groups_handler.rb +39 -0
- data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
- data/lib/rdkafka/callbacks.rb +60 -246
- data/lib/rdkafka/config.rb +69 -50
- data/lib/rdkafka/consumer/headers.rb +19 -5
- data/lib/rdkafka/consumer/topic_partition_list.rb +57 -42
- data/lib/rdkafka/consumer.rb +190 -48
- data/lib/rdkafka/defaults.rb +32 -3
- data/lib/rdkafka/error.rb +14 -5
- data/lib/rdkafka/helpers/list_offsets.rb +127 -0
- data/lib/rdkafka/helpers/metadata.rb +29 -0
- data/lib/rdkafka/helpers/oauth.rb +10 -5
- data/lib/rdkafka/metadata.rb +84 -19
- data/lib/rdkafka/native_kafka.rb +20 -7
- data/lib/rdkafka/producer/delivery_handle.rb +4 -5
- data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
- data/lib/rdkafka/producer/testing.rb +2 -1
- data/lib/rdkafka/producer.rb +100 -71
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +19 -0
- data/package-lock.json +6 -6
- data/renovate.json +11 -1
- metadata +21 -5
- data/Gemfile +0 -13
- data/Gemfile.lint +0 -14
- data/Gemfile.lint.lock +0 -123
|
@@ -7,6 +7,11 @@ module Rdkafka
|
|
|
7
7
|
# Empty frozen hash used when there are no headers
|
|
8
8
|
EMPTY_HEADERS = {}.freeze
|
|
9
9
|
|
|
10
|
+
# Key under which reusable scratch pointers are stored on the current thread/fiber
|
|
11
|
+
SCRATCH_KEY = :rdkafka_headers_scratch
|
|
12
|
+
|
|
13
|
+
private_constant :SCRATCH_KEY
|
|
14
|
+
|
|
10
15
|
# Reads a librdkafka native message's headers and returns them as a Ruby Hash
|
|
11
16
|
# where each key maps to either a String (single value) or Array<String> (multiple values)
|
|
12
17
|
# to support duplicate headers per KIP-82
|
|
@@ -17,7 +22,20 @@ module Rdkafka
|
|
|
17
22
|
# @return [Hash{String => String, Array<String>}] headers Hash for the native_message
|
|
18
23
|
# @raise [Rdkafka::RdkafkaError] when fail to read headers
|
|
19
24
|
def self.from_native(native_message)
|
|
20
|
-
|
|
25
|
+
# Scratch output pointers for the header FFI calls, reused across messages. They are
|
|
26
|
+
# stored in fiber-local storage (`Thread.current[]` is fiber-local by design), so each
|
|
27
|
+
# thread and each fiber gets its own set and reuse is safe with fiber schedulers. They
|
|
28
|
+
# never escape this method and all data is read out of them before returning.
|
|
29
|
+
# Allocating them per message would cost several native allocations for every consumed
|
|
30
|
+
# message.
|
|
31
|
+
headers_ptrptr, name_ptrptr, value_ptrptr, size_ptr =
|
|
32
|
+
Thread.current[SCRATCH_KEY] ||= [
|
|
33
|
+
FFI::MemoryPointer.new(:pointer),
|
|
34
|
+
FFI::MemoryPointer.new(:pointer),
|
|
35
|
+
FFI::MemoryPointer.new(:pointer),
|
|
36
|
+
Rdkafka::Bindings::SizePtr.new
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
21
39
|
err = Rdkafka::Bindings.rd_kafka_message_headers(native_message, headers_ptrptr)
|
|
22
40
|
|
|
23
41
|
if err == Rdkafka::Bindings::RD_KAFKA_RESP_ERR__NOENT
|
|
@@ -28,10 +46,6 @@ module Rdkafka
|
|
|
28
46
|
|
|
29
47
|
headers_ptr = headers_ptrptr.read_pointer
|
|
30
48
|
|
|
31
|
-
name_ptrptr = FFI::MemoryPointer.new(:pointer)
|
|
32
|
-
value_ptrptr = FFI::MemoryPointer.new(:pointer)
|
|
33
|
-
size_ptr = Rdkafka::Bindings::SizePtr.new
|
|
34
|
-
|
|
35
49
|
headers = {}
|
|
36
50
|
|
|
37
51
|
idx = 0
|
|
@@ -6,7 +6,8 @@ module Rdkafka
|
|
|
6
6
|
class TopicPartitionList
|
|
7
7
|
# Create a topic partition list.
|
|
8
8
|
#
|
|
9
|
-
# @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an
|
|
9
|
+
# @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an
|
|
10
|
+
# empty list
|
|
10
11
|
#
|
|
11
12
|
# @return [TopicPartitionList]
|
|
12
13
|
def initialize(data = nil)
|
|
@@ -34,10 +35,12 @@ module Rdkafka
|
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Add a topic with optionally partitions to the list.
|
|
37
|
-
# Calling this method multiple times for the same topic will overwrite the previous
|
|
38
|
+
# Calling this method multiple times for the same topic will overwrite the previous
|
|
39
|
+
# configuraton.
|
|
38
40
|
#
|
|
39
41
|
# @param topic [String] The topic's name
|
|
40
|
-
# @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or
|
|
42
|
+
# @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or
|
|
43
|
+
# partition count
|
|
41
44
|
#
|
|
42
45
|
# @return [nil]
|
|
43
46
|
#
|
|
@@ -61,11 +64,12 @@ module Rdkafka
|
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
# Add a topic with partitions and offsets set to the list
|
|
64
|
-
# Calling this method multiple times for the same topic will overwrite the previous
|
|
67
|
+
# Calling this method multiple times for the same topic will overwrite the previous
|
|
68
|
+
# configuraton.
|
|
65
69
|
#
|
|
66
70
|
# @param topic [String] The topic's name
|
|
67
|
-
# @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The
|
|
68
|
-
# partitions and offsets (Hash) or partitions with offsets and metadata (Array)
|
|
71
|
+
# @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The
|
|
72
|
+
# topic's partitions and offsets (Hash) or partitions with offsets and metadata (Array)
|
|
69
73
|
# @return [nil]
|
|
70
74
|
def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
|
|
71
75
|
@data[topic.to_s] = partitions_with_offsets.map do |p, o|
|
|
@@ -73,7 +77,8 @@ module Rdkafka
|
|
|
73
77
|
end
|
|
74
78
|
end
|
|
75
79
|
|
|
76
|
-
# Return a `Hash` with the topics as keys and and an array of partition information as the
|
|
80
|
+
# Return a `Hash` with the topics as keys and and an array of partition information as the
|
|
81
|
+
# value if present.
|
|
77
82
|
#
|
|
78
83
|
# @return [Hash{String => Array<Partition>,nil}]
|
|
79
84
|
def to_h
|
|
@@ -97,7 +102,8 @@ module Rdkafka
|
|
|
97
102
|
#
|
|
98
103
|
# @private
|
|
99
104
|
#
|
|
100
|
-
# @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will
|
|
105
|
+
# @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will
|
|
106
|
+
# be copied.
|
|
101
107
|
#
|
|
102
108
|
# @return [TopicPartitionList]
|
|
103
109
|
def self.from_native_tpl(pointer)
|
|
@@ -118,7 +124,6 @@ module Rdkafka
|
|
|
118
124
|
else
|
|
119
125
|
elem[:offset]
|
|
120
126
|
end
|
|
121
|
-
|
|
122
127
|
partition = Partition.new(
|
|
123
128
|
elem[:partition],
|
|
124
129
|
offset,
|
|
@@ -136,51 +141,61 @@ module Rdkafka
|
|
|
136
141
|
|
|
137
142
|
# Create a native tpl with the contents of this object added.
|
|
138
143
|
#
|
|
139
|
-
# The pointer
|
|
144
|
+
# The returned pointer is caller-owned and must be released with
|
|
145
|
+
# `rd_kafka_topic_partition_list_destroy`. It is not cleaned up by GC.
|
|
140
146
|
#
|
|
141
147
|
# @private
|
|
142
148
|
# @return [FFI::Pointer]
|
|
143
149
|
def to_native_tpl
|
|
144
150
|
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(count)
|
|
145
151
|
|
|
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(
|
|
152
|
+
begin
|
|
153
|
+
@data.each do |topic, partitions|
|
|
154
|
+
if partitions
|
|
155
|
+
partitions.each do |p|
|
|
156
|
+
ref = Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
170
157
|
tpl,
|
|
171
158
|
topic,
|
|
172
|
-
p.partition
|
|
173
|
-
offset
|
|
159
|
+
p.partition
|
|
174
160
|
)
|
|
161
|
+
|
|
162
|
+
# Remove the respond to check after karafka 2.3.0 is released
|
|
163
|
+
if p.respond_to?(:metadata) && p.metadata
|
|
164
|
+
part = Rdkafka::Bindings::TopicPartition.new(ref)
|
|
165
|
+
str_ptr = FFI::MemoryPointer.from_string(p.metadata)
|
|
166
|
+
# released here:
|
|
167
|
+
# https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
|
|
168
|
+
str_ptr.autorelease = false
|
|
169
|
+
part[:metadata] = str_ptr
|
|
170
|
+
part[:metadata_size] = p.metadata.bytesize
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if p.offset
|
|
174
|
+
offset = p.offset.is_a?(Time) ? p.offset.to_f * 1_000 : p.offset
|
|
175
|
+
|
|
176
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(
|
|
177
|
+
tpl,
|
|
178
|
+
topic,
|
|
179
|
+
p.partition,
|
|
180
|
+
offset
|
|
181
|
+
)
|
|
182
|
+
end
|
|
175
183
|
end
|
|
184
|
+
else
|
|
185
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(
|
|
186
|
+
tpl,
|
|
187
|
+
topic,
|
|
188
|
+
Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
189
|
+
)
|
|
176
190
|
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
191
|
end
|
|
192
|
+
rescue Exception
|
|
193
|
+
# The native list is caller-owned and only handed back on success. If population raises
|
|
194
|
+
# partway (e.g. non-string metadata, or an offset FFI can't coerce to int64) we must
|
|
195
|
+
# destroy it here, otherwise the half-built list leaks.
|
|
196
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
197
|
+
|
|
198
|
+
raise
|
|
184
199
|
end
|
|
185
200
|
|
|
186
201
|
tpl
|
data/lib/rdkafka/consumer.rb
CHANGED
|
@@ -14,14 +14,48 @@ 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
|
|
39
|
+
# handle.
|
|
40
|
+
#
|
|
41
|
+
# @private
|
|
42
|
+
# @param native_kafka [NativeKafka] the wrapped native client
|
|
43
|
+
# @param queue_holder [Array] single-element holder carrying the consumer queue pointer
|
|
44
|
+
# (or empty)
|
|
45
|
+
# @return [Proc] finalizer proc that must not reference the consumer instance
|
|
46
|
+
def self.finalizer(native_kafka, queue_holder)
|
|
47
|
+
proc do
|
|
48
|
+
next if native_kafka.closed?
|
|
49
|
+
|
|
50
|
+
native_kafka.synchronize do |inner|
|
|
51
|
+
Rdkafka::Bindings.rd_kafka_consumer_close(inner)
|
|
52
|
+
|
|
53
|
+
queue = queue_holder[0]
|
|
54
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue) if queue
|
|
55
|
+
end
|
|
22
56
|
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
native_kafka.close
|
|
58
|
+
end
|
|
25
59
|
end
|
|
26
60
|
|
|
27
61
|
# Starts the native Kafka polling thread and kicks off the init polling
|
|
@@ -93,11 +127,6 @@ module Rdkafka
|
|
|
93
127
|
# @return [nil]
|
|
94
128
|
# @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
|
|
95
129
|
#
|
|
96
|
-
# @note This method holds the inner lock until the queue is empty or `:stop` is returned.
|
|
97
|
-
# Other consumer operations will wait until this method returns.
|
|
98
|
-
# @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
|
|
99
|
-
# @note Do NOT use this if `consumer_poll_set` was set to `true`
|
|
100
|
-
#
|
|
101
130
|
# @example Drain all pending events
|
|
102
131
|
# consumer.events_poll_nb_each { |_count| }
|
|
103
132
|
#
|
|
@@ -106,6 +135,10 @@ module Rdkafka
|
|
|
106
135
|
# consumer.events_poll_nb_each do |_count|
|
|
107
136
|
# :stop if monotonic_now >= deadline
|
|
108
137
|
# end
|
|
138
|
+
# @note This method holds the inner lock until the queue is empty or `:stop` is returned.
|
|
139
|
+
# Other consumer operations will wait until this method returns.
|
|
140
|
+
# @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
|
|
141
|
+
# @note Do NOT use this if `consumer_poll_set` was set to `true`
|
|
109
142
|
def events_poll_nb_each
|
|
110
143
|
closed_consumer_check(__method__)
|
|
111
144
|
|
|
@@ -131,14 +164,6 @@ module Rdkafka
|
|
|
131
164
|
# @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
|
|
132
165
|
# @raise [Rdkafka::RdkafkaError] if a Kafka error occurs while polling
|
|
133
166
|
#
|
|
134
|
-
# @note This method uses `rd_kafka_consumer_poll` to fetch messages, unlike
|
|
135
|
-
# `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
|
|
136
|
-
# statistics, etc.). For consumers, use this method to receive messages and
|
|
137
|
-
# `events_poll_nb_each` for processing background events.
|
|
138
|
-
# @note This method holds the inner lock for the duration. Other consumer operations
|
|
139
|
-
# will wait until this method returns.
|
|
140
|
-
# @note Timeout/max_messages logic should be implemented by the caller
|
|
141
|
-
#
|
|
142
167
|
# @example Process messages until queue is empty
|
|
143
168
|
# consumer.poll_nb_each do |message|
|
|
144
169
|
# process(message)
|
|
@@ -151,6 +176,13 @@ module Rdkafka
|
|
|
151
176
|
# count += 1
|
|
152
177
|
# :stop if count >= 10
|
|
153
178
|
# end
|
|
179
|
+
# @note This method uses `rd_kafka_consumer_poll` to fetch messages, unlike
|
|
180
|
+
# `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
|
|
181
|
+
# statistics, etc.). For consumers, use this method to receive messages and
|
|
182
|
+
# `events_poll_nb_each` for processing background events.
|
|
183
|
+
# @note This method holds the inner lock for the duration. Other consumer operations
|
|
184
|
+
# will wait until this method returns.
|
|
185
|
+
# @note Timeout/max_messages logic should be implemented by the caller
|
|
154
186
|
def poll_nb_each
|
|
155
187
|
closed_consumer_check(__method__)
|
|
156
188
|
|
|
@@ -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
|
|
|
@@ -404,14 +437,18 @@ module Rdkafka
|
|
|
404
437
|
end
|
|
405
438
|
|
|
406
439
|
# Return the current positions (offsets) for topics and partitions.
|
|
407
|
-
# The offset field of each requested partition will be set to the offset of the last consumed
|
|
440
|
+
# The offset field of each requested partition will be set to the offset of the last consumed
|
|
441
|
+
# message + 1, or nil in case there was no previous message.
|
|
408
442
|
#
|
|
409
|
-
# @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil
|
|
443
|
+
# @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil
|
|
444
|
+
# to use the current subscription.
|
|
410
445
|
#
|
|
411
446
|
# @return [TopicPartitionList]
|
|
412
447
|
#
|
|
413
448
|
# @raise [RdkafkaError] When getting the positions fails.
|
|
414
449
|
def position(list = nil)
|
|
450
|
+
closed_consumer_check(__method__)
|
|
451
|
+
|
|
415
452
|
if list.nil?
|
|
416
453
|
list = assignment
|
|
417
454
|
elsif !list.is_a?(TopicPartitionList)
|
|
@@ -466,39 +503,77 @@ module Rdkafka
|
|
|
466
503
|
# possible to create one yourself, in this case you have to provide a list that
|
|
467
504
|
# already contains all the partitions you need the lag for.
|
|
468
505
|
#
|
|
506
|
+
# The end offsets of all requested partitions are fetched in a single batched
|
|
507
|
+
# {#list_offsets} query - librdkafka fans it out to the involved partition leaders
|
|
508
|
+
# internally and concurrently - instead of one blocking {#query_watermark_offsets}
|
|
509
|
+
# broker roundtrip per partition.
|
|
510
|
+
#
|
|
469
511
|
# @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
|
|
470
|
-
# @param watermark_timeout_ms [Integer] The timeout for
|
|
512
|
+
# @param watermark_timeout_ms [Integer] The timeout for the batched end-offsets query.
|
|
471
513
|
# @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
|
|
472
514
|
# per partition
|
|
515
|
+
# @raise [ClosedConsumerError] when the consumer is closed
|
|
473
516
|
# @raise [RdkafkaError] When querying the broker fails.
|
|
474
517
|
def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
|
|
518
|
+
closed_consumer_check(__method__)
|
|
519
|
+
|
|
475
520
|
out = {}
|
|
521
|
+
request = {}
|
|
522
|
+
partitions_by_topic = topic_partition_list.to_h
|
|
523
|
+
|
|
524
|
+
partitions_by_topic.each do |topic, partitions|
|
|
525
|
+
out[topic] = {}
|
|
526
|
+
|
|
527
|
+
partitions.each do |p|
|
|
528
|
+
next if p.offset.nil?
|
|
529
|
+
|
|
530
|
+
(request[topic] ||= []) << { partition: p.partition, offset: :latest }
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
return out if request.empty?
|
|
535
|
+
|
|
536
|
+
report = begin
|
|
537
|
+
# The isolation level is forwarded so the end offsets match what the old per-partition
|
|
538
|
+
# watermark query returned: librdkafka resolves that query with the consumer's configured
|
|
539
|
+
# `isolation.level` (LSO for the default read_committed), while the admin-style batched
|
|
540
|
+
# query would otherwise default to read_uncommitted (true high watermark).
|
|
541
|
+
list_offsets(request, isolation_level: isolation_level)
|
|
542
|
+
.wait(max_wait_timeout_ms: watermark_timeout_ms)
|
|
543
|
+
rescue AbstractHandle::WaitTimeoutError
|
|
544
|
+
# Keep the pre-batching contract: a slow broker surfaced as a timed-out RdkafkaError
|
|
545
|
+
# from the per-partition watermark query, not as a handle wait timeout.
|
|
546
|
+
raise RdkafkaError.new(
|
|
547
|
+
Rdkafka::Bindings::RD_KAFKA_RESP_ERR__TIMED_OUT,
|
|
548
|
+
"Error querying watermark offsets of '#{request.keys.join(", ")}'"
|
|
549
|
+
)
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
end_offsets = {}
|
|
553
|
+
report.offsets.each do |result|
|
|
554
|
+
(end_offsets[result[:topic]] ||= {})[result[:partition]] = result[:offset]
|
|
555
|
+
end
|
|
476
556
|
|
|
477
|
-
|
|
478
|
-
# Query high watermarks for this topic's partitions
|
|
479
|
-
# and compare to the offset in the list.
|
|
480
|
-
topic_out = {}
|
|
557
|
+
partitions_by_topic.each do |topic, partitions|
|
|
481
558
|
partitions.each do |p|
|
|
482
559
|
next if p.offset.nil?
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
p.partition,
|
|
486
|
-
watermark_timeout_ms
|
|
487
|
-
)
|
|
488
|
-
topic_out[p.partition] = high - p.offset
|
|
560
|
+
|
|
561
|
+
out[topic][p.partition] = end_offsets.fetch(topic).fetch(p.partition) - p.offset
|
|
489
562
|
end
|
|
490
|
-
out[topic] = topic_out
|
|
491
563
|
end
|
|
564
|
+
|
|
492
565
|
out
|
|
493
566
|
end
|
|
494
567
|
|
|
495
568
|
# Returns the ClusterId as reported in broker metadata.
|
|
496
569
|
#
|
|
497
|
-
# @
|
|
498
|
-
|
|
570
|
+
# @param timeout_ms [Integer] how long to block waiting for metadata when the cluster id is
|
|
571
|
+
# not already cached (0 for a non-blocking call)
|
|
572
|
+
# @return [String, nil] the cluster id, or nil if it could not be retrieved in time
|
|
573
|
+
def cluster_id(timeout_ms = Defaults::CONSUMER_CLUSTER_ID_TIMEOUT_MS)
|
|
499
574
|
closed_consumer_check(__method__)
|
|
500
575
|
@native_kafka.with_inner do |inner|
|
|
501
|
-
Rdkafka::Bindings.rd_kafka_clusterid(inner)
|
|
576
|
+
read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_clusterid(inner, timeout_ms))
|
|
502
577
|
end
|
|
503
578
|
end
|
|
504
579
|
|
|
@@ -510,7 +585,7 @@ module Rdkafka
|
|
|
510
585
|
def member_id
|
|
511
586
|
closed_consumer_check(__method__)
|
|
512
587
|
@native_kafka.with_inner do |inner|
|
|
513
|
-
Rdkafka::Bindings.rd_kafka_memberid(inner)
|
|
588
|
+
read_and_free_native_string(inner, Rdkafka::Bindings.rd_kafka_memberid(inner))
|
|
514
589
|
end
|
|
515
590
|
end
|
|
516
591
|
|
|
@@ -519,7 +594,7 @@ module Rdkafka
|
|
|
519
594
|
# When using this `enable.auto.offset.store` should be set to `false` in the config.
|
|
520
595
|
#
|
|
521
596
|
# @param message [Rdkafka::Consumer::Message] The message which offset will be stored
|
|
522
|
-
# @param metadata [String, nil] commit metadata string
|
|
597
|
+
# @param metadata [String, nil] commit metadata string to store alongside the offset
|
|
523
598
|
# @return [nil]
|
|
524
599
|
# @raise [RdkafkaError] When storing the offset fails
|
|
525
600
|
def store_offset(message, metadata = nil)
|
|
@@ -573,9 +648,8 @@ module Rdkafka
|
|
|
573
648
|
seek_by(message.topic, message.partition, message.offset)
|
|
574
649
|
end
|
|
575
650
|
|
|
576
|
-
# Seek to a particular message by providing the topic, partition and offset.
|
|
577
|
-
#
|
|
578
|
-
# message at the given offset.
|
|
651
|
+
# Seek to a particular message by providing the topic, partition and offset. The next poll on
|
|
652
|
+
# the topic/partition will return the message at the given offset.
|
|
579
653
|
#
|
|
580
654
|
# @param topic [String] The topic in which to seek
|
|
581
655
|
# @param partition [Integer] The partition number to seek
|
|
@@ -802,7 +876,7 @@ module Rdkafka
|
|
|
802
876
|
# returns without further waiting.
|
|
803
877
|
#
|
|
804
878
|
# Error events (e.g. `:partition_eof`) are returned inline as {RdkafkaError} objects
|
|
805
|
-
# rather than raised, so callers receive the complete batch
|
|
879
|
+
# rather than raised, so callers receive the complete batch - both messages and errors -
|
|
806
880
|
# and can decide how to handle each. This is particularly useful when multiple partitions
|
|
807
881
|
# signal EOF simultaneously: all signals appear in the returned array rather than only
|
|
808
882
|
# the first one being raised and the rest silently discarded.
|
|
@@ -847,8 +921,17 @@ module Rdkafka
|
|
|
847
921
|
next
|
|
848
922
|
end
|
|
849
923
|
|
|
850
|
-
|
|
851
|
-
|
|
924
|
+
begin
|
|
925
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
926
|
+
rescue Rdkafka::RdkafkaError => e
|
|
927
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
928
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
929
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
930
|
+
results << e
|
|
931
|
+
ensure
|
|
932
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
933
|
+
end
|
|
934
|
+
|
|
852
935
|
i += 1
|
|
853
936
|
end
|
|
854
937
|
ensure
|
|
@@ -868,15 +951,15 @@ module Rdkafka
|
|
|
868
951
|
# particularly useful in fiber scheduler contexts where GVL release/reacquire
|
|
869
952
|
# overhead is wasteful since we don't expect to wait.
|
|
870
953
|
#
|
|
954
|
+
# @param timeout_ms [Integer] Timeout waiting for the first message
|
|
955
|
+
# (default: 0 for non-blocking)
|
|
956
|
+
# @param max_items [Integer] Maximum number of messages to return per call
|
|
957
|
+
# @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
|
|
958
|
+
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
871
959
|
# @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
|
|
872
960
|
# threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
|
|
873
961
|
#
|
|
874
962
|
# Error events are returned inline as {RdkafkaError} objects; see {#poll_batch} for details.
|
|
875
|
-
#
|
|
876
|
-
# @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
|
|
877
|
-
# @param max_items [Integer] Maximum number of messages to return per call
|
|
878
|
-
# @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
|
|
879
|
-
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
880
963
|
def poll_batch_nb(timeout_ms = 0, max_items: 100)
|
|
881
964
|
closed_consumer_check(__method__)
|
|
882
965
|
|
|
@@ -913,8 +996,17 @@ module Rdkafka
|
|
|
913
996
|
next
|
|
914
997
|
end
|
|
915
998
|
|
|
916
|
-
|
|
917
|
-
|
|
999
|
+
begin
|
|
1000
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
1001
|
+
rescue Rdkafka::RdkafkaError => e
|
|
1002
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
1003
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
1004
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
1005
|
+
results << e
|
|
1006
|
+
ensure
|
|
1007
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
1008
|
+
end
|
|
1009
|
+
|
|
918
1010
|
i += 1
|
|
919
1011
|
end
|
|
920
1012
|
ensure
|
|
@@ -1000,12 +1092,62 @@ module Rdkafka
|
|
|
1000
1092
|
def closed_consumer_check(method)
|
|
1001
1093
|
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
|
1002
1094
|
end
|
|
1095
|
+
alias_method :closed_check, :closed_consumer_check
|
|
1096
|
+
|
|
1097
|
+
# Reads a librdkafka-allocated string and frees the underlying native buffer.
|
|
1098
|
+
#
|
|
1099
|
+
# `rd_kafka_memberid`/`rd_kafka_clusterid` return a string the caller owns and must release
|
|
1100
|
+
# with `rd_kafka_mem_free`; without this the buffer leaks on every call.
|
|
1101
|
+
#
|
|
1102
|
+
# @param inner [FFI::Pointer] the native client handle (needed to free the buffer)
|
|
1103
|
+
# @param ptr [FFI::Pointer] the librdkafka-allocated string, possibly NULL
|
|
1104
|
+
# @return [String, nil] the copied string, or nil when the pointer was NULL
|
|
1105
|
+
def read_and_free_native_string(inner, ptr)
|
|
1106
|
+
return nil if ptr.null?
|
|
1107
|
+
|
|
1108
|
+
ptr.read_string
|
|
1109
|
+
ensure
|
|
1110
|
+
Rdkafka::Bindings.rd_kafka_mem_free(inner, ptr) unless ptr.null?
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Reads this consumer's effective `isolation.level` from the live librdkafka configuration
|
|
1114
|
+
# and maps it to the numeric isolation level constant. Memoized: the value cannot change
|
|
1115
|
+
# after client creation.
|
|
1116
|
+
#
|
|
1117
|
+
# @return [Integer] `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` or
|
|
1118
|
+
# `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED`
|
|
1119
|
+
# @raise [Rdkafka::Config::ConfigError] when the property cannot be read
|
|
1120
|
+
def isolation_level
|
|
1121
|
+
@isolation_level ||= @native_kafka.with_inner do |inner|
|
|
1122
|
+
conf = Rdkafka::Bindings.rd_kafka_conf(inner)
|
|
1123
|
+
|
|
1124
|
+
size_ptr = Rdkafka::Bindings::SizePtr.new
|
|
1125
|
+
size_ptr[:value] = 64
|
|
1126
|
+
value_ptr = FFI::MemoryPointer.new(:char, 64)
|
|
1127
|
+
|
|
1128
|
+
result = Rdkafka::Bindings.rd_kafka_conf_get(conf, "isolation.level", value_ptr, size_ptr)
|
|
1129
|
+
|
|
1130
|
+
if result != :config_ok
|
|
1131
|
+
raise Rdkafka::Config::ConfigError.new("Could not read isolation.level: #{result}")
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
if value_ptr.read_string == "read_committed"
|
|
1135
|
+
Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED
|
|
1136
|
+
else
|
|
1137
|
+
Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1003
1141
|
|
|
1004
1142
|
# Returns the consumer queue pointer, lazily initialized
|
|
1005
1143
|
# @return [FFI::Pointer] consumer queue handle
|
|
1006
1144
|
def consumer_queue
|
|
1007
1145
|
@consumer_queue ||= @native_kafka.with_inner do |inner|
|
|
1008
|
-
Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
1146
|
+
queue = Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
1147
|
+
# Share the pointer with the finalizer so it is destroyed even if the consumer is GC'd
|
|
1148
|
+
# without an explicit close.
|
|
1149
|
+
@consumer_queue_holder[0] = queue
|
|
1150
|
+
queue
|
|
1009
1151
|
end
|
|
1010
1152
|
end
|
|
1011
1153
|
|
data/lib/rdkafka/defaults.rb
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
module Rdkafka
|
|
4
4
|
# Provides default timeout and configuration values used throughout the library.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# All timeout values can be overridden per-call via method parameters. These constants provide
|
|
7
|
+
# a central place to understand and reference the default values used across the library.
|
|
8
|
+
#
|
|
9
|
+
# @example Overriding a timeout per-call
|
|
10
|
+
# consumer.committed(timeout_ms: 5_000) # Use 5 seconds instead of default 2 seconds
|
|
11
|
+
#
|
|
12
|
+
# @example Checking the default value
|
|
13
|
+
# Rdkafka::Defaults::CONSUMER_COMMITTED_TIMEOUT_MS # => 2000
|
|
14
|
+
# @note These are rdkafka-ruby defaults, not librdkafka configuration options.
|
|
15
|
+
# For librdkafka options, see:
|
|
16
|
+
# https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md
|
|
8
17
|
module Defaults
|
|
9
18
|
# Consumer timeout for fetching committed offsets
|
|
10
19
|
# @see Consumer#committed
|
|
@@ -22,6 +31,10 @@ module Rdkafka
|
|
|
22
31
|
# @see Consumer#offsets_for_times
|
|
23
32
|
CONSUMER_OFFSETS_FOR_TIMES_TIMEOUT_MS = 1_000
|
|
24
33
|
|
|
34
|
+
# Consumer timeout for fetching the cluster id when it is not already cached from metadata
|
|
35
|
+
# @see Consumer#cluster_id
|
|
36
|
+
CONSUMER_CLUSTER_ID_TIMEOUT_MS = 1_000
|
|
37
|
+
|
|
25
38
|
# Consumer timeout for poll operations (used in each iteration)
|
|
26
39
|
# @see Consumer#each
|
|
27
40
|
CONSUMER_POLL_TIMEOUT_MS = 250
|
|
@@ -55,14 +68,30 @@ module Rdkafka
|
|
|
55
68
|
# @see Admin#metadata
|
|
56
69
|
METADATA_TIMEOUT_MS = 2_000
|
|
57
70
|
|
|
58
|
-
#
|
|
71
|
+
# Hard ceiling on metadata fetch attempts on transient errors (backstop; the retry budget
|
|
72
|
+
# normally ends the loop first)
|
|
59
73
|
# @see Metadata#initialize
|
|
60
74
|
METADATA_MAX_RETRIES = 10
|
|
61
75
|
|
|
76
|
+
# Minimum metadata fetch attempts before the retry budget may end the loop, so a slow broker
|
|
77
|
+
# (whose requests each consume the full timeout) still gets a few tries
|
|
78
|
+
# @see Metadata#initialize
|
|
79
|
+
METADATA_MIN_ATTEMPTS = 3
|
|
80
|
+
|
|
81
|
+
# Soft wall-clock budget for the whole metadata retry loop; past it (and past
|
|
82
|
+
# METADATA_MIN_ATTEMPTS) the loop stops so a synchronous fetch cannot block the caller for long
|
|
83
|
+
# @see Metadata#initialize
|
|
84
|
+
METADATA_RETRY_BUDGET_MS = 5_000
|
|
85
|
+
|
|
62
86
|
# Base backoff time for metadata retry (100ms = 0.1s)
|
|
63
87
|
# @see Metadata#initialize
|
|
64
88
|
METADATA_RETRY_BACKOFF_BASE_MS = 100
|
|
65
89
|
|
|
90
|
+
# Maximum backoff time between metadata retries. Caps the exponential backoff so a long retry
|
|
91
|
+
# sequence against an unhealthy cluster cannot block the calling thread for minutes.
|
|
92
|
+
# @see Metadata#initialize
|
|
93
|
+
METADATA_RETRY_BACKOFF_MAX_MS = 1_000
|
|
94
|
+
|
|
66
95
|
# Default wait timeout for operation handles
|
|
67
96
|
# @see AbstractHandle#wait
|
|
68
97
|
HANDLE_WAIT_TIMEOUT_MS = 60_000
|