karafka-rdkafka 0.30.0 → 0.30.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 290a1e79997166534339567e0f46bf560191141a22c754bdd67c656ea68594f4
4
- data.tar.gz: 972ca0af039c92a2b68d6767214ede8c06d7da0eaffde72f98c79bfcef0b8943
3
+ metadata.gz: 7b68458fe1759b2645cce196bd5a602c236fe36853e428940641301272d5ecaf
4
+ data.tar.gz: 6248362a62a402e6004286abc9a7c1f7f29baa9ac375983fa1fc0a2d64159fc3
5
5
  SHA512:
6
- metadata.gz: 33ba78845a20d938ad79d61c859b08303dfe28ffdbf6a0fa5e99184805fa1d842153c0d676ba6fa25640241b71b2188a65d8ac58208e78526a5e2e3eedc433ad
7
- data.tar.gz: 92f7639fca0c51e06844f705285df809a8680315d44531953d6d9b0049aff2964a7633933c31522b8fd75f4bb1606cb13f6a1a5c1d097e8a3f7011b5f3e04083
6
+ metadata.gz: c69f725864465b708ac3159db0df5a8597e8cf6894f6108ed5ca34654bb4e905fc6dc1e79bd6689bbd2ff4378f4e7bb2f1edfb1bda4416e944912154fda608de
7
+ data.tar.gz: e0193cf81b907d704b97a27c1d0998f14b83ffb12361b8f0557e87b4345ae58c29caa2d2544192e8524a9feb719b3d707bb42b24cf2d4079ace9d481075277db
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Rdkafka Changelog
2
2
 
3
+ ## 0.30.2 (2026-10-01)
4
+ - [Feature] Add `ShareConsumer#events_poll` (and `#events_poll_nb`) to service the statistics, error, log and OAuthBearer callbacks without acquiring records.
5
+
6
+ ## 0.30.1 (2026-09-30)
7
+ - [Fix] Derive `ShareConsumer#name` from the native handle at creation (mirroring `Consumer#name`) instead of leaving it `nil` until an OAuthBearer callback happened to set it. A `nil` name meant downstreams that route the global statistics and error callbacks by client name (e.g. Karafka) silently dropped every share-consumer statistic and background error.
8
+
3
9
  ## 0.30.0 (2026-09-25)
4
10
  - [Fix] Register share consumers in `Rdkafka::Clients` and destroy the native handle when `Config#share_consumer` fails part-way. Share consumers own the native handle directly rather than a `NativeKafka` wrapper, so they bypass `build_native_client` and were missed by the `at_exit` shutdown hook, leaving librdkafka to be unloaded with a live share handle.
5
11
  - [Feature] Add preview support for KIP-932 share groups ("Queues for Kafka") via `Rdkafka::ShareConsumer`, created with `Config#share_consumer`. Members consume the same partitions cooperatively with per-record acknowledgements (`:accept`, `:release`, `:reject`) instead of committed offsets. Like the regular `Consumer`, `ShareConsumer` is a thin binding that exposes the librdkafka share primitives (`#subscribe`/`#unsubscribe`/`#subscription`, batch `#poll`, `#acknowledge`, `#commit_sync`, `#commit_async`, `#acknowledgement_commit_callback=`, `#close`) but drives none of them itself - there is no built-in poll loop, acknowledgement strategy or offset/lifecycle orchestration, leaving that to a higher layer such as Karafka. The handle is single-threaded by design, servicing log, statistics and error callbacks from within `#poll`. Requires a broker with share groups enabled (Apache Kafka 4.2.0+); librdkafka marks the feature as preview and not production-ready.
@@ -517,6 +517,18 @@ module Rdkafka
517
517
  # rd_kafka_share_consumer_new instead of rd_kafka_new. It is single-threaded by design and
518
518
  # librdkafka itself rejects concurrent access with RD_KAFKA_RESP_ERR__CONFLICT.
519
519
 
520
+ # Minimal mirror of the head of librdkafka's `struct rd_kafka_share_s`, whose first member is
521
+ # the wrapped `rd_kafka_t` (`rkshare_rk`). `rd_kafka_share_t` has no public accessors of its
522
+ # own - not even for the client name - but the handle it wraps is a normal `rd_kafka_t` and is
523
+ # the very handle librdkafka passes to the statistics, error and OAuthBearer callbacks. Reading
524
+ # it lets a share consumer report a name that matches its statistics `name` field, which
525
+ # `rd_kafka_name` cannot do when handed the `rd_kafka_share_t` directly (it would read an
526
+ # unrelated field and return garbage). This mirrors how the gem already maps other librdkafka
527
+ # structs by layout for the pinned librdkafka version.
528
+ class NativeShareConsumer < FFI::Struct
529
+ layout :rkshare_rk, :pointer
530
+ end
531
+
520
532
  # Acknowledge types for share consumer records
521
533
  RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_ACCEPT = 1
522
534
  RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_RELEASE = 2
@@ -51,6 +51,10 @@ module Rdkafka
51
51
  # @see ShareConsumer#poll
52
52
  SHARE_CONSUMER_POLL_TIMEOUT_MS = 250
53
53
 
54
+ # Share consumer timeout for events_poll (0 = non-blocking async)
55
+ # @see ShareConsumer#events_poll
56
+ SHARE_CONSUMER_EVENTS_POLL_TIMEOUT_MS = 0
57
+
54
58
  # Share consumer timeout for synchronous acknowledgement commits
55
59
  # @see ShareConsumer#commit_sync
56
60
  SHARE_CONSUMER_COMMIT_SYNC_TIMEOUT_MS = 5_000
@@ -22,9 +22,10 @@ module Rdkafka
22
22
  # share consumer per thread. librdkafka enforces this and calls from a second thread raise
23
23
  # an `RdkafkaError` with code `conflict`. This class is deliberately not built on
24
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.
25
+ # are all serviced from within {#poll}, or from {#events_poll} when the caller is not polling
26
+ # for records) and the native handle must never be used from a polling thread, so only the
27
+ # {#close} interaction is synchronized here. {#close} may be called from any thread and waits
28
+ # for in-flight calls to finish before tearing down.
28
29
  #
29
30
  # Preview limitations inherited from librdkafka worth knowing at this layer:
30
31
  # - `max.poll.records` (default 500) is a soft bound
@@ -57,14 +58,17 @@ module Rdkafka
57
58
  private_constant :State
58
59
 
59
60
  # 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]
61
+ # correlate the global callbacks (statistics, error and the OAuthBearer token refresh
62
+ # callback, all of which carry the client name) with this instance. Captured from the native
63
+ # handle when the consumer is created, so it is available immediately - before the first
64
+ # {#poll} and without OAuth configured - and stays readable after the consumer is closed or
65
+ # inherited by a forked child.
66
+ # @return [String] the librdkafka client name
64
67
  attr_reader :name
65
68
 
66
69
  # @private
67
- # Captures the librdkafka client name once known (from callback context)
70
+ # Lets the OAuthBearer token refresh callback set the name from its callback context. It
71
+ # carries the same `rd_kafka_name` value already captured at creation, so both paths agree.
68
72
  attr_writer :name
69
73
 
70
74
  # @private
@@ -80,6 +84,18 @@ module Rdkafka
80
84
  # every teardown path can skip the native destroy in a forked child (librdkafka is not
81
85
  # fork-safe).
82
86
  @state = State.new(native, nil, Process.pid)
87
+ # Capture the librdkafka client name up front. It is needed to correlate this consumer with
88
+ # the global statistics and error callbacks (which carry the client name) - the common
89
+ # downstream pattern (e.g. Karafka) filters those broadcast callbacks by matching `name`.
90
+ #
91
+ # `rd_kafka_name` has no `rd_kafka_share_t` overload: handing it the share handle reads an
92
+ # unrelated field and returns garbage. The share handle's first member is the `rd_kafka_t`
93
+ # it wraps (`rkshare_rk`), which is the same handle librdkafka passes to those callbacks, so
94
+ # the name read from it matches the statistics `name` field. Capturing it here (rather than
95
+ # lazily like the regular consumer) keeps it available before the first poll, without OAuth,
96
+ # and after close or in a forked child, where the native handle must not be touched.
97
+ # librdkafka assigns the name at creation, so no broker connection is required.
98
+ @name = Rdkafka::Bindings.rd_kafka_name(Rdkafka::Bindings::NativeShareConsumer.new(native)[:rkshare_rk])
83
99
  # Guards handle teardown against in-flight calls: increments happen under this mutex and
84
100
  # close holds it while draining and destroying (mirrors NativeKafka's approach)
85
101
  @access_mutex = Mutex.new
@@ -249,6 +265,50 @@ module Rdkafka
249
265
  end
250
266
  end
251
267
 
268
+ # Polls the main rdkafka queue (not the share one), serving queued global callbacks without
269
+ # acquiring any records.
270
+ #
271
+ # Events will cause application-provided callbacks to be called:
272
+ # - error callbacks
273
+ # - stats callbacks
274
+ # - log and OAuthBearer token refresh callbacks
275
+ #
276
+ # Lets a caller keep those callbacks flowing while it is not polling for records (waiting on
277
+ # in-flight work, quieting, or draining on shutdown). Does **NOT** replace {#poll}, which
278
+ # drains the same queue while acquiring records; the "one share consumer per thread" rule
279
+ # means the two never run concurrently.
280
+ #
281
+ # @param timeout_ms [Integer] poll timeout. If set to 0 will run async, when set to -1 will
282
+ # block until any events available.
283
+ # @return [Integer] number of events served
284
+ # @raise [ClosedConsumerError] when the consumer is closed or closing
285
+ def events_poll(timeout_ms = Defaults::SHARE_CONSUMER_EVENTS_POLL_TIMEOUT_MS)
286
+ with_native(__method__) do |native|
287
+ # The main queue lives on the rd_kafka_t the share handle wraps (rkshare_rk), which is the
288
+ # handle librdkafka hands the callbacks; rd_kafka_poll has to run against it (see #name).
289
+ inner = Rdkafka::Bindings::NativeShareConsumer.new(native)[:rkshare_rk]
290
+ Rdkafka::Bindings.rd_kafka_poll(inner, timeout_ms)
291
+ end
292
+ end
293
+
294
+ # Polls the main rdkafka queue without releasing the GVL (Global VM Lock).
295
+ #
296
+ # This is more efficient than {#events_poll} for non-blocking `poll(0)` calls, particularly
297
+ # useful in fiber scheduler contexts where GVL release/reacquire overhead is wasteful since we
298
+ # don't expect to wait.
299
+ #
300
+ # @param timeout_ms [Integer] poll timeout (default: 0 for non-blocking)
301
+ # @return [Integer] number of events served
302
+ # @raise [ClosedConsumerError] when the consumer is closed or closing
303
+ #
304
+ # @see #events_poll for more details on when to use this method
305
+ def events_poll_nb(timeout_ms = 0)
306
+ with_native(__method__) do |native|
307
+ inner = Rdkafka::Bindings::NativeShareConsumer.new(native)[:rkshare_rk]
308
+ Rdkafka::Bindings.rd_kafka_poll_nb(inner, timeout_ms)
309
+ end
310
+ end
311
+
252
312
  # Acknowledges a message delivered by {#poll} in explicit acknowledgement mode
253
313
  # (`share.acknowledgement.mode` set to `explicit`).
254
314
  #
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rdkafka
4
4
  # Current rdkafka-ruby gem version
5
- VERSION = "0.30.0"
5
+ VERSION = "0.30.2"
6
6
  # Target librdkafka version to be used
7
7
  LIBRDKAFKA_VERSION = "2.15.1"
8
8
  # SHA256 hash of the librdkafka source tarball for verification
data/renovate.json CHANGED
@@ -1,4 +1,7 @@
1
1
  {
2
+ "bundler": {
3
+ "managerFilePatterns": ["/(^|/)Gemfile\\.lint$/"]
4
+ },
2
5
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
6
  "extends": [
4
7
  "config:recommended"
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.30.0
4
+ version: 0.30.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thijs Cadier