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.
@@ -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
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Rdkafka
4
4
  # Current rdkafka-ruby gem version
5
- VERSION = "0.28.2"
5
+ VERSION = "0.30.0"
6
6
  # Target librdkafka version to be used
7
- LIBRDKAFKA_VERSION = "2.14.2"
7
+ LIBRDKAFKA_VERSION = "2.15.1"
8
8
  # SHA256 hash of the librdkafka source tarball for verification
9
- LIBRDKAFKA_SOURCE_SHA256 = "d7eec9c31c817fa44402f679c252dfbf97e4c338a849a25c3579a31fd127beb8"
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.28.2
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.16
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+