rdkafka 0.22.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -3
- data/Gemfile +8 -0
- data/Gemfile.lint +14 -0
- data/Gemfile.lint.lock +123 -0
- data/README.md +19 -14
- data/Rakefile +21 -21
- data/bin/verify_kafka_warnings +39 -0
- data/dist/{librdkafka-2.8.0.tar.gz → librdkafka-2.14.0.tar.gz} +0 -0
- data/docker-compose-ssl.yml +35 -0
- data/docker-compose.yml +2 -2
- data/ext/Rakefile +27 -27
- data/lib/rdkafka/abstract_handle.rb +23 -5
- data/lib/rdkafka/admin/acl_binding_result.rb +5 -5
- data/lib/rdkafka/admin/config_resource_binding_result.rb +1 -0
- data/lib/rdkafka/admin/create_acl_handle.rb +7 -4
- data/lib/rdkafka/admin/create_acl_report.rb +3 -2
- data/lib/rdkafka/admin/create_partitions_handle.rb +8 -5
- data/lib/rdkafka/admin/create_partitions_report.rb +1 -0
- data/lib/rdkafka/admin/create_topic_handle.rb +8 -5
- data/lib/rdkafka/admin/create_topic_report.rb +3 -0
- data/lib/rdkafka/admin/delete_acl_handle.rb +9 -6
- data/lib/rdkafka/admin/delete_acl_report.rb +5 -3
- data/lib/rdkafka/admin/delete_groups_handle.rb +10 -5
- data/lib/rdkafka/admin/delete_groups_report.rb +3 -0
- data/lib/rdkafka/admin/delete_topic_handle.rb +8 -5
- data/lib/rdkafka/admin/delete_topic_report.rb +3 -0
- data/lib/rdkafka/admin/describe_acl_handle.rb +9 -6
- data/lib/rdkafka/admin/describe_acl_report.rb +5 -3
- data/lib/rdkafka/admin/describe_configs_handle.rb +7 -4
- data/lib/rdkafka/admin/describe_configs_report.rb +7 -1
- data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +7 -4
- data/lib/rdkafka/admin/incremental_alter_configs_report.rb +7 -1
- data/lib/rdkafka/admin/list_offsets_handle.rb +36 -0
- data/lib/rdkafka/admin/list_offsets_report.rb +51 -0
- data/lib/rdkafka/admin.rb +301 -135
- data/lib/rdkafka/bindings.rb +199 -110
- data/lib/rdkafka/callbacks.rb +124 -21
- data/lib/rdkafka/config.rb +81 -33
- data/lib/rdkafka/consumer/headers.rb +3 -2
- data/lib/rdkafka/consumer/message.rb +12 -11
- data/lib/rdkafka/consumer/partition.rb +8 -4
- data/lib/rdkafka/consumer/topic_partition_list.rb +21 -17
- data/lib/rdkafka/consumer.rb +397 -45
- data/lib/rdkafka/defaults.rb +106 -0
- data/lib/rdkafka/error.rb +40 -14
- data/lib/rdkafka/helpers/oauth.rb +45 -13
- data/lib/rdkafka/helpers/time.rb +5 -0
- data/lib/rdkafka/metadata.rb +45 -21
- data/lib/rdkafka/native_kafka.rb +89 -4
- data/lib/rdkafka/producer/delivery_handle.rb +5 -5
- data/lib/rdkafka/producer/delivery_report.rb +10 -6
- data/lib/rdkafka/producer/partitions_count_cache.rb +29 -19
- data/lib/rdkafka/producer.rb +168 -82
- data/lib/rdkafka/version.rb +6 -3
- data/lib/rdkafka.rb +3 -0
- data/package-lock.json +331 -0
- data/package.json +9 -0
- data/rdkafka.gemspec +57 -36
- data/renovate.json +29 -24
- metadata +29 -124
- data/.github/CODEOWNERS +0 -3
- data/.github/FUNDING.yml +0 -1
- data/.github/workflows/ci_linux_x86_64_gnu.yml +0 -271
- data/.github/workflows/ci_linux_x86_64_musl.yml +0 -194
- data/.github/workflows/ci_macos_arm64.yml +0 -284
- data/.github/workflows/push_linux_x86_64_gnu.yml +0 -65
- data/.github/workflows/push_linux_x86_64_musl.yml +0 -79
- data/.github/workflows/push_macos_arm64.yml +0 -54
- data/.github/workflows/push_ruby.yml +0 -37
- data/.github/workflows/verify-action-pins.yml +0 -16
- data/.gitignore +0 -14
- data/.rspec +0 -2
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.yardopts +0 -2
- data/ext/README.md +0 -19
- data/ext/build_common.sh +0 -361
- data/ext/build_linux_x86_64_gnu.sh +0 -306
- data/ext/build_linux_x86_64_musl.sh +0 -763
- data/ext/build_macos_arm64.sh +0 -550
- data/spec/rdkafka/abstract_handle_spec.rb +0 -117
- data/spec/rdkafka/admin/create_acl_handle_spec.rb +0 -56
- data/spec/rdkafka/admin/create_acl_report_spec.rb +0 -18
- data/spec/rdkafka/admin/create_topic_handle_spec.rb +0 -52
- data/spec/rdkafka/admin/create_topic_report_spec.rb +0 -16
- data/spec/rdkafka/admin/delete_acl_handle_spec.rb +0 -85
- data/spec/rdkafka/admin/delete_acl_report_spec.rb +0 -72
- data/spec/rdkafka/admin/delete_topic_handle_spec.rb +0 -52
- data/spec/rdkafka/admin/delete_topic_report_spec.rb +0 -16
- data/spec/rdkafka/admin/describe_acl_handle_spec.rb +0 -85
- data/spec/rdkafka/admin/describe_acl_report_spec.rb +0 -73
- data/spec/rdkafka/admin_spec.rb +0 -971
- data/spec/rdkafka/bindings_spec.rb +0 -199
- data/spec/rdkafka/callbacks_spec.rb +0 -20
- data/spec/rdkafka/config_spec.rb +0 -258
- data/spec/rdkafka/consumer/headers_spec.rb +0 -73
- data/spec/rdkafka/consumer/message_spec.rb +0 -139
- data/spec/rdkafka/consumer/partition_spec.rb +0 -57
- data/spec/rdkafka/consumer/topic_partition_list_spec.rb +0 -248
- data/spec/rdkafka/consumer_spec.rb +0 -1274
- data/spec/rdkafka/error_spec.rb +0 -89
- data/spec/rdkafka/metadata_spec.rb +0 -79
- data/spec/rdkafka/native_kafka_spec.rb +0 -130
- data/spec/rdkafka/producer/delivery_handle_spec.rb +0 -45
- data/spec/rdkafka/producer/delivery_report_spec.rb +0 -25
- data/spec/rdkafka/producer/partitions_count_cache_spec.rb +0 -359
- data/spec/rdkafka/producer_spec.rb +0 -1345
- data/spec/spec_helper.rb +0 -195
data/lib/rdkafka/consumer.rb
CHANGED
|
@@ -16,8 +16,12 @@ module Rdkafka
|
|
|
16
16
|
include Helpers::OAuth
|
|
17
17
|
|
|
18
18
|
# @private
|
|
19
|
+
# @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
|
|
19
20
|
def initialize(native_kafka)
|
|
20
21
|
@native_kafka = native_kafka
|
|
22
|
+
|
|
23
|
+
# Makes sure, that native kafka gets closed before it gets GCed by Ruby
|
|
24
|
+
ObjectSpace.define_finalizer(self, native_kafka.finalizer)
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# Starts the native Kafka polling thread and kicks off the init polling
|
|
@@ -33,8 +37,142 @@ module Rdkafka
|
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
# Enable IO event notifications for fiber scheduler integration
|
|
41
|
+
# When the consumer queue has messages, librdkafka will write to your FD
|
|
42
|
+
#
|
|
43
|
+
# @param fd [Integer] file descriptor to signal (from IO.pipe or eventfd)
|
|
44
|
+
# @param payload [String] data to write to fd (default: "\x01")
|
|
45
|
+
# @return [nil]
|
|
46
|
+
# @raise [ClosedInnerError] when the consumer is closed
|
|
47
|
+
#
|
|
48
|
+
# @example Using with fiber scheduler
|
|
49
|
+
# consumer = config.consumer
|
|
50
|
+
# consumer.subscribe("topic")
|
|
51
|
+
#
|
|
52
|
+
# # Create notification FD
|
|
53
|
+
# signal_r, signal_w = IO.pipe
|
|
54
|
+
#
|
|
55
|
+
# # Enable librdkafka to signal when messages arrive
|
|
56
|
+
# consumer.enable_queue_io_events(signal_w.fileno)
|
|
57
|
+
#
|
|
58
|
+
# # Monitor with select/poll
|
|
59
|
+
# loop do
|
|
60
|
+
# readable, = IO.select([signal_r], nil, nil, timeout)
|
|
61
|
+
# if readable
|
|
62
|
+
# signal_r.read_nonblock(1024) rescue nil # Drain signal
|
|
63
|
+
# while msg = consumer.poll(0)
|
|
64
|
+
# process(msg)
|
|
65
|
+
# end
|
|
66
|
+
# end
|
|
67
|
+
# end
|
|
68
|
+
def enable_queue_io_events(fd, payload = "\x01")
|
|
69
|
+
@native_kafka.enable_main_queue_io_events(fd, payload)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Enable IO event notifications for background events
|
|
73
|
+
# @param fd [Integer] file descriptor to signal (from IO.pipe or eventfd)
|
|
74
|
+
# @param payload [String] data to write to fd (default: "\x01")
|
|
75
|
+
# @return [nil]
|
|
76
|
+
# @raise [ClosedInnerError] when the consumer is closed
|
|
77
|
+
def enable_background_queue_io_events(fd, payload = "\x01")
|
|
78
|
+
@native_kafka.enable_background_queue_io_events(fd, payload)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Polls for events in a non-blocking loop, yielding the count after each iteration.
|
|
82
|
+
#
|
|
83
|
+
# This method processes events (stats, errors, etc.) in a single GVL/mutex session,
|
|
84
|
+
# which is more efficient than repeated individual polls. It uses non-blocking polls
|
|
85
|
+
# internally (no GVL release between polls).
|
|
86
|
+
#
|
|
87
|
+
# Yields the count of events processed after each poll iteration, allowing the caller
|
|
88
|
+
# to implement timeout or other termination logic by returning `:stop`.
|
|
89
|
+
#
|
|
90
|
+
# @yield [count] Called after each poll iteration
|
|
91
|
+
# @yieldparam count [Integer] Number of events processed in this iteration
|
|
92
|
+
# @yieldreturn [Symbol, Object] Return `:stop` to break the loop, any other value continues
|
|
93
|
+
# @return [nil]
|
|
94
|
+
# @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
|
|
95
|
+
#
|
|
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
|
+
# @example Drain all pending events
|
|
102
|
+
# consumer.events_poll_nb_each { |_count| }
|
|
103
|
+
#
|
|
104
|
+
# @example With timeout control
|
|
105
|
+
# deadline = monotonic_now + timeout_ms
|
|
106
|
+
# consumer.events_poll_nb_each do |_count|
|
|
107
|
+
# :stop if monotonic_now >= deadline
|
|
108
|
+
# end
|
|
109
|
+
def events_poll_nb_each
|
|
110
|
+
closed_consumer_check(__method__)
|
|
111
|
+
|
|
112
|
+
@native_kafka.with_inner do |inner|
|
|
113
|
+
loop do
|
|
114
|
+
count = Rdkafka::Bindings.rd_kafka_poll_nb(inner, 0)
|
|
115
|
+
break if count.zero?
|
|
116
|
+
break if yield(count) == :stop
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Polls for messages in a non-blocking loop, yielding each message to the caller.
|
|
122
|
+
#
|
|
123
|
+
# This method processes messages in a single GVL/mutex session until the queue is empty
|
|
124
|
+
# or the caller returns `:stop`. It handles the message pointer lifecycle internally,
|
|
125
|
+
# ensuring proper cleanup via `rd_kafka_message_destroy`.
|
|
126
|
+
#
|
|
127
|
+
# @yield [message] Called for each message received
|
|
128
|
+
# @yieldparam message [Consumer::Message] The received message
|
|
129
|
+
# @yieldreturn [Symbol, Object] Return `:stop` to break the loop, any other value continues
|
|
130
|
+
# @return [nil]
|
|
131
|
+
# @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
|
|
132
|
+
# @raise [Rdkafka::RdkafkaError] if a Kafka error occurs while polling
|
|
133
|
+
#
|
|
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
|
+
# @example Process messages until queue is empty
|
|
143
|
+
# consumer.poll_nb_each do |message|
|
|
144
|
+
# process(message)
|
|
145
|
+
# end
|
|
146
|
+
#
|
|
147
|
+
# @example Process with early termination
|
|
148
|
+
# count = 0
|
|
149
|
+
# consumer.poll_nb_each do |message|
|
|
150
|
+
# process(message)
|
|
151
|
+
# count += 1
|
|
152
|
+
# :stop if count >= 10
|
|
153
|
+
# end
|
|
154
|
+
def poll_nb_each
|
|
155
|
+
closed_consumer_check(__method__)
|
|
156
|
+
|
|
157
|
+
@native_kafka.with_inner do |inner|
|
|
158
|
+
loop do
|
|
159
|
+
message_ptr = Rdkafka::Bindings.rd_kafka_consumer_poll_nb(inner, 0)
|
|
160
|
+
break if message_ptr.null?
|
|
161
|
+
|
|
162
|
+
begin
|
|
163
|
+
native_message = Rdkafka::Bindings::Message.new(message_ptr)
|
|
164
|
+
|
|
165
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
166
|
+
raise Rdkafka::RdkafkaError.new(native_message[:err])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
result = yield Consumer::Message.new(native_message)
|
|
170
|
+
break if result == :stop
|
|
171
|
+
ensure
|
|
172
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
38
176
|
end
|
|
39
177
|
|
|
40
178
|
# Close this consumer
|
|
@@ -45,6 +183,11 @@ module Rdkafka
|
|
|
45
183
|
|
|
46
184
|
@native_kafka.synchronize do |inner|
|
|
47
185
|
Rdkafka::Bindings.rd_kafka_consumer_close(inner)
|
|
186
|
+
|
|
187
|
+
if @consumer_queue
|
|
188
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(@consumer_queue)
|
|
189
|
+
@consumer_queue = nil
|
|
190
|
+
end
|
|
48
191
|
end
|
|
49
192
|
|
|
50
193
|
@native_kafka.close
|
|
@@ -67,15 +210,15 @@ module Rdkafka
|
|
|
67
210
|
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
|
|
68
211
|
|
|
69
212
|
topics.each do |topic|
|
|
70
|
-
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic,
|
|
213
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, Rdkafka::Bindings::RD_KAFKA_PARTITION_UA)
|
|
71
214
|
end
|
|
72
215
|
|
|
73
216
|
# Subscribe to topic partition list and check this was successful
|
|
74
217
|
response = @native_kafka.with_inner do |inner|
|
|
75
218
|
Rdkafka::Bindings.rd_kafka_subscribe(inner, tpl)
|
|
76
219
|
end
|
|
77
|
-
if response !=
|
|
78
|
-
raise Rdkafka::RdkafkaError.new(response, "Error subscribing to '#{topics.join(
|
|
220
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
221
|
+
raise Rdkafka::RdkafkaError.new(response, "Error subscribing to '#{topics.join(", ")}'")
|
|
79
222
|
end
|
|
80
223
|
ensure
|
|
81
224
|
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) unless tpl.nil?
|
|
@@ -91,7 +234,7 @@ module Rdkafka
|
|
|
91
234
|
response = @native_kafka.with_inner do |inner|
|
|
92
235
|
Rdkafka::Bindings.rd_kafka_unsubscribe(inner)
|
|
93
236
|
end
|
|
94
|
-
if response !=
|
|
237
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
95
238
|
raise Rdkafka::RdkafkaError.new(response)
|
|
96
239
|
end
|
|
97
240
|
end
|
|
@@ -115,7 +258,7 @@ module Rdkafka
|
|
|
115
258
|
Rdkafka::Bindings.rd_kafka_pause_partitions(inner, tpl)
|
|
116
259
|
end
|
|
117
260
|
|
|
118
|
-
if response !=
|
|
261
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
119
262
|
list = TopicPartitionList.from_native_tpl(tpl)
|
|
120
263
|
raise Rdkafka::RdkafkaTopicPartitionListError.new(response, list, "Error pausing '#{list.to_h}'")
|
|
121
264
|
end
|
|
@@ -142,7 +285,7 @@ module Rdkafka
|
|
|
142
285
|
response = @native_kafka.with_inner do |inner|
|
|
143
286
|
Rdkafka::Bindings.rd_kafka_resume_partitions(inner, tpl)
|
|
144
287
|
end
|
|
145
|
-
if response !=
|
|
288
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
146
289
|
raise Rdkafka::RdkafkaError.new(response, "Error resume '#{list.to_h}'")
|
|
147
290
|
end
|
|
148
291
|
ensure
|
|
@@ -162,7 +305,7 @@ module Rdkafka
|
|
|
162
305
|
Rdkafka::Bindings.rd_kafka_subscription(inner, ptr)
|
|
163
306
|
end
|
|
164
307
|
|
|
165
|
-
if response !=
|
|
308
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
166
309
|
raise Rdkafka::RdkafkaError.new(response)
|
|
167
310
|
end
|
|
168
311
|
|
|
@@ -192,7 +335,7 @@ module Rdkafka
|
|
|
192
335
|
response = @native_kafka.with_inner do |inner|
|
|
193
336
|
Rdkafka::Bindings.rd_kafka_assign(inner, tpl)
|
|
194
337
|
end
|
|
195
|
-
if response !=
|
|
338
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
196
339
|
raise Rdkafka::RdkafkaError.new(response, "Error assigning '#{list.to_h}'")
|
|
197
340
|
end
|
|
198
341
|
ensure
|
|
@@ -211,7 +354,7 @@ module Rdkafka
|
|
|
211
354
|
response = @native_kafka.with_inner do |inner|
|
|
212
355
|
Rdkafka::Bindings.rd_kafka_assignment(inner, ptr)
|
|
213
356
|
end
|
|
214
|
-
if response !=
|
|
357
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
215
358
|
raise Rdkafka::RdkafkaError.new(response)
|
|
216
359
|
end
|
|
217
360
|
|
|
@@ -225,7 +368,7 @@ module Rdkafka
|
|
|
225
368
|
end
|
|
226
369
|
end
|
|
227
370
|
ensure
|
|
228
|
-
ptr
|
|
371
|
+
ptr&.free
|
|
229
372
|
end
|
|
230
373
|
|
|
231
374
|
# @return [Boolean] true if our current assignment has been lost involuntarily.
|
|
@@ -246,7 +389,7 @@ module Rdkafka
|
|
|
246
389
|
# @param timeout_ms [Integer] The timeout for fetching this information.
|
|
247
390
|
# @return [TopicPartitionList]
|
|
248
391
|
# @raise [RdkafkaError] When getting the committed positions fails.
|
|
249
|
-
def committed(list=nil, timeout_ms=
|
|
392
|
+
def committed(list = nil, timeout_ms = Defaults::CONSUMER_COMMITTED_TIMEOUT_MS)
|
|
250
393
|
closed_consumer_check(__method__)
|
|
251
394
|
|
|
252
395
|
if list.nil?
|
|
@@ -261,7 +404,7 @@ module Rdkafka
|
|
|
261
404
|
response = @native_kafka.with_inner do |inner|
|
|
262
405
|
Rdkafka::Bindings.rd_kafka_committed(inner, tpl, timeout_ms)
|
|
263
406
|
end
|
|
264
|
-
if response !=
|
|
407
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
265
408
|
raise Rdkafka::RdkafkaError.new(response)
|
|
266
409
|
end
|
|
267
410
|
TopicPartitionList.from_native_tpl(tpl)
|
|
@@ -278,7 +421,7 @@ module Rdkafka
|
|
|
278
421
|
# @return [TopicPartitionList]
|
|
279
422
|
#
|
|
280
423
|
# @raise [RdkafkaError] When getting the positions fails.
|
|
281
|
-
def position(list=nil)
|
|
424
|
+
def position(list = nil)
|
|
282
425
|
if list.nil?
|
|
283
426
|
list = assignment
|
|
284
427
|
elsif !list.is_a?(TopicPartitionList)
|
|
@@ -291,11 +434,13 @@ module Rdkafka
|
|
|
291
434
|
Rdkafka::Bindings.rd_kafka_position(inner, tpl)
|
|
292
435
|
end
|
|
293
436
|
|
|
294
|
-
if response !=
|
|
437
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
295
438
|
raise Rdkafka::RdkafkaError.new(response)
|
|
296
439
|
end
|
|
297
440
|
|
|
298
441
|
TopicPartitionList.from_native_tpl(tpl)
|
|
442
|
+
ensure
|
|
443
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
|
|
299
444
|
end
|
|
300
445
|
|
|
301
446
|
# Query broker for low (oldest/beginning) and high (newest/end) offsets for a partition.
|
|
@@ -305,7 +450,7 @@ module Rdkafka
|
|
|
305
450
|
# @param timeout_ms [Integer] The timeout for querying the broker
|
|
306
451
|
# @return [Integer] The low and high watermark
|
|
307
452
|
# @raise [RdkafkaError] When querying the broker fails.
|
|
308
|
-
def query_watermark_offsets(topic, partition, timeout_ms=
|
|
453
|
+
def query_watermark_offsets(topic, partition, timeout_ms = Defaults::CONSUMER_QUERY_WATERMARK_TIMEOUT_MS)
|
|
309
454
|
closed_consumer_check(__method__)
|
|
310
455
|
|
|
311
456
|
low = FFI::MemoryPointer.new(:int64, 1)
|
|
@@ -318,17 +463,17 @@ module Rdkafka
|
|
|
318
463
|
partition,
|
|
319
464
|
low,
|
|
320
465
|
high,
|
|
321
|
-
timeout_ms
|
|
466
|
+
timeout_ms
|
|
322
467
|
)
|
|
323
468
|
end
|
|
324
|
-
if response !=
|
|
469
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
325
470
|
raise Rdkafka::RdkafkaError.new(response, "Error querying watermark offsets for partition #{partition} of #{topic}")
|
|
326
471
|
end
|
|
327
472
|
|
|
328
|
-
|
|
473
|
+
[low.read_array_of_int64(1).first, high.read_array_of_int64(1).first]
|
|
329
474
|
ensure
|
|
330
|
-
low
|
|
331
|
-
high
|
|
475
|
+
low&.free
|
|
476
|
+
high&.free
|
|
332
477
|
end
|
|
333
478
|
|
|
334
479
|
# Calculate the consumer lag per partition for the provided topic partition list.
|
|
@@ -338,10 +483,10 @@ module Rdkafka
|
|
|
338
483
|
#
|
|
339
484
|
# @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
|
|
340
485
|
# @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
|
|
341
|
-
# @return [Hash
|
|
486
|
+
# @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
|
|
342
487
|
# per partition
|
|
343
488
|
# @raise [RdkafkaError] When querying the broker fails.
|
|
344
|
-
def lag(topic_partition_list, watermark_timeout_ms=
|
|
489
|
+
def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
|
|
345
490
|
out = {}
|
|
346
491
|
|
|
347
492
|
topic_partition_list.to_h.each do |topic, partitions|
|
|
@@ -350,7 +495,7 @@ module Rdkafka
|
|
|
350
495
|
topic_out = {}
|
|
351
496
|
partitions.each do |p|
|
|
352
497
|
next if p.offset.nil?
|
|
353
|
-
|
|
498
|
+
_low, high = query_watermark_offsets(
|
|
354
499
|
topic,
|
|
355
500
|
p.partition,
|
|
356
501
|
watermark_timeout_ms
|
|
@@ -409,7 +554,7 @@ module Rdkafka
|
|
|
409
554
|
)
|
|
410
555
|
end
|
|
411
556
|
|
|
412
|
-
if response !=
|
|
557
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
413
558
|
raise Rdkafka::RdkafkaError.new(response)
|
|
414
559
|
end
|
|
415
560
|
ensure
|
|
@@ -451,9 +596,9 @@ module Rdkafka
|
|
|
451
596
|
native_topic,
|
|
452
597
|
partition,
|
|
453
598
|
offset,
|
|
454
|
-
|
|
599
|
+
Defaults::CONSUMER_SEEK_TIMEOUT_MS
|
|
455
600
|
)
|
|
456
|
-
if response !=
|
|
601
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
457
602
|
raise Rdkafka::RdkafkaError.new(response)
|
|
458
603
|
end
|
|
459
604
|
ensure
|
|
@@ -465,11 +610,10 @@ module Rdkafka
|
|
|
465
610
|
# Lookup offset for the given partitions by timestamp.
|
|
466
611
|
#
|
|
467
612
|
# @param list [TopicPartitionList] The TopicPartitionList with timestamps instead of offsets
|
|
468
|
-
#
|
|
613
|
+
# @param timeout_ms [Integer] timeout in milliseconds for the operation
|
|
469
614
|
# @return [TopicPartitionList]
|
|
470
|
-
#
|
|
471
615
|
# @raise [RdKafkaError] When the OffsetForTimes lookup fails
|
|
472
|
-
def offsets_for_times(list, timeout_ms =
|
|
616
|
+
def offsets_for_times(list, timeout_ms = Defaults::CONSUMER_OFFSETS_FOR_TIMES_TIMEOUT_MS)
|
|
473
617
|
closed_consumer_check(__method__)
|
|
474
618
|
|
|
475
619
|
if !list.is_a?(TopicPartitionList)
|
|
@@ -486,7 +630,7 @@ module Rdkafka
|
|
|
486
630
|
)
|
|
487
631
|
end
|
|
488
632
|
|
|
489
|
-
if response !=
|
|
633
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
490
634
|
raise Rdkafka::RdkafkaError.new(response)
|
|
491
635
|
end
|
|
492
636
|
|
|
@@ -508,20 +652,20 @@ module Rdkafka
|
|
|
508
652
|
# @param async [Boolean] Whether to commit async or wait for the commit to finish
|
|
509
653
|
# @return [nil]
|
|
510
654
|
# @raise [RdkafkaError] When committing fails
|
|
511
|
-
def commit(list=nil, async=false)
|
|
655
|
+
def commit(list = nil, async = false)
|
|
512
656
|
closed_consumer_check(__method__)
|
|
513
657
|
|
|
514
658
|
if !list.nil? && !list.is_a?(TopicPartitionList)
|
|
515
659
|
raise TypeError.new("list has to be nil or a TopicPartitionList")
|
|
516
660
|
end
|
|
517
661
|
|
|
518
|
-
tpl = list
|
|
662
|
+
tpl = list&.to_native_tpl
|
|
519
663
|
|
|
520
664
|
begin
|
|
521
665
|
response = @native_kafka.with_inner do |inner|
|
|
522
666
|
Rdkafka::Bindings.rd_kafka_commit(inner, tpl, async)
|
|
523
667
|
end
|
|
524
|
-
if response !=
|
|
668
|
+
if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
525
669
|
raise Rdkafka::RdkafkaError.new(response)
|
|
526
670
|
end
|
|
527
671
|
ensure
|
|
@@ -546,7 +690,48 @@ module Rdkafka
|
|
|
546
690
|
# Create struct wrapper
|
|
547
691
|
native_message = Rdkafka::Bindings::Message.new(message_ptr)
|
|
548
692
|
# Raise error if needed
|
|
549
|
-
if native_message[:err] !=
|
|
693
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
694
|
+
raise Rdkafka::RdkafkaError.new(native_message[:err])
|
|
695
|
+
end
|
|
696
|
+
# Create a message to pass out
|
|
697
|
+
Rdkafka::Consumer::Message.new(native_message)
|
|
698
|
+
end
|
|
699
|
+
ensure
|
|
700
|
+
# Clean up rdkafka message if there is one
|
|
701
|
+
if message_ptr && !message_ptr.null?
|
|
702
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(message_ptr)
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# Poll for the next message without releasing the GVL (Global VM Lock).
|
|
707
|
+
#
|
|
708
|
+
# This is more efficient than regular polling for non-blocking poll(0) calls,
|
|
709
|
+
# particularly useful in fiber scheduler contexts where GVL release/reacquire
|
|
710
|
+
# overhead is wasteful since we don't expect to wait.
|
|
711
|
+
#
|
|
712
|
+
# @param timeout_ms [Integer] Timeout of this poll (default: 0 for non-blocking)
|
|
713
|
+
# @return [Message, nil] A message or nil if there was no new message within the timeout
|
|
714
|
+
# @raise [RdkafkaError] When polling fails
|
|
715
|
+
#
|
|
716
|
+
# @example Using with fiber scheduler
|
|
717
|
+
# # After receiving IO notification that messages are available
|
|
718
|
+
# while msg = consumer.poll_nb
|
|
719
|
+
# process(msg)
|
|
720
|
+
# end
|
|
721
|
+
def poll_nb(timeout_ms = 0)
|
|
722
|
+
closed_consumer_check(__method__)
|
|
723
|
+
|
|
724
|
+
message_ptr = @native_kafka.with_inner do |inner|
|
|
725
|
+
Rdkafka::Bindings.rd_kafka_consumer_poll_nb(inner, timeout_ms)
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
if message_ptr.null?
|
|
729
|
+
nil
|
|
730
|
+
else
|
|
731
|
+
# Create struct wrapper
|
|
732
|
+
native_message = Rdkafka::Bindings::Message.new(message_ptr)
|
|
733
|
+
# Raise error if needed
|
|
734
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
550
735
|
raise Rdkafka::RdkafkaError.new(native_message[:err])
|
|
551
736
|
end
|
|
552
737
|
# Create a message to pass out
|
|
@@ -579,37 +764,182 @@ module Rdkafka
|
|
|
579
764
|
# @note This method technically should be called `#poll` and the current `#poll` should be
|
|
580
765
|
# called `#consumer_poll` though we keep the current naming convention to make it backward
|
|
581
766
|
# compatible.
|
|
582
|
-
def events_poll(timeout_ms =
|
|
767
|
+
def events_poll(timeout_ms = Defaults::CONSUMER_EVENTS_POLL_TIMEOUT_MS)
|
|
583
768
|
@native_kafka.with_inner do |inner|
|
|
584
769
|
Rdkafka::Bindings.rd_kafka_poll(inner, timeout_ms)
|
|
585
770
|
end
|
|
586
771
|
end
|
|
587
772
|
|
|
773
|
+
# Polls the main rdkafka queue without releasing the GVL (Global VM Lock).
|
|
774
|
+
#
|
|
775
|
+
# This is more efficient than regular events_poll for non-blocking poll(0) calls,
|
|
776
|
+
# particularly useful in fiber scheduler contexts where GVL release/reacquire
|
|
777
|
+
# overhead is wasteful since we don't expect to wait.
|
|
778
|
+
#
|
|
779
|
+
# @param timeout_ms [Integer] poll timeout (default: 0 for non-blocking)
|
|
780
|
+
# @return [Integer] the number of events served
|
|
781
|
+
#
|
|
782
|
+
# @see #events_poll for more details on when to use this method
|
|
783
|
+
def events_poll_nb(timeout_ms = 0)
|
|
784
|
+
@native_kafka.with_inner do |inner|
|
|
785
|
+
Rdkafka::Bindings.rd_kafka_poll_nb(inner, timeout_ms)
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Poll for a batch of messages from the consumer queue in a single FFI call.
|
|
790
|
+
#
|
|
791
|
+
# This is more efficient than calling {#poll} in a loop because it crosses the FFI
|
|
792
|
+
# boundary only once to fetch up to `max_items` messages.
|
|
793
|
+
#
|
|
794
|
+
# The timeout controls how long to wait for the **first** message. Once any message
|
|
795
|
+
# is available, librdkafka fills the buffer with whatever is immediately ready and
|
|
796
|
+
# returns without further waiting.
|
|
797
|
+
#
|
|
798
|
+
# @param timeout_ms [Integer] Timeout waiting for the first message (-1 for infinite)
|
|
799
|
+
# @param max_items [Integer] Maximum number of messages to return per call
|
|
800
|
+
# @return [Array<Message>] Array of messages (empty if none available within timeout)
|
|
801
|
+
# @raise [RdkafkaError] When a consumed message contains an error
|
|
802
|
+
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
803
|
+
def poll_batch(timeout_ms, max_items: 100)
|
|
804
|
+
closed_consumer_check(__method__)
|
|
805
|
+
|
|
806
|
+
buffer = batch_buffer(max_items)
|
|
807
|
+
messages = []
|
|
808
|
+
|
|
809
|
+
count = @native_kafka.with_inner do |_inner|
|
|
810
|
+
Rdkafka::Bindings.rd_kafka_consume_batch_queue(
|
|
811
|
+
consumer_queue,
|
|
812
|
+
timeout_ms,
|
|
813
|
+
buffer,
|
|
814
|
+
max_items
|
|
815
|
+
)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
return messages if count <= 0
|
|
819
|
+
|
|
820
|
+
i = 0
|
|
821
|
+
begin
|
|
822
|
+
while i < count
|
|
823
|
+
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
824
|
+
|
|
825
|
+
if ptr.null?
|
|
826
|
+
i += 1
|
|
827
|
+
next
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
831
|
+
|
|
832
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
833
|
+
raise Rdkafka::RdkafkaError.new(native_message[:err])
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
messages << Rdkafka::Consumer::Message.new(native_message)
|
|
837
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
838
|
+
i += 1
|
|
839
|
+
end
|
|
840
|
+
ensure
|
|
841
|
+
while i < count
|
|
842
|
+
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
843
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr) unless ptr.null?
|
|
844
|
+
i += 1
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
messages
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
# Poll for a batch of messages without releasing the GVL (Global VM Lock).
|
|
852
|
+
#
|
|
853
|
+
# This is more efficient than {#poll_batch} for non-blocking poll(0) calls,
|
|
854
|
+
# particularly useful in fiber scheduler contexts where GVL release/reacquire
|
|
855
|
+
# overhead is wasteful since we don't expect to wait.
|
|
856
|
+
#
|
|
857
|
+
# @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
|
|
858
|
+
# threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
|
|
859
|
+
#
|
|
860
|
+
# @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
|
|
861
|
+
# @param max_items [Integer] Maximum number of messages to return per call
|
|
862
|
+
# @return [Array<Message>] Array of messages (empty if none available within timeout)
|
|
863
|
+
# @raise [RdkafkaError] When a consumed message contains an error
|
|
864
|
+
# @raise [ClosedConsumerError] When called on a closed consumer
|
|
865
|
+
def poll_batch_nb(timeout_ms = 0, max_items: 100)
|
|
866
|
+
closed_consumer_check(__method__)
|
|
867
|
+
|
|
868
|
+
buffer = batch_buffer(max_items)
|
|
869
|
+
messages = []
|
|
870
|
+
|
|
871
|
+
count = @native_kafka.with_inner do |_inner|
|
|
872
|
+
Rdkafka::Bindings.rd_kafka_consume_batch_queue_nb(
|
|
873
|
+
consumer_queue,
|
|
874
|
+
timeout_ms,
|
|
875
|
+
buffer,
|
|
876
|
+
max_items
|
|
877
|
+
)
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
return messages if count <= 0
|
|
881
|
+
|
|
882
|
+
i = 0
|
|
883
|
+
begin
|
|
884
|
+
while i < count
|
|
885
|
+
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
886
|
+
|
|
887
|
+
if ptr.null?
|
|
888
|
+
i += 1
|
|
889
|
+
next
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
893
|
+
|
|
894
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
895
|
+
raise Rdkafka::RdkafkaError.new(native_message[:err])
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
messages << Rdkafka::Consumer::Message.new(native_message)
|
|
899
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
900
|
+
i += 1
|
|
901
|
+
end
|
|
902
|
+
ensure
|
|
903
|
+
while i < count
|
|
904
|
+
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
905
|
+
Rdkafka::Bindings.rd_kafka_message_destroy(ptr) unless ptr.null?
|
|
906
|
+
i += 1
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
messages
|
|
911
|
+
end
|
|
912
|
+
|
|
588
913
|
# Poll for new messages and yield for each received one. Iteration
|
|
589
914
|
# will end when the consumer is closed.
|
|
590
915
|
#
|
|
591
916
|
# If `enable.partition.eof` is turned on in the config this will raise an error when an eof is
|
|
592
917
|
# reached, so you probably want to disable that when using this method of iteration.
|
|
593
918
|
#
|
|
919
|
+
# @param timeout_ms [Integer] Timeout for each poll iteration
|
|
594
920
|
# @yieldparam message [Message] Received message
|
|
595
921
|
# @return [nil]
|
|
596
922
|
# @raise [RdkafkaError] When polling fails
|
|
597
|
-
def each
|
|
923
|
+
def each(timeout_ms: Defaults::CONSUMER_POLL_TIMEOUT_MS)
|
|
598
924
|
loop do
|
|
599
|
-
message = poll(
|
|
925
|
+
message = poll(timeout_ms)
|
|
600
926
|
if message
|
|
601
927
|
yield(message)
|
|
928
|
+
elsif closed?
|
|
929
|
+
break
|
|
602
930
|
else
|
|
603
|
-
|
|
604
|
-
break
|
|
605
|
-
else
|
|
606
|
-
next
|
|
607
|
-
end
|
|
931
|
+
next
|
|
608
932
|
end
|
|
609
933
|
end
|
|
610
934
|
end
|
|
611
935
|
|
|
612
|
-
#
|
|
936
|
+
# @deprecated This method has been removed due to data consistency concerns
|
|
937
|
+
# @param max_items [Integer] unused
|
|
938
|
+
# @param bytes_threshold [Numeric] unused
|
|
939
|
+
# @param timeout_ms [Integer] unused
|
|
940
|
+
# @param yield_on_error [Boolean] unused
|
|
941
|
+
# @param block [Proc] unused block
|
|
942
|
+
# @raise [NotImplementedError] Always raises as this method is no longer supported
|
|
613
943
|
def each_batch(max_items: 100, bytes_threshold: Float::INFINITY, timeout_ms: 250, yield_on_error: false, &block)
|
|
614
944
|
raise NotImplementedError, <<~ERROR
|
|
615
945
|
`each_batch` has been removed due to data consistency concerns.
|
|
@@ -646,8 +976,30 @@ module Rdkafka
|
|
|
646
976
|
|
|
647
977
|
private
|
|
648
978
|
|
|
979
|
+
# Checks if the consumer is closed and raises an error if so
|
|
980
|
+
# @param method [Symbol] name of the calling method for error context
|
|
981
|
+
# @raise [ClosedConsumerError] when the consumer is closed
|
|
649
982
|
def closed_consumer_check(method)
|
|
650
983
|
raise Rdkafka::ClosedConsumerError.new(method) if closed?
|
|
651
984
|
end
|
|
985
|
+
|
|
986
|
+
# Returns the consumer queue pointer, lazily initialized
|
|
987
|
+
# @return [FFI::Pointer] consumer queue handle
|
|
988
|
+
def consumer_queue
|
|
989
|
+
@consumer_queue ||= @native_kafka.with_inner do |inner|
|
|
990
|
+
Rdkafka::Bindings.rd_kafka_queue_get_consumer(inner)
|
|
991
|
+
end
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# Returns a reusable FFI buffer for batch polling, growing if needed
|
|
995
|
+
# @param max_items [Integer] minimum buffer capacity
|
|
996
|
+
# @return [FFI::MemoryPointer] pointer buffer
|
|
997
|
+
def batch_buffer(max_items)
|
|
998
|
+
if @batch_buffer.nil? || @batch_buffer_size < max_items
|
|
999
|
+
@batch_buffer = FFI::MemoryPointer.new(:pointer, max_items)
|
|
1000
|
+
@batch_buffer_size = max_items
|
|
1001
|
+
end
|
|
1002
|
+
@batch_buffer
|
|
1003
|
+
end
|
|
652
1004
|
end
|
|
653
1005
|
end
|