karafka-rdkafka 0.28.2-aarch64-linux-gnu → 0.30.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 +56 -19
- data/README.md +2 -0
- data/bin/verify_kafka_warnings +2 -0
- data/docker-compose-ssl.yml +13 -0
- data/docker-compose.yml +11 -0
- data/ext/librdkafka.so +0 -0
- data/lib/rdkafka/admin/alter_consumer_group_offsets_handle.rb +31 -0
- data/lib/rdkafka/admin/alter_consumer_group_offsets_report.rb +50 -0
- data/lib/rdkafka/admin/delete_consumer_group_offsets_handle.rb +31 -0
- data/lib/rdkafka/admin/delete_consumer_group_offsets_report.rb +50 -0
- data/lib/rdkafka/admin.rb +120 -0
- data/lib/rdkafka/bindings.rb +105 -4
- data/lib/rdkafka/callbacks/alter_consumer_group_offsets_handler.rb +65 -0
- data/lib/rdkafka/callbacks/delete_consumer_group_offsets_handler.rb +65 -0
- data/lib/rdkafka/callbacks.rb +14 -16
- data/lib/rdkafka/config.rb +143 -14
- data/lib/rdkafka/defaults.rb +8 -0
- data/lib/rdkafka/error.rb +13 -2
- data/lib/rdkafka/helpers/oauth.rb +24 -6
- data/lib/rdkafka/share_consumer/message.rb +40 -0
- data/lib/rdkafka/share_consumer.rb +558 -0
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +8 -0
- metadata +10 -2
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
# A KIP-932 share group consumer of Kafka messages.
|
|
5
|
+
#
|
|
6
|
+
# Share groups bring queue-like semantics to Kafka: multiple members of the same group
|
|
7
|
+
# consume from the same partitions cooperatively and progress is tracked per record via
|
|
8
|
+
# acknowledgements (`:accept`, `:release`, `:reject`) instead of committed offsets. Partition
|
|
9
|
+
# assignment is entirely broker-driven: there is no rebalance callback and no `assign` step.
|
|
10
|
+
#
|
|
11
|
+
# To create a share consumer set up a {Config} with `:"group.id"` and call
|
|
12
|
+
# {Config#share_consumer share_consumer} on it. The acknowledgement mode is selected with the
|
|
13
|
+
# `:"share.acknowledgement.mode"` config property (`implicit`, the default, acknowledges every
|
|
14
|
+
# polled record as accepted on the next poll/commit; `explicit` requires every record to be
|
|
15
|
+
# acknowledged via {#acknowledge} before the next poll).
|
|
16
|
+
#
|
|
17
|
+
# @note The share consumer is a **preview** feature of librdkafka and requires a broker with
|
|
18
|
+
# share groups enabled (Apache Kafka 4.2.0+). Its API may change before general availability
|
|
19
|
+
# and it is not recommended for production use.
|
|
20
|
+
#
|
|
21
|
+
# @note The share consumer handle is **not thread-safe by design** (matching KIP-932): use one
|
|
22
|
+
# share consumer per thread. librdkafka enforces this and calls from a second thread raise
|
|
23
|
+
# an `RdkafkaError` with code `conflict`. This class is deliberately not built on
|
|
24
|
+
# {NativeKafka}: there is no background polling thread (statistics, error and log callbacks
|
|
25
|
+
# are all serviced from within {#poll}) and the native handle must never be used from a
|
|
26
|
+
# polling thread, so only the {#close} interaction is synchronized here. {#close} may be
|
|
27
|
+
# called from any thread and waits for in-flight calls to finish before tearing down.
|
|
28
|
+
#
|
|
29
|
+
# Preview limitations inherited from librdkafka worth knowing at this layer:
|
|
30
|
+
# - `max.poll.records` (default 500) is a soft bound
|
|
31
|
+
# - a blocking poll can only be ended by its timeout (no wakeup API)
|
|
32
|
+
# - {#close} takes no timeout and is bounded internally by `socket.timeout.ms`
|
|
33
|
+
# - failed acknowledgements are not retried automatically; outcomes are reported through
|
|
34
|
+
# {#acknowledgement_commit_callback=} or the per-partition results of {#commit_sync}
|
|
35
|
+
class ShareConsumer
|
|
36
|
+
include Enumerable
|
|
37
|
+
include Helpers::OAuth
|
|
38
|
+
|
|
39
|
+
# Mapping of friendly acknowledge types to their librdkafka enum values
|
|
40
|
+
ACKNOWLEDGE_TYPES = {
|
|
41
|
+
accept: Bindings::RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_ACCEPT,
|
|
42
|
+
release: Bindings::RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_RELEASE,
|
|
43
|
+
reject: Bindings::RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_REJECT
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
private_constant :ACKNOWLEDGE_TYPES
|
|
47
|
+
|
|
48
|
+
# State shared with the GC finalizer. Carrying the registered acknowledgement commit FFI
|
|
49
|
+
# function next to the native handle guarantees its native trampoline outlives the consumer
|
|
50
|
+
# object: the finalizer-driven destroy flushes pending acknowledgements through that
|
|
51
|
+
# callback, so it must not be collected in the same GC cycle as the consumer. `creator_pid`
|
|
52
|
+
# rides along so the finalizer (which must not capture `self`) can tell whether it is running
|
|
53
|
+
# in the process that created the handle - librdkafka is not fork-safe, so an inherited handle
|
|
54
|
+
# must never be destroyed from a forked child (its backing threads do not exist there).
|
|
55
|
+
State = Struct.new(:native, :ack_callback, :creator_pid)
|
|
56
|
+
|
|
57
|
+
private_constant :State
|
|
58
|
+
|
|
59
|
+
# The client name librdkafka reports for this consumer (e.g. "rdkafka#consumer-1"), used to
|
|
60
|
+
# correlate global callbacks (such as the OAuthBearer token refresh callback) with this
|
|
61
|
+
# instance. librdkafka exposes no name accessor on the share handle, so this is captured
|
|
62
|
+
# from the first callback that carries it and is nil until then.
|
|
63
|
+
# @return [String, nil]
|
|
64
|
+
attr_reader :name
|
|
65
|
+
|
|
66
|
+
# @private
|
|
67
|
+
# Captures the librdkafka client name once known (from callback context)
|
|
68
|
+
attr_writer :name
|
|
69
|
+
|
|
70
|
+
# @private
|
|
71
|
+
# @param native [FFI::Pointer] pointer to the native rd_kafka_share_t handle
|
|
72
|
+
# @param opaque [Rdkafka::Opaque, nil] opaque used for callback context. Held here so it is
|
|
73
|
+
# not garbage collected while the native client can still reference it
|
|
74
|
+
# (Config.opaques is a WeakMap).
|
|
75
|
+
def initialize(native, opaque: nil)
|
|
76
|
+
@opaque = opaque
|
|
77
|
+
# State shared with the GC finalizer so it can destroy the native handle (and keep the
|
|
78
|
+
# registered acknowledgement callback alive while doing so) without capturing `self`
|
|
79
|
+
# (which would pin the consumer and prevent collection). The creating pid rides along so
|
|
80
|
+
# every teardown path can skip the native destroy in a forked child (librdkafka is not
|
|
81
|
+
# fork-safe).
|
|
82
|
+
@state = State.new(native, nil, Process.pid)
|
|
83
|
+
# Guards handle teardown against in-flight calls: increments happen under this mutex and
|
|
84
|
+
# close holds it while draining and destroying (mirrors NativeKafka's approach)
|
|
85
|
+
@access_mutex = Mutex.new
|
|
86
|
+
# Separate mutex for decrements so a blocked close (holding @access_mutex) cannot
|
|
87
|
+
# deadlock with an operation trying to finish
|
|
88
|
+
@decrement_mutex = Mutex.new
|
|
89
|
+
# Number of calls currently using the native handle
|
|
90
|
+
@operations_in_progress = 0
|
|
91
|
+
|
|
92
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@state))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Builds the GC finalizer for a share consumer. `rd_kafka_share_destroy` internally closes
|
|
96
|
+
# the consumer (sending pending acknowledgements and leaving the group) when it was not
|
|
97
|
+
# closed explicitly. The acknowledgement callback function rides along in the state holder
|
|
98
|
+
# so it is still alive when that close flushes acknowledgements through it.
|
|
99
|
+
#
|
|
100
|
+
# @private
|
|
101
|
+
# @param state [State] holder carrying the native handle, the ack callback function and the
|
|
102
|
+
# creating pid
|
|
103
|
+
# @return [Proc] finalizer proc that must not reference the consumer instance
|
|
104
|
+
def self.finalizer(state)
|
|
105
|
+
proc do
|
|
106
|
+
native = state.native
|
|
107
|
+
|
|
108
|
+
# librdkafka is not fork-safe: a handle inherited by a forked child must never be
|
|
109
|
+
# destroyed there (its backing threads do not exist in the child), so only the process
|
|
110
|
+
# that created it runs the native teardown.
|
|
111
|
+
if native && state.creator_pid == Process.pid
|
|
112
|
+
state.native = nil
|
|
113
|
+
error = Rdkafka::Bindings.rd_kafka_share_destroy(native)
|
|
114
|
+
Rdkafka::Bindings.rd_kafka_error_destroy(error) unless error.null?
|
|
115
|
+
state.ack_callback = nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Subscribes to one or more topics letting the broker assign partitions to the members of
|
|
121
|
+
# the share group.
|
|
122
|
+
#
|
|
123
|
+
# Wildcard (regex) subscriptions are not supported by share consumers.
|
|
124
|
+
#
|
|
125
|
+
# @param topics [Array<String>] One or more topic names
|
|
126
|
+
# @return [nil]
|
|
127
|
+
# @raise [ArgumentError] When no topics are given. librdkafka would treat an empty
|
|
128
|
+
# subscribe as an unsubscribe (unlike the regular consumer, which rejects it), which
|
|
129
|
+
# silently drops the subscription - use {#unsubscribe} for that.
|
|
130
|
+
# @raise [RdkafkaError] When subscribing fails
|
|
131
|
+
def subscribe(*topics)
|
|
132
|
+
raise ArgumentError, "at least one topic is required (an empty subscribe would unsubscribe)" if topics.empty?
|
|
133
|
+
|
|
134
|
+
with_native(__method__) do |native|
|
|
135
|
+
tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
topics.each do |topic|
|
|
139
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, Rdkafka::Bindings::RD_KAFKA_PARTITION_UA)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
response = Rdkafka::Bindings.rd_kafka_share_subscribe(native, tpl)
|
|
143
|
+
Rdkafka::RdkafkaError.validate!(response, "Error subscribing to '#{topics.join(", ")}'")
|
|
144
|
+
ensure
|
|
145
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Unsubscribes from all subscribed topics and leaves the share group assignment.
|
|
153
|
+
#
|
|
154
|
+
# @return [nil]
|
|
155
|
+
# @raise [RdkafkaError] When unsubscribing fails
|
|
156
|
+
def unsubscribe
|
|
157
|
+
with_native(__method__) do |native|
|
|
158
|
+
response = Rdkafka::Bindings.rd_kafka_share_unsubscribe(native)
|
|
159
|
+
Rdkafka::RdkafkaError.validate!(response)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Returns the current subscription to topics.
|
|
166
|
+
#
|
|
167
|
+
# @return [Rdkafka::Consumer::TopicPartitionList]
|
|
168
|
+
# @raise [RdkafkaError] When getting the subscription fails
|
|
169
|
+
def subscription
|
|
170
|
+
with_native(__method__) do |native|
|
|
171
|
+
ptr = FFI::MemoryPointer.new(:pointer)
|
|
172
|
+
response = Rdkafka::Bindings.rd_kafka_share_subscription(native, ptr)
|
|
173
|
+
Rdkafka::RdkafkaError.validate!(response)
|
|
174
|
+
|
|
175
|
+
native_tpl = ptr.read_pointer
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
Rdkafka::Consumer::TopicPartitionList.from_native_tpl(native_tpl)
|
|
179
|
+
ensure
|
|
180
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(native_tpl)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Polls for a batch of messages.
|
|
186
|
+
#
|
|
187
|
+
# Unlike {Consumer#poll} a single call returns a whole batch (up to `max.poll.records`,
|
|
188
|
+
# which is a soft bound in the preview). In implicit acknowledgement mode every record
|
|
189
|
+
# delivered by the previous poll is acknowledged as accepted by this call. In explicit mode
|
|
190
|
+
# every previously delivered record must have been acknowledged via {#acknowledge}, otherwise
|
|
191
|
+
# this call raises.
|
|
192
|
+
#
|
|
193
|
+
# Record-level errors (for example an unauthorized topic) are delivered as messages with
|
|
194
|
+
# {ShareConsumer::Message#error} set rather than raised, since a batch can mix valid records
|
|
195
|
+
# and errors. Callers should check {ShareConsumer::Message#error?} before using the payload.
|
|
196
|
+
# Error entries do not need to be acknowledged.
|
|
197
|
+
#
|
|
198
|
+
# @param timeout_ms [Integer] Timeout of this poll
|
|
199
|
+
# @return [Array<ShareConsumer::Message, RdkafkaError>] polled messages, empty when the
|
|
200
|
+
# timeout expired without records. A message that fails to build (see below) is an
|
|
201
|
+
# `RdkafkaError` rather than a `ShareConsumer::Message`.
|
|
202
|
+
# @raise [RdkafkaError] When polling fails at the batch level (for example unacknowledged
|
|
203
|
+
# records in explicit mode)
|
|
204
|
+
def poll(timeout_ms = Defaults::SHARE_CONSUMER_POLL_TIMEOUT_MS)
|
|
205
|
+
with_native(__method__) do |native|
|
|
206
|
+
messages_ptr = FFI::MemoryPointer.new(:pointer)
|
|
207
|
+
error_ptr = Rdkafka::Bindings.rd_kafka_share_poll(native, timeout_ms, messages_ptr)
|
|
208
|
+
|
|
209
|
+
# Destroys the native error and raises when it carries an actual error
|
|
210
|
+
Rdkafka::RdkafkaError.validate!(error_ptr)
|
|
211
|
+
|
|
212
|
+
batch = messages_ptr.read_pointer
|
|
213
|
+
|
|
214
|
+
next [] if batch.null?
|
|
215
|
+
|
|
216
|
+
begin
|
|
217
|
+
count = Rdkafka::Bindings.rd_kafka_messages_count(batch)
|
|
218
|
+
|
|
219
|
+
Array.new(count) do |i|
|
|
220
|
+
message_ptr = Rdkafka::Bindings.rd_kafka_messages_get(batch, i)
|
|
221
|
+
native_message = Rdkafka::Bindings::Message.new(message_ptr)
|
|
222
|
+
|
|
223
|
+
# Only touch the error machinery for the rare record-level error entries
|
|
224
|
+
error = if native_message[:err] == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
225
|
+
nil
|
|
226
|
+
else
|
|
227
|
+
Rdkafka::RdkafkaError.build(native_message)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
begin
|
|
231
|
+
Message.new(
|
|
232
|
+
native_message,
|
|
233
|
+
delivery_count: Rdkafka::Bindings.rd_kafka_message_delivery_count(message_ptr),
|
|
234
|
+
error: error
|
|
235
|
+
)
|
|
236
|
+
rescue Rdkafka::RdkafkaError => e
|
|
237
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
238
|
+
# error rather than discarding the whole batch - including the messages already
|
|
239
|
+
# built - and raising, which would silently lose them. Mirrors how
|
|
240
|
+
# Consumer#poll_batch handles the same failure mode.
|
|
241
|
+
e
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
ensure
|
|
245
|
+
# Destroys the batch together with all the messages it contains. All message content
|
|
246
|
+
# has been copied into Ruby objects at this point.
|
|
247
|
+
Rdkafka::Bindings.rd_kafka_messages_destroy(batch)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Acknowledges a message delivered by {#poll} in explicit acknowledgement mode
|
|
253
|
+
# (`share.acknowledgement.mode` set to `explicit`).
|
|
254
|
+
#
|
|
255
|
+
# Acknowledgements are accumulated locally and sent to the broker on the next {#poll},
|
|
256
|
+
# {#commit_sync} or {#commit_async}.
|
|
257
|
+
#
|
|
258
|
+
# @param message [ShareConsumer::Message] the message to acknowledge
|
|
259
|
+
# @param type [Symbol] `:accept` (processed successfully), `:release` (make it available for
|
|
260
|
+
# redelivery) or `:reject` (do not deliver again, archive)
|
|
261
|
+
# @return [nil]
|
|
262
|
+
# @raise [ArgumentError] For an unknown acknowledge type
|
|
263
|
+
# @raise [RdkafkaError] When acknowledging fails (for example in implicit mode, or for a
|
|
264
|
+
# record that is not currently acquired)
|
|
265
|
+
def acknowledge(message, type = :accept)
|
|
266
|
+
ack_type = ACKNOWLEDGE_TYPES.fetch(type) do
|
|
267
|
+
raise ArgumentError, "Unknown acknowledge type: #{type.inspect} (expected one of #{ACKNOWLEDGE_TYPES.keys.map(&:inspect).join(", ")})"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
with_native(__method__) do |native|
|
|
271
|
+
response = Rdkafka::Bindings.rd_kafka_share_acknowledge_offset(
|
|
272
|
+
native,
|
|
273
|
+
message.topic,
|
|
274
|
+
message.partition,
|
|
275
|
+
message.offset,
|
|
276
|
+
ack_type
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# This runs once per consumed message in explicit mode, so the error prefix is only
|
|
280
|
+
# built on the failure path
|
|
281
|
+
unless response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
282
|
+
Rdkafka::RdkafkaError.validate!(
|
|
283
|
+
response,
|
|
284
|
+
"Error acknowledging #{message.topic}/#{message.partition}@#{message.offset}"
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Sends all pending acknowledgements to the broker and waits for the responses.
|
|
293
|
+
#
|
|
294
|
+
# @param timeout_ms [Integer] Maximum time to wait for the broker replies
|
|
295
|
+
# @return [Rdkafka::Consumer::TopicPartitionList, nil] per-partition results where each
|
|
296
|
+
# partition carries the acknowledgement outcome in its `err` field, or nil when there was
|
|
297
|
+
# nothing to commit
|
|
298
|
+
# @raise [RdkafkaError] When the commit fails as a whole
|
|
299
|
+
def commit_sync(timeout_ms = Defaults::SHARE_CONSUMER_COMMIT_SYNC_TIMEOUT_MS)
|
|
300
|
+
with_native(__method__) do |native|
|
|
301
|
+
tpl_ptr = FFI::MemoryPointer.new(:pointer)
|
|
302
|
+
error_ptr = Rdkafka::Bindings.rd_kafka_share_commit_sync(native, timeout_ms, tpl_ptr)
|
|
303
|
+
|
|
304
|
+
Rdkafka::RdkafkaError.validate!(error_ptr)
|
|
305
|
+
|
|
306
|
+
native_tpl = tpl_ptr.read_pointer
|
|
307
|
+
|
|
308
|
+
next nil if native_tpl.null?
|
|
309
|
+
|
|
310
|
+
begin
|
|
311
|
+
Rdkafka::Consumer::TopicPartitionList.from_native_tpl(native_tpl)
|
|
312
|
+
ensure
|
|
313
|
+
Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(native_tpl)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Sends all pending acknowledgements to the broker without waiting for the responses.
|
|
319
|
+
#
|
|
320
|
+
# Outcomes are reported through the callback set with {#acknowledgement_commit_callback=}
|
|
321
|
+
# once the broker replies arrive (serviced by a later {#poll}).
|
|
322
|
+
#
|
|
323
|
+
# @return [nil]
|
|
324
|
+
# @raise [RdkafkaError] When enqueuing the commit fails
|
|
325
|
+
def commit_async
|
|
326
|
+
with_native(__method__) do |native|
|
|
327
|
+
error_ptr = Rdkafka::Bindings.rd_kafka_share_commit_async(native)
|
|
328
|
+
|
|
329
|
+
Rdkafka::RdkafkaError.validate!(error_ptr)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
nil
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Sets the callback invoked with the outcome of acknowledgement commits, once per partition
|
|
336
|
+
# per commit, from within {#poll}/{#commit_sync}/{#commit_async} on the consumer thread.
|
|
337
|
+
#
|
|
338
|
+
# The callable is invoked with two arguments:
|
|
339
|
+
# - an Array of Hashes `{ topic: String, partition: Integer, offsets: Array<Integer> }`
|
|
340
|
+
# describing the acknowledged offsets
|
|
341
|
+
# - an RdkafkaError or nil with the outcome for those offsets
|
|
342
|
+
#
|
|
343
|
+
# @param callback [Proc, #call, nil] callable object or nil to clear the callback
|
|
344
|
+
# @return [nil]
|
|
345
|
+
# @raise [TypeError] When the callback is not callable
|
|
346
|
+
# @raise [RdkafkaError] When (de)registering the callback fails
|
|
347
|
+
#
|
|
348
|
+
# @note No share consumer methods may be called from within the callback (librdkafka rejects
|
|
349
|
+
# re-entry). Exceptions raised by the callback are logged and swallowed, mirroring the
|
|
350
|
+
# producer delivery callback behavior.
|
|
351
|
+
def acknowledgement_commit_callback=(callback)
|
|
352
|
+
raise TypeError.new("Callback has to be callable") unless callback.nil? || callback.respond_to?(:call)
|
|
353
|
+
|
|
354
|
+
function = callback && self.class.build_acknowledgement_commit_function(callback)
|
|
355
|
+
|
|
356
|
+
with_native(__method__) do |native|
|
|
357
|
+
error_ptr = Rdkafka::Bindings.rd_kafka_share_set_acknowledgement_commit_cb(
|
|
358
|
+
native,
|
|
359
|
+
function,
|
|
360
|
+
FFI::Pointer::NULL
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
Rdkafka::RdkafkaError.validate!(error_ptr)
|
|
364
|
+
|
|
365
|
+
# Retain the function only after successful registration, in the state holder shared
|
|
366
|
+
# with the finalizer: the finalizer-driven destroy can still invoke it while flushing
|
|
367
|
+
# pending acknowledgements, so it must outlive the consumer object itself
|
|
368
|
+
@state.ack_callback = function
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Builds the native acknowledgement commit callback function wrapping the given callable.
|
|
373
|
+
#
|
|
374
|
+
# Built in a class method on purpose: a Ruby block always closes over its `self`, and an
|
|
375
|
+
# instance-level block would chain finalizer -> state -> function -> block -> consumer,
|
|
376
|
+
# making a consumer with a registered callback permanently uncollectable. Here the captured
|
|
377
|
+
# `self` is the class and the block holds only the user callback.
|
|
378
|
+
#
|
|
379
|
+
# @private
|
|
380
|
+
# @param callback [#call] user callable
|
|
381
|
+
# @return [FFI::Function] native callback function
|
|
382
|
+
def self.build_acknowledgement_commit_function(callback)
|
|
383
|
+
FFI::Function.new(
|
|
384
|
+
:void, [:pointer, :pointer, :int, :pointer]
|
|
385
|
+
) do |_share_ptr, list_ptr, err_code, _opaque|
|
|
386
|
+
callback.call(
|
|
387
|
+
partition_offsets_from_native(list_ptr),
|
|
388
|
+
Rdkafka::RdkafkaError.build(err_code) || nil
|
|
389
|
+
)
|
|
390
|
+
rescue Exception => err
|
|
391
|
+
Rdkafka::Config.logger.error("Unhandled exception in acknowledgement commit callback: #{err.class} - #{err.message}")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Converts a native rd_kafka_share_partition_offsets_list_t into an Array of Hashes. The
|
|
396
|
+
# native list is owned by librdkafka for the duration of the callback and must not be
|
|
397
|
+
# retained or destroyed.
|
|
398
|
+
#
|
|
399
|
+
# @private
|
|
400
|
+
# @param list_ptr [FFI::Pointer] native partition offsets list
|
|
401
|
+
# @return [Array<Hash>] one Hash per partition with :topic, :partition and :offsets keys
|
|
402
|
+
def self.partition_offsets_from_native(list_ptr)
|
|
403
|
+
count = Rdkafka::Bindings.rd_kafka_share_partition_offsets_list_count(list_ptr)
|
|
404
|
+
|
|
405
|
+
Array.new(count) do |i|
|
|
406
|
+
entry_ptr = Rdkafka::Bindings.rd_kafka_share_partition_offsets_list_get(list_ptr, i)
|
|
407
|
+
partition = Rdkafka::Bindings::TopicPartition.new(
|
|
408
|
+
Rdkafka::Bindings.rd_kafka_share_partition_offsets_partition(entry_ptr)
|
|
409
|
+
)
|
|
410
|
+
offsets_cnt = Rdkafka::Bindings.rd_kafka_share_partition_offsets_offsets_cnt(entry_ptr)
|
|
411
|
+
offsets_ptr = Rdkafka::Bindings.rd_kafka_share_partition_offsets_offsets(entry_ptr)
|
|
412
|
+
|
|
413
|
+
{
|
|
414
|
+
topic: partition[:topic],
|
|
415
|
+
partition: partition[:partition],
|
|
416
|
+
offsets: offsets_ptr.null? ? [] : offsets_ptr.read_array_of_int64(offsets_cnt)
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Poll for messages repeatedly and yield them one by one. Iteration ends when the consumer
|
|
422
|
+
# is closed.
|
|
423
|
+
#
|
|
424
|
+
# As with {#poll}, an entry that fails to build is yielded as an `RdkafkaError` rather than a
|
|
425
|
+
# {ShareConsumer::Message}, so a block that touches message accessors should guard accordingly.
|
|
426
|
+
#
|
|
427
|
+
# @yield [message] a polled entry
|
|
428
|
+
# @yieldparam message [ShareConsumer::Message, RdkafkaError]
|
|
429
|
+
# @return [nil]
|
|
430
|
+
# @raise [RdkafkaError] When polling fails
|
|
431
|
+
def each
|
|
432
|
+
loop do
|
|
433
|
+
# Only a ClosedConsumerError raised by poll itself ends iteration; the yield is kept
|
|
434
|
+
# outside the rescue so an error raised from the caller's block propagates instead of
|
|
435
|
+
# being mistaken for the consumer closing.
|
|
436
|
+
batch =
|
|
437
|
+
begin
|
|
438
|
+
poll
|
|
439
|
+
rescue Rdkafka::ClosedConsumerError
|
|
440
|
+
break
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
batch.each { |message| yield(message) }
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Marks failed oauth token acquire in librdkafka for this share consumer
|
|
448
|
+
#
|
|
449
|
+
# @param reason [String] human readable error reason for failing to acquire token
|
|
450
|
+
def oauthbearer_set_token_failure(reason)
|
|
451
|
+
with_native(__method__) do |native|
|
|
452
|
+
Rdkafka::Bindings.rd_kafka_share_oauthbearer_set_token_failure(native, reason)
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Whether this share consumer has closed. Also reports closed in any process other than the
|
|
457
|
+
# one that created it: librdkafka is not fork-safe, so an inherited handle must be treated as
|
|
458
|
+
# unusable (and never torn down) in a forked child.
|
|
459
|
+
# @return [Boolean]
|
|
460
|
+
def closed?
|
|
461
|
+
@state.native.nil? || @state.creator_pid != Process.pid
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Closes this share consumer: sends any pending acknowledgements, leaves the share group and
|
|
465
|
+
# frees the native handle. Waits for in-flight calls from other threads to finish first.
|
|
466
|
+
# Close is bounded internally by `socket.timeout.ms` (there is no timeout argument in the
|
|
467
|
+
# librdkafka preview).
|
|
468
|
+
#
|
|
469
|
+
# @return [nil]
|
|
470
|
+
# @raise [RdkafkaError] When closing or destroying reported an error. librdkafka leaves the
|
|
471
|
+
# instance intact in that case, so the consumer keeps its native handle and `close` can be
|
|
472
|
+
# retried (for example after returning from the acknowledgement commit callback, from
|
|
473
|
+
# within which closing is rejected).
|
|
474
|
+
def close
|
|
475
|
+
return if closed?
|
|
476
|
+
|
|
477
|
+
@access_mutex.synchronize do
|
|
478
|
+
# Wait for in-flight calls on other threads to finish using the handle. New calls block
|
|
479
|
+
# on the mutex above while we hold it.
|
|
480
|
+
sleep(Defaults::NATIVE_KAFKA_SYNCHRONIZE_SLEEP_INTERVAL_MS / 1_000.0) until @operations_in_progress.zero?
|
|
481
|
+
|
|
482
|
+
native = @state.native
|
|
483
|
+
|
|
484
|
+
# Lost the race against a concurrent close that already finished
|
|
485
|
+
return if native.nil?
|
|
486
|
+
|
|
487
|
+
# librdkafka leaves the instance intact when close/destroy fail, and validate! raises
|
|
488
|
+
# before any reference (or the finalizer) is dropped, so the caller can retry close
|
|
489
|
+
# and the handle cannot leak silently.
|
|
490
|
+
#
|
|
491
|
+
# The close step is skipped when librdkafka already considers the consumer closed
|
|
492
|
+
# (e.g. a prior close attempt succeeded but its destroy failed and was retried).
|
|
493
|
+
if Rdkafka::Bindings.rd_kafka_share_consumer_closed(native).zero?
|
|
494
|
+
Rdkafka::RdkafkaError.validate!(Rdkafka::Bindings.rd_kafka_share_consumer_close(native))
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
Rdkafka::RdkafkaError.validate!(Rdkafka::Bindings.rd_kafka_share_destroy(native))
|
|
498
|
+
|
|
499
|
+
# The native handle is gone only now; drop all references and the finalizer
|
|
500
|
+
ObjectSpace.undefine_finalizer(self)
|
|
501
|
+
@state.native = nil
|
|
502
|
+
@state.ack_callback = nil
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
nil
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
private
|
|
509
|
+
|
|
510
|
+
# Executes a block with the native share handle while tracking the call so {#close} cannot
|
|
511
|
+
# tear the handle down mid-call from another thread. Increments happen under the access
|
|
512
|
+
# mutex that close holds for the whole teardown, so once close is draining no new call can
|
|
513
|
+
# slip in; librdkafka's own single-owner gate still rejects genuinely concurrent API calls.
|
|
514
|
+
#
|
|
515
|
+
# @param method [Symbol] public method name, used for the ClosedConsumerError
|
|
516
|
+
# @yield [FFI::Pointer] the native share handle
|
|
517
|
+
# @raise [ClosedConsumerError] when the consumer is closed or closing
|
|
518
|
+
def with_native(method)
|
|
519
|
+
# Owning the mutex here means re-entry from a callback fired during #close's teardown
|
|
520
|
+
# (the only internal holder) - the handle is going away, so reject before tracking
|
|
521
|
+
raise Rdkafka::ClosedConsumerError.new(method) if @access_mutex.owned?
|
|
522
|
+
|
|
523
|
+
@access_mutex.synchronize { @operations_in_progress += 1 }
|
|
524
|
+
|
|
525
|
+
begin
|
|
526
|
+
native = @state.native
|
|
527
|
+
|
|
528
|
+
# nil handle => closed; different pid => inherited by a forked child, where librdkafka is
|
|
529
|
+
# not fork-safe and the handle must not be used.
|
|
530
|
+
raise Rdkafka::ClosedConsumerError.new(method) if native.nil? || @state.creator_pid != Process.pid
|
|
531
|
+
|
|
532
|
+
yield(native)
|
|
533
|
+
ensure
|
|
534
|
+
@decrement_mutex.synchronize { @operations_in_progress -= 1 }
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Performs the share-specific native token set call, reusing the buffer and extension
|
|
539
|
+
# plumbing from {Helpers::OAuth}
|
|
540
|
+
#
|
|
541
|
+
# @param token [String] the token value
|
|
542
|
+
# @param lifetime_ms [Integer] token expiry, in milliseconds since the epoch
|
|
543
|
+
# @param principal_name [String] the Kafka principal name associated with the token
|
|
544
|
+
# @param extensions_ptr [FFI::Pointer, nil] `const char **` built by
|
|
545
|
+
# {Helpers::OAuth#map_extensions}
|
|
546
|
+
# @param extensions_size [Integer] number of key-value pairs pointed to by `extensions_ptr`
|
|
547
|
+
# @param error_buffer [FFI::MemoryPointer] 256-byte buffer for the error string
|
|
548
|
+
# @return [Integer] 0 on success
|
|
549
|
+
def oauthbearer_native_set_token(token, lifetime_ms, principal_name, extensions_ptr, extensions_size, error_buffer)
|
|
550
|
+
with_native(:oauthbearer_set_token) do |native|
|
|
551
|
+
Rdkafka::Bindings.rd_kafka_share_oauthbearer_set_token(
|
|
552
|
+
native, token, lifetime_ms, principal_name,
|
|
553
|
+
extensions_ptr, extensions_size, error_buffer, 256
|
|
554
|
+
)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end
|
data/lib/rdkafka/version.rb
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Rdkafka
|
|
4
4
|
# Current rdkafka-ruby gem version
|
|
5
|
-
VERSION = "0.
|
|
5
|
+
VERSION = "0.30.0"
|
|
6
6
|
# Target librdkafka version to be used
|
|
7
|
-
LIBRDKAFKA_VERSION = "2.
|
|
7
|
+
LIBRDKAFKA_VERSION = "2.15.1"
|
|
8
8
|
# SHA256 hash of the librdkafka source tarball for verification
|
|
9
|
-
LIBRDKAFKA_SOURCE_SHA256 = "
|
|
9
|
+
LIBRDKAFKA_SOURCE_SHA256 = "23c8575c7d1ced07246cb9cf200c11325b72201fd4134a02414ca869fbdd8ed3"
|
|
10
10
|
end
|
data/lib/rdkafka.rb
CHANGED
|
@@ -13,6 +13,10 @@ require "rdkafka/helpers/metadata"
|
|
|
13
13
|
require "rdkafka/helpers/list_offsets"
|
|
14
14
|
require "rdkafka/abstract_handle"
|
|
15
15
|
require "rdkafka/admin"
|
|
16
|
+
require "rdkafka/admin/alter_consumer_group_offsets_handle"
|
|
17
|
+
require "rdkafka/admin/alter_consumer_group_offsets_report"
|
|
18
|
+
require "rdkafka/admin/delete_consumer_group_offsets_handle"
|
|
19
|
+
require "rdkafka/admin/delete_consumer_group_offsets_report"
|
|
16
20
|
require "rdkafka/admin/create_topic_handle"
|
|
17
21
|
require "rdkafka/admin/create_topic_report"
|
|
18
22
|
require "rdkafka/admin/delete_groups_handle"
|
|
@@ -46,6 +50,8 @@ require "rdkafka/callbacks/base_handler"
|
|
|
46
50
|
require "rdkafka/callbacks/create_topic_handler"
|
|
47
51
|
require "rdkafka/callbacks/delete_topic_handler"
|
|
48
52
|
require "rdkafka/callbacks/create_partitions_handler"
|
|
53
|
+
require "rdkafka/callbacks/alter_consumer_group_offsets_handler"
|
|
54
|
+
require "rdkafka/callbacks/delete_consumer_group_offsets_handler"
|
|
49
55
|
require "rdkafka/callbacks/delete_groups_handler"
|
|
50
56
|
require "rdkafka/callbacks/delete_records_handler"
|
|
51
57
|
require "rdkafka/callbacks/create_acl_handler"
|
|
@@ -62,6 +68,8 @@ require "rdkafka/consumer/message"
|
|
|
62
68
|
require "rdkafka/consumer/partition"
|
|
63
69
|
require "rdkafka/consumer/allocator"
|
|
64
70
|
require "rdkafka/consumer/topic_partition_list"
|
|
71
|
+
require "rdkafka/share_consumer"
|
|
72
|
+
require "rdkafka/share_consumer/message"
|
|
65
73
|
require "rdkafka/error"
|
|
66
74
|
require "rdkafka/metadata"
|
|
67
75
|
require "rdkafka/native_kafka"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: karafka-rdkafka
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.30.0
|
|
5
5
|
platform: aarch64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Thijs Cadier
|
|
@@ -100,6 +100,8 @@ files:
|
|
|
100
100
|
- lib/rdkafka/abstract_handle.rb
|
|
101
101
|
- lib/rdkafka/admin.rb
|
|
102
102
|
- lib/rdkafka/admin/acl_binding_result.rb
|
|
103
|
+
- lib/rdkafka/admin/alter_consumer_group_offsets_handle.rb
|
|
104
|
+
- lib/rdkafka/admin/alter_consumer_group_offsets_report.rb
|
|
103
105
|
- lib/rdkafka/admin/config_binding_result.rb
|
|
104
106
|
- lib/rdkafka/admin/config_resource_binding_result.rb
|
|
105
107
|
- lib/rdkafka/admin/create_acl_handle.rb
|
|
@@ -110,6 +112,8 @@ files:
|
|
|
110
112
|
- lib/rdkafka/admin/create_topic_report.rb
|
|
111
113
|
- lib/rdkafka/admin/delete_acl_handle.rb
|
|
112
114
|
- lib/rdkafka/admin/delete_acl_report.rb
|
|
115
|
+
- lib/rdkafka/admin/delete_consumer_group_offsets_handle.rb
|
|
116
|
+
- lib/rdkafka/admin/delete_consumer_group_offsets_report.rb
|
|
113
117
|
- lib/rdkafka/admin/delete_groups_handle.rb
|
|
114
118
|
- lib/rdkafka/admin/delete_groups_report.rb
|
|
115
119
|
- lib/rdkafka/admin/delete_records_handle.rb
|
|
@@ -128,11 +132,13 @@ files:
|
|
|
128
132
|
- lib/rdkafka/admin/list_offsets_report.rb
|
|
129
133
|
- lib/rdkafka/bindings.rb
|
|
130
134
|
- lib/rdkafka/callbacks.rb
|
|
135
|
+
- lib/rdkafka/callbacks/alter_consumer_group_offsets_handler.rb
|
|
131
136
|
- lib/rdkafka/callbacks/base_handler.rb
|
|
132
137
|
- lib/rdkafka/callbacks/create_acl_handler.rb
|
|
133
138
|
- lib/rdkafka/callbacks/create_partitions_handler.rb
|
|
134
139
|
- lib/rdkafka/callbacks/create_topic_handler.rb
|
|
135
140
|
- lib/rdkafka/callbacks/delete_acl_handler.rb
|
|
141
|
+
- lib/rdkafka/callbacks/delete_consumer_group_offsets_handler.rb
|
|
136
142
|
- lib/rdkafka/callbacks/delete_groups_handler.rb
|
|
137
143
|
- lib/rdkafka/callbacks/delete_records_handler.rb
|
|
138
144
|
- lib/rdkafka/callbacks/delete_topic_handler.rb
|
|
@@ -162,6 +168,8 @@ files:
|
|
|
162
168
|
- lib/rdkafka/producer/delivery_report.rb
|
|
163
169
|
- lib/rdkafka/producer/partitions_count_cache.rb
|
|
164
170
|
- lib/rdkafka/producer/testing.rb
|
|
171
|
+
- lib/rdkafka/share_consumer.rb
|
|
172
|
+
- lib/rdkafka/share_consumer/message.rb
|
|
165
173
|
- lib/rdkafka/version.rb
|
|
166
174
|
- package-lock.json
|
|
167
175
|
- package.json
|
|
@@ -190,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
190
198
|
- !ruby/object:Gem::Version
|
|
191
199
|
version: '0'
|
|
192
200
|
requirements: []
|
|
193
|
-
rubygems_version: 4.0.
|
|
201
|
+
rubygems_version: 4.0.20
|
|
194
202
|
specification_version: 4
|
|
195
203
|
summary: The rdkafka gem is a modern Kafka client library for Ruby based on librdkafka.
|
|
196
204
|
It wraps the production-ready C client using the ffi gem and targets Kafka 1.0+
|