karafka-rdkafka 0.30.1-aarch64-linux-gnu → 0.31.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -27
  3. data/README.md +1 -0
  4. data/docker-compose-ssl.yml +1 -0
  5. data/docker-compose.yml +1 -0
  6. data/ext/librdkafka.so +0 -0
  7. data/karafka-rdkafka.gemspec +1 -1
  8. data/lib/rdkafka/admin/describe_cluster_handle.rb +31 -0
  9. data/lib/rdkafka/admin/describe_cluster_report.rb +48 -0
  10. data/lib/rdkafka/admin/describe_consumer_groups_handle.rb +31 -0
  11. data/lib/rdkafka/admin/describe_consumer_groups_report.rb +151 -0
  12. data/lib/rdkafka/admin/describe_result_parsing.rb +47 -0
  13. data/lib/rdkafka/admin/describe_topics_handle.rb +31 -0
  14. data/lib/rdkafka/admin/describe_topics_report.rb +118 -0
  15. data/lib/rdkafka/admin.rb +245 -0
  16. data/lib/rdkafka/bindings/admin/acls.rb +85 -0
  17. data/lib/rdkafka/bindings/admin/configs.rb +37 -0
  18. data/lib/rdkafka/bindings/admin/list_offsets.rb +26 -0
  19. data/lib/rdkafka/bindings/admin.rb +197 -0
  20. data/lib/rdkafka/bindings/callbacks.rb +148 -0
  21. data/lib/rdkafka/bindings/configuration.rb +50 -0
  22. data/lib/rdkafka/bindings/consumer.rb +83 -0
  23. data/lib/rdkafka/bindings/errors.rb +17 -0
  24. data/lib/rdkafka/bindings/handle.rb +16 -0
  25. data/lib/rdkafka/bindings/messages.rb +28 -0
  26. data/lib/rdkafka/bindings/metadata.rb +17 -0
  27. data/lib/rdkafka/bindings/polling.rb +17 -0
  28. data/lib/rdkafka/bindings/producer.rb +82 -0
  29. data/lib/rdkafka/bindings/share_consumer.rb +67 -0
  30. data/lib/rdkafka/bindings/structs.rb +62 -0
  31. data/lib/rdkafka/bindings.rb +13 -775
  32. data/lib/rdkafka/callbacks/describe_cluster_handler.rb +36 -0
  33. data/lib/rdkafka/callbacks/describe_consumer_groups_handler.rb +36 -0
  34. data/lib/rdkafka/callbacks/describe_topics_handler.rb +36 -0
  35. data/lib/rdkafka/callbacks.rb +10 -1
  36. data/lib/rdkafka/clients.rb +13 -46
  37. data/lib/rdkafka/consumer/message.rb +6 -9
  38. data/lib/rdkafka/defaults.rb +4 -0
  39. data/lib/rdkafka/producer/delivery_handle.rb +18 -16
  40. data/lib/rdkafka/producer/delivery_report.rb +51 -1
  41. data/lib/rdkafka/share_consumer.rb +54 -3
  42. data/lib/rdkafka/version.rb +3 -3
  43. data/lib/rdkafka.rb +10 -0
  44. data/package-lock.json +9 -9
  45. data/renovate.json +3 -0
  46. metadata +27 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7948198b70e6be9ea2ceae0392b50b1babc7875c4986028e2e244f2391391be2
4
- data.tar.gz: ad3a6555db52fad384f1ebc1a7b45a8b0e058d85396c4e4c775091be73dae645
3
+ metadata.gz: 31a31d86d0d0ee5888adc297e5bfc9338f18b453b4834095fd4159f6562458ad
4
+ data.tar.gz: 1da9da2a1268fb3a9256475dd5cb7e8cccaf8dbaaa061436ae4a1174ba1f3f44
5
5
  SHA512:
6
- metadata.gz: 5b53dfba1d8765777243906de1ef4522c8466c14e54316b418a74a1feb70218ed347f089a52a10359474d5bca63b911aeb88ffd7018d82b29fe8879e9de69ab9
7
- data.tar.gz: f261e03bfa7eaf8f7bbcf85ac1563214f066759b1d6aa805e9997aa17eaa141349ea9cad3b13b88d401bad8baf68f934c45bb10c56f4c99fc95a16fbc9cd90fa
6
+ metadata.gz: 70d99fbf5bf5d225f51b461266c8aac8a4f21e7d5ea860ffd17aa94429ec44bfb52e84e7d770e37b9297218e38147610ecdac24181d93a8c6910c4e791e8a52c
7
+ data.tar.gz: 52271992a6802a31a8032264d9a7b61b8f890cdfc479c22402ea9c59c22193f77e5f637318a1701dea71e9c442d03162c1401b3383331d3702b4676f7f978a01
data/CHANGELOG.md CHANGED
@@ -1,30 +1,49 @@
1
1
  # Rdkafka Changelog
2
2
 
3
+ ## 0.31.0 (2026-10-12)
4
+ - [Feature] Add `Admin#describe_cluster` to get the cluster id, controller and broker nodes (with racks) without a full metadata request. Ported from rdkafka-ruby (#1015).
5
+ - [Feature] Add `Admin#describe_topics` to get topic ids, partition leaders and replicas, and per-topic errors. Both methods can also return the operations the client is authorized to perform. Ported from rdkafka-ruby (#1015).
6
+ - [Feature] Add `Admin#describe_consumer_groups` to read the state, type, coordinator, authorized operations and members (with their current and target partition assignments) of consumer groups. Ported from rdkafka-ruby (#1016).
7
+ - [Feature] Add `status`, `latency` and `broker_id` to `Producer::DeliveryReport`, with `#persisted?`, `#possibly_persisted?` and `#not_persisted?`, so a failed delivery tells whether a retry may duplicate the message. Ported from rdkafka-ruby (#1019).
8
+ - [Enhancement] Bump librdkafka to `2.16.0` for OpenSSL and libcurl security fixes. Admin requests in flight on a decommissioned broker now fail with a retryable transport error. Ported from rdkafka-ruby (#1020).
9
+ - [Enhancement] Bump the bundled OpenSSL to `3.5.9` (LTS) for its security fixes. Ported from rdkafka-ruby (#1007).
10
+ - [Enhancement] Read each pointer field once in `Message#initialize`, saving 3 allocations per consumed message. Ported from rdkafka-ruby (#1026).
11
+ - [Change] Drop Ruby 3.2 support; require Ruby 3.3+.
12
+ - [Change] Build the precompiled `aarch64-linux-musl` gem on Alpine `3.24` (was `3.23`), matching `x86_64-linux-musl`. Ported from rdkafka-ruby (#1012).
13
+ - [Change] Build the precompiled `macos_arm64` library on macOS 26 with the minimum macOS pinned to 14.0, instead of inheriting it from the build host. Ported from rdkafka-ruby (#1013).
14
+ - [Maintenance] Split `lib/rdkafka/bindings.rb` into per-area files under `lib/rdkafka/bindings/`. `Rdkafka::Bindings` is unchanged. Ported from rdkafka-ruby (#1022).
15
+ - [Maintenance] Stabilize the flaky partitions count cache "multiple topics with different TTLs" spec by driving its clock instead of sleeping. Ported from rdkafka-ruby (#1028).
16
+ - [Fix] Sort merged share-group async acknowledgements so the broker does not reject them with `invalid_request` (librdkafka patch).
17
+ - [Fix] Release share records acquired by an in-flight fetch on unsubscribe instead of keeping them locked (librdkafka patch). `ShareConsumer#unsubscribe` now closes the share sessions like `#close` does and waits for that (up to `fetch.wait.max.ms` with a fetch in flight).
18
+
19
+ ## 0.30.2 (2026-10-01)
20
+ - [Feature] Add `ShareConsumer#events_poll` (and `#events_poll_nb`) to service the statistics, error, log and OAuthBearer callbacks without acquiring records.
21
+
3
22
  ## 0.30.1 (2026-09-30)
4
- - [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.
23
+ - [Fix] Set `ShareConsumer#name` from the native handle at creation, so name-based callback routing (e.g. Karafka) no longer drops share-consumer statistics and errors.
5
24
 
6
25
  ## 0.30.0 (2026-09-25)
7
- - [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.
8
- - [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.
9
- - [Enhancement] `RdkafkaError.build_from_c` now uses the human readable string librdkafka attaches to the `rd_kafka_error_t` instead of always using `rd_kafka_err2str`, improving diagnostics for every error-pointer path. It also destroys a code-0 error object instead of leaking it on the early return.
10
- - [Enhancement] Extract the OAuthBearer token buffer/extension plumbing in `Helpers::OAuth` around two private native-call hooks so the share consumer overrides only the hooks instead of duplicating both public methods.
11
- - [Fix] Make `ShareConsumer#close` safe against in-flight calls from other threads: closing now waits for operations in progress (mirroring `NativeKafka`), preventing a NULL handle from reaching librdkafka or a double destroy when two threads race `close`.
12
- - [Fix] Keep the `ShareConsumer` native handle and its GC finalizer when `close`/`destroy` report an error. librdkafka leaves the instance intact in that case, so dropping the references beforehand leaked the client permanently; `close` is now retriable after such an error.
13
- - [Fix] Pin the acknowledgement commit callback `FFI::Function` in the state holder shared with the `ShareConsumer` GC finalizer, preventing a use-after-free when an unclosed consumer is garbage collected and the finalizer-driven destroy flushes pending acknowledgements through it.
14
- - [Fix] Make `ShareConsumer` fork-aware, mirroring `NativeKafka`. `rd_kafka_share_consumer_new` is backed by the same broker/main threads as any librdkafka client, which do not survive `fork`; the hand-rolled share lifecycle previously had no `@creator_pid` guard, so a process that forked after creating a share consumer would segfault (or deadlock) when the child's GC finalizer or `close` ran `rd_kafka_share_destroy` on the inherited handle. It now records the creating pid and reports `closed?` in any other process, so the finalizer and `close` skip the native teardown for an inherited handle while `with_native` rejects any use of it.
15
- - [Fix] Scope `ShareConsumer#each`'s `ClosedConsumerError` rescue to the `poll` call only. It previously wrapped the `yield`, so a `ClosedConsumerError` raised from inside the caller's block was swallowed and silently ended iteration; the block's errors now propagate, and only the consumer actually closing ends the loop. Its docs also note that, like `#poll`, `#each` can yield an `RdkafkaError` for a record that fails to build.
16
- - [Fix] Raise `ArgumentError` for `ShareConsumer#subscribe` without topics. librdkafka treats an empty share subscribe as an unsubscribe and returns no error (unlike the regular consumer), silently dropping the subscription.
17
- - [Fix] Capture the librdkafka client name onto `ShareConsumer#name` from the OAuthBearer token refresh callback. The share handle has no native name accessor, which made the documented name-based routing of the global oauth callback impossible for share consumers.
26
+ - [Fix] Register share consumers in `Rdkafka::Clients` and destroy the native handle when `Config#share_consumer` fails part-way, so a share consumer is no longer missed by the `at_exit` shutdown hook.
27
+ - [Feature] Add preview support for KIP-932 share groups ("Queues for Kafka") via `Rdkafka::ShareConsumer` (`Config#share_consumer`), with per-record acknowledgements (`:accept`, `:release`, `:reject`). Requires Apache Kafka 4.2.0+ with share groups enabled. Not production-ready.
28
+ - [Enhancement] `RdkafkaError.build_from_c` now uses the human-readable string librdkafka attaches to the `rd_kafka_error_t` instead of `rd_kafka_err2str`, improving diagnostics for every error-pointer path.
29
+ - [Enhancement] Refactor the `Helpers::OAuth` token plumbing around two private hooks so the share consumer overrides only those instead of duplicating both public methods.
30
+ - [Fix] Make `ShareConsumer#close` thread-safe: it now waits for in-flight operations (mirroring `NativeKafka`), preventing a double destroy when two threads race `close`.
31
+ - [Fix] Keep the `ShareConsumer` native handle and its GC finalizer when `close`/`destroy` report an error, so the client is no longer leaked and `close` stays retriable.
32
+ - [Fix] Pin the acknowledgement commit callback `FFI::Function` for the lifetime of the `ShareConsumer`, preventing a use-after-free when an unclosed consumer is garbage collected.
33
+ - [Fix] Make `ShareConsumer` fork-aware (mirroring `NativeKafka`): it reports `closed?` in a forked child, which no longer segfaults on `close` or finalization.
34
+ - [Fix] Rescue `ClosedConsumerError` in `ShareConsumer#each` only around `poll`, so a `ClosedConsumerError` raised inside the block propagates instead of silently ending iteration. Like `#poll`, `#each` can also yield an `RdkafkaError` for a record that fails to build.
35
+ - [Fix] Raise `ArgumentError` for `ShareConsumer#subscribe` without topics, which librdkafka otherwise treats as an unsubscribe without error, silently dropping the subscription.
36
+ - [Fix] Capture the librdkafka client name onto `ShareConsumer#name` from the OAuthBearer token refresh callback, since the share handle has no native name accessor for the documented name-based oauth callback routing.
18
37
  - [Fix] Update `ext/build_common.sh` (precompiled builds) to librdkafka `2.15.0` and its tarball checksum; it still pinned `2.14.1`, whose tarball is no longer vendored.
19
- - [Fix] Stop the `share_consumer_multi_member` integration spec from failing on redelivery. KIP-932 share groups are documented as at-least-once, so a record whose acquisition lock expires before it is acknowledged may be redelivered with an incremented `delivery_count`; the spec now tracks distinct payloads and fails only when a record is never delivered.
20
- - [Fix] Apply the same at-least-once tolerance to the `share_consumer_explicit_ack` integration spec, which still asserted exactly one redelivered record. It now requires only the released record to be redelivered (with `delivery_count >= 2`), forbids only the rejected record, and tolerates an accepted record reappearing when its acquisition lock expired before the commit flushed the accept.
21
- - [Fix] Add the `nofile` ulimit to `docker-compose-ssl.yml`, mirroring `docker-compose.yml`. The SSL CI broker runs the same topic-heavy suite (now enlarged by the share consumer specs) and could otherwise exhaust the default file-descriptor limit under the many short-lived topics.
22
- - [Fix] Enable KIP-932 share groups on the macOS CI broker, whose KRaft `server.properties` never opted into the share rebalance protocol, so the one broker-backed share consumer spec received zero records on every macOS runner. `CONFLUENT_VERSION` is bumped to `8.3.0` and the download path is derived from it so a Renovate bump cannot desync them.
23
- - [Enhancement] Add two share consumer integration specs: `share_consumer_implicit_ack_spec` (polling alone accepts the previous batch, and accepted records are not redelivered to a second member) and `share_consumer_multi_topic_spec` (one consumer subscribed to several topics receives every topic's records, attributed to the right topic). Both track distinct payloads so an at-least-once redelivery cannot end the consume loop early.
24
- - [Fix] Stop the `statistics_unassigned_producer` integration spec from flaking on a metadata-propagation race: the freshly built producer could send before fetching the new topic's metadata, so the produce is now retried on `unknown_topic_or_part` and `leader_not_available`.
25
- - [Fix] Also stabilize the `statistics_unassigned_producer` integration spec against a delivery wait that outlives its budget. With 1000 partitions librdkafka keeps the message queued while leaders are still being elected, so the handle stays pending and the wait raises `WaitTimeoutError` - a `RuntimeError` the spec's existing code-based rescue could never catch. The produce now uses the default 60s handle wait (matching the sibling `statistics_unassigned_*` specs, instead of a self-imposed 15s) and retries on that timeout too.
26
- - [Fix] Stabilize the `consumer_memberid_clusterid_leak` integration spec against RSS measurement noise on Ruby 4.0. It now compacts the heap before sampling (matching the `to_native_tpl` leak spec) and the ceiling is 3 MB, still well under the ~5.7 MB a genuine per-call leak would cost.
27
- - [Note] Share consumers emit the regular consumer statistics JSON (including `cgrp`; there is no share-specific section in librdkafka 2.15.0) through the usual `statistics_callback`, serviced from within `#poll`. The share fetch path bypasses the per-partition stats bookkeeping entirely, so the `topics` section carries no partition entries and the `statistics.unassigned.include` filter needs no share-consumer special-casing; per-partition share metrics are not exposed by librdkafka 2.15.0 at all.
38
+ - [Fix] Stop the `share_consumer_multi_member` integration spec from failing on an at-least-once redelivery; it now tracks distinct payloads and fails only when a record is never delivered.
39
+ - [Fix] Apply the same at-least-once tolerance to the `share_consumer_explicit_ack` integration spec: require only the released record to be redelivered, forbid the rejected one, and tolerate an accepted record reappearing.
40
+ - [Fix] Add the `nofile` ulimit to `docker-compose-ssl.yml` (mirroring `docker-compose.yml`) so the SSL CI broker does not exhaust file descriptors under the topic-heavy share-consumer suite.
41
+ - [Fix] Enable KIP-932 share groups on the macOS CI broker, whose KRaft config never opted into the share rebalance protocol, so its share-consumer spec no longer receives zero records. `CONFLUENT_VERSION` is bumped to `8.3.0`.
42
+ - [Enhancement] Add `share_consumer_implicit_ack` and `share_consumer_multi_topic` integration specs.
43
+ - [Fix] Stop the `statistics_unassigned_producer` integration spec from flaking on a metadata-propagation race by retrying the produce on `unknown_topic_or_part` and `leader_not_available`.
44
+ - [Fix] Also stabilize the `statistics_unassigned_producer` integration spec against a delivery wait that outlives its budget: it now uses the default 60s handle wait and retries on `WaitTimeoutError`.
45
+ - [Fix] Stabilize the `consumer_memberid_clusterid_leak` integration spec against RSS measurement noise on Ruby 4.0 by compacting the heap before sampling and raising the ceiling to 3 MB.
46
+ - [Note] Share consumers emit the regular consumer statistics through `statistics_callback`. librdkafka 2.15.0 has no share-specific metrics, so the `topics` section has no partition entries.
28
47
  - [Feature] Add `Admin#alter_consumer_group_offsets` and `Admin#delete_consumer_group_offsets` to set or clear a consumer group's committed offsets from the admin client. Ported from rdkafka-ruby (#983).
29
48
  - [Enhancement] Bump librdkafka to `2.15.1` for OpenSSL and libcurl security fixes. It changes how IPv6 addresses are formatted and validated against broker certificates.
30
49
  - [Enhancement] Bump the bundled zlib to `1.3.2`.
@@ -34,8 +53,8 @@
34
53
  - [Maintenance] Drop the unused `dist/openssl-3.0.16.tar.gz` build cache.
35
54
  - [Fix] Honor the `isolation_level:` argument to `Admin#list_offsets`, which was silently ignored. Ported from rdkafka-ruby (#983).
36
55
  - [Fix] Return correct results for every item of a multi-item topic, partition, group or ACL admin request, not just the first. Ported from rdkafka-ruby (#987).
37
- - [Maintenance] Fix a use-after-free in the multi-item admin integration test that read result-name pointers after librdkafka had destroyed the background event, which made it flaky on newer glibc (e.g. Debian trixie). Ported from rdkafka-ruby (#991).
38
- - [Maintenance] Stabilize consumer specs on slow CI runners: wait longer for a partition assignment, raise the `TestTopics.create` admin timeout (a timeout there also tripped the leaked-handle guard), give the non-blocking `poll_nb` message check a generous retry budget instead of a fixed ~2s, and let the long-running consumption spec drain its backlog instead of stopping at a fixed 60s. The assignment wait is tunable via `RDKAFKA_TEST_ASSIGNMENT_TIMEOUT`. Ported from rdkafka-ruby (#990).
56
+ - [Maintenance] Fix a use-after-free in the multi-item admin integration test that made it flaky on newer glibc. Ported from rdkafka-ruby (#991).
57
+ - [Maintenance] Stabilize consumer specs on slow CI runners. The assignment wait is tunable via `RDKAFKA_TEST_ASSIGNMENT_TIMEOUT`. Ported from rdkafka-ruby (#990).
39
58
 
40
59
  ## 0.29.0 (2026-09-14)
41
60
  - [Enhancement] Bump librdkafka to `2.15.0` (staying on `2.15.0` rather than `2.15.1` so users hitting a regression in `2.15.1` have a stable fallback).
@@ -123,8 +142,8 @@
123
142
  - [Fix] Prevent cascading test failures in admin specs when a single handle leaks into the registry (from upstream).
124
143
 
125
144
  ## 0.24.0 (2026-02-25)
126
- - **[Feature]** Add `Producer#queue_size` (and `#queue_length` alias) to report the number of messages waiting in the librdkafka output queue. Useful for monitoring producer backpressure, implementing custom flow control, debugging message delivery issues, and graceful shutdown logic.
127
- - **[Feature]** Add fiber scheduler API for integration with Ruby fiber schedulers (Falcon, Async) and custom event loops (from upstream). Expose `enable_queue_io_events` and `enable_background_queue_io_events` methods on `Consumer`, `Producer`, and `Admin`.
145
+ - **[Feature]** Add `Producer#queue_size` (and the `#queue_length` alias) to report the number of messages waiting in the librdkafka output queue.
146
+ - **[Feature]** Add `enable_queue_io_events` and `enable_background_queue_io_events` to `Consumer`, `Producer` and `Admin` for fiber schedulers (Falcon, Async) and custom event loops (from upstream).
128
147
  - **[Deprecation]** `AbstractHandle#wait` parameter `max_wait_timeout` (seconds) is deprecated in favor of `max_wait_timeout_ms` (milliseconds). The old parameter still works with backwards compatibility but will be removed in v1.0.0.
129
148
  - **[Deprecation]** `PartitionsCountCache` constructor parameter `ttl` (seconds) is deprecated in favor of `ttl_ms` (milliseconds). The old parameter still works with backwards compatibility but will be removed in v1.0.0.
130
149
  - [Enhancement] Add Ruby 4.0 support.
@@ -151,7 +170,7 @@
151
170
  - [Enhancement] Add `RdkafkaError.build_fatal` class method for centralized fatal error construction.
152
171
  - [Enhancement] Add comprehensive tests for fatal error handling including unit tests and integration tests.
153
172
  - [Enhancement] Add `RD_KAFKA_PARTITION_UA` constant for unassigned partition (-1).
154
- - [Enhancement] Replace magic numbers with named constants: use `RD_KAFKA_RESP_ERR_NO_ERROR` instead of `0` for error code checks (18 instances) and `RD_KAFKA_PARTITION_UA` instead of `-1` for partition values (9 instances) across the codebase for better code clarity and maintainability.
173
+ - [Enhancement] Replace magic numbers with the `RD_KAFKA_RESP_ERR_NO_ERROR` and `RD_KAFKA_PARTITION_UA` constants.
155
174
  - [Enhancement] Add `Rdkafka::Testing` module for testing fatal error scenarios on both producers and consumers.
156
175
  - [Deprecated] `RdkafkaError.validate_fatal!` - use `validate!` with `client_ptr` parameter instead.
157
176
 
@@ -165,7 +184,7 @@
165
184
 
166
185
  ## 0.22.1 (2025-10-09)
167
186
  - [Enhancement] Optimize header processing to eliminate double hash lookups and method checking overhead.
168
- - [Enhancement] Optimize producer header processing with early returns and efficient array operations (69% faster for nil headers, 41% faster for empty headers, 12-32% faster when headers are present, with larger improvements for complex header scenarios).
187
+ - [Enhancement] Speed up producer header processing (69% faster for nil headers, 41% for empty headers, 12-32% when headers are present).
169
188
 
170
189
  ## 0.22.0 (2025-09-26)
171
190
  - **[EOL]** Drop support for Ruby 3.1 to move forward with the fiber scheduler work.
data/README.md CHANGED
@@ -63,6 +63,7 @@ Contributions should generally be made to the upstream [rdkafka-ruby repository]
63
63
 
64
64
  | karafka-rdkafka | librdkafka | patches |
65
65
  |-|-|-|
66
+ | 0.31.x (2026-10-12) | 2.16.0 (2026-10-06) | yes |
66
67
  | 0.30.x (2026-09-25) | 2.15.1 (2026-09-09) | yes |
67
68
  | 0.29.x (2026-09-14) | 2.15.0 (2026-06-30) | yes |
68
69
  | 0.28.x (2026-07-12) | 2.14.2 (2026-06-03) | yes |
@@ -25,6 +25,7 @@ services:
25
25
  KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL
26
26
  KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9093
27
27
  KAFKA_BROKER_ID: 1
28
+ KAFKA_BROKER_RACK: rack-1
28
29
  KAFKA_CONTROLLER_QUORUM_VOTERS: 1@127.0.0.1:9094
29
30
  ALLOW_PLAINTEXT_LISTENER: 'yes'
30
31
  KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false'
data/docker-compose.yml CHANGED
@@ -23,6 +23,7 @@ services:
23
23
  KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
24
24
  KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092
25
25
  KAFKA_BROKER_ID: 1
26
+ KAFKA_BROKER_RACK: rack-1
26
27
  KAFKA_CONTROLLER_QUORUM_VOTERS: 1@127.0.0.1:9093
27
28
  ALLOW_PLAINTEXT_LISTENER: 'yes'
28
29
  KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false'
data/ext/librdkafka.so CHANGED
Binary file
@@ -12,7 +12,7 @@ Gem::Specification.new do |gem|
12
12
  gem.name = "karafka-rdkafka"
13
13
  gem.require_paths = ["lib"]
14
14
  gem.version = Rdkafka::VERSION
15
- gem.required_ruby_version = ">= 3.2"
15
+ gem.required_ruby_version = ">= 3.3"
16
16
 
17
17
  files = `git ls-files`.split($\)
18
18
  files = files.reject do |file|
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Handle for describe cluster operation
6
+ class DescribeClusterHandle < AbstractHandle
7
+ layout :pending, :bool,
8
+ :response, :int
9
+
10
+ # @return [String] the name of the operation
11
+ def operation_name
12
+ "describe cluster"
13
+ end
14
+
15
+ # @return [DescribeClusterReport] report prepared by the background event callback, with
16
+ # the cluster id, controller and broker nodes.
17
+ def create_result
18
+ prepared_result
19
+ end
20
+
21
+ # Raises an error if the operation failed
22
+ # @raise [RdkafkaError]
23
+ def raise_error
24
+ raise RdkafkaError.new(
25
+ self[:response],
26
+ broker_message: broker_message
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Report for describe cluster operation result
6
+ class DescribeClusterReport
7
+ include DescribeResultParsing
8
+
9
+ # @return [String, nil] the cluster id
10
+ attr_reader :cluster_id
11
+
12
+ # The current controller node, as a hash with `:id` [Integer], `:host` [String],
13
+ # `:port` [Integer] and `:rack` [String, nil]
14
+ # @return [Hash, nil]
15
+ attr_reader :controller
16
+
17
+ # Broker nodes of the cluster, each in the same shape as {#controller}
18
+ # @return [Array<Hash>]
19
+ attr_reader :nodes
20
+
21
+ # Operations the client is authorized to perform on the cluster, as
22
+ # `Bindings::RD_KAFKA_ACL_OPERATION_*` codes. `nil` unless requested with
23
+ # `include_authorized_operations: true`.
24
+ # @return [Array<Integer>, nil]
25
+ attr_reader :authorized_operations
26
+
27
+ # @param result_ptr [FFI::Pointer] pointer to the `rd_kafka_DescribeCluster_result_t`
28
+ def initialize(result_ptr)
29
+ @nodes = []
30
+
31
+ return if result_ptr.null?
32
+
33
+ cluster_id_ptr = Bindings.rd_kafka_DescribeCluster_result_cluster_id(result_ptr)
34
+ @cluster_id = cluster_id_ptr.null? ? nil : cluster_id_ptr.read_string
35
+ @controller = extract_node(Bindings.rd_kafka_DescribeCluster_result_controller(result_ptr))
36
+
37
+ count_ptr = FFI::MemoryPointer.new(:size_t)
38
+ @nodes = extract_nodes(Bindings.rd_kafka_DescribeCluster_result_nodes(result_ptr, count_ptr), count_ptr)
39
+
40
+ count_ptr = FFI::MemoryPointer.new(:size_t)
41
+ @authorized_operations = extract_authorized_operations(
42
+ Bindings.rd_kafka_DescribeCluster_result_authorized_operations(result_ptr, count_ptr),
43
+ count_ptr
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Handle for describe consumer groups operation
6
+ class DescribeConsumerGroupsHandle < AbstractHandle
7
+ layout :pending, :bool,
8
+ :response, :int
9
+
10
+ # @return [String] the name of the operation
11
+ def operation_name
12
+ "describe consumer groups"
13
+ end
14
+
15
+ # @return [DescribeConsumerGroupsReport] report prepared by the background event callback,
16
+ # with the described consumer groups.
17
+ def create_result
18
+ prepared_result
19
+ end
20
+
21
+ # Raises an error if the operation failed
22
+ # @raise [RdkafkaError]
23
+ def raise_error
24
+ raise RdkafkaError.new(
25
+ self[:response],
26
+ broker_message: broker_message
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Report for describe consumer groups operation result
6
+ class DescribeConsumerGroupsReport
7
+ include DescribeResultParsing
8
+
9
+ # Described consumer groups, one per requested group id. Each entry is a hash with:
10
+ # - `:group_id` [String] the consumer group id
11
+ # - `:error` [RdkafkaError, nil] error describing this group, or `nil` on success. The
12
+ # other fields are not meaningful when it is set.
13
+ # - `:is_simple_consumer_group` [Boolean] `true` for a group that assigns partitions
14
+ # manually and uses Kafka only for offset storage
15
+ # - `:state` [Integer] the group state as a `Bindings::RD_KAFKA_CONSUMER_GROUP_STATE_*`
16
+ # code. A group id the cluster does not know is reported as
17
+ # `RD_KAFKA_CONSUMER_GROUP_STATE_DEAD` with no members.
18
+ # - `:state_name` [String] human-readable name of that state (e.g. `"Stable"`, `"Empty"`)
19
+ # - `:type` [Integer] the group protocol type as a
20
+ # `Bindings::RD_KAFKA_CONSUMER_GROUP_TYPE_*` code, one of:
21
+ # `RD_KAFKA_CONSUMER_GROUP_TYPE_CLASSIC`,
22
+ # `RD_KAFKA_CONSUMER_GROUP_TYPE_CONSUMER` (KIP-848),
23
+ # `RD_KAFKA_CONSUMER_GROUP_TYPE_UNKNOWN`
24
+ # - `:type_name` [String] human-readable name of that type (e.g. `"Classic"`, `"Consumer"`)
25
+ # - `:partition_assignor` [String, nil] partition assignor in use (e.g. `"range"`)
26
+ # - `:coordinator` [Hash, nil] the group coordinator broker as
27
+ # `{ id:, host:, port:, rack: }`
28
+ # - `:authorized_operations` [Array<Integer>, nil] ACL operations the client may perform on
29
+ # the group, as `Bindings::RD_KAFKA_ACL_OPERATION_*` codes. `nil` unless requested with
30
+ # `include_authorized_operations: true`.
31
+ # - `:members` [Array<Hash>] current group members, each with:
32
+ # - `:member_id` [String] the member (consumer) id
33
+ # - `:client_id` [String] the member's `client.id`
34
+ # - `:group_instance_id` [String, nil] the static membership instance id, if any
35
+ # - `:host` [String] the host the member connects from
36
+ # - `:assignment` [Hash{String => Array<Integer>}] assigned partitions, by topic
37
+ # - `:target_assignment` [Hash{String => Array<Integer>}, nil] partitions the member is
38
+ # moving to during a rebalance, by topic. Only `consumer` (KIP-848) groups have it;
39
+ # `nil` for `classic` groups.
40
+ # @return [Array<Hash>]
41
+ attr_reader :groups
42
+
43
+ # @param result_ptr [FFI::Pointer] pointer to the `rd_kafka_DescribeConsumerGroups_result_t`
44
+ def initialize(result_ptr)
45
+ @groups = []
46
+
47
+ return if result_ptr.null?
48
+
49
+ count_ptr = FFI::MemoryPointer.new(:size_t)
50
+ array_ptr = Bindings.rd_kafka_DescribeConsumerGroups_result_groups(result_ptr, count_ptr)
51
+
52
+ return if array_ptr.null?
53
+
54
+ array_ptr.read_array_of_pointer(count_ptr.read(:size_t)).each do |group_ptr|
55
+ @groups << build_group(group_ptr)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ # @param group_ptr [FFI::Pointer] pointer to the `rd_kafka_ConsumerGroupDescription_t`
62
+ # @return [Hash]
63
+ def build_group(group_ptr)
64
+ state = Bindings.rd_kafka_ConsumerGroupDescription_state(group_ptr)
65
+ type = Bindings.rd_kafka_ConsumerGroupDescription_type(group_ptr)
66
+
67
+ {
68
+ group_id: read_string(Bindings.rd_kafka_ConsumerGroupDescription_group_id(group_ptr)),
69
+ error: build_error(Bindings.rd_kafka_ConsumerGroupDescription_error(group_ptr)),
70
+ is_simple_consumer_group:
71
+ Bindings.rd_kafka_ConsumerGroupDescription_is_simple_consumer_group(group_ptr) != 0,
72
+ state: state,
73
+ state_name: read_string(Bindings.rd_kafka_consumer_group_state_name(state)),
74
+ type: type,
75
+ type_name: read_string(Bindings.rd_kafka_consumer_group_type_name(type)),
76
+ partition_assignor:
77
+ read_string(Bindings.rd_kafka_ConsumerGroupDescription_partition_assignor(group_ptr)),
78
+ coordinator: extract_node(Bindings.rd_kafka_ConsumerGroupDescription_coordinator(group_ptr)),
79
+ authorized_operations: build_authorized_operations(group_ptr),
80
+ members: Array.new(Bindings.rd_kafka_ConsumerGroupDescription_member_count(group_ptr)) do |index|
81
+ build_member(Bindings.rd_kafka_ConsumerGroupDescription_member(group_ptr, index))
82
+ end
83
+ }
84
+ end
85
+
86
+ # @param member_ptr [FFI::Pointer] pointer to the `rd_kafka_MemberDescription_t`
87
+ # @return [Hash]
88
+ def build_member(member_ptr)
89
+ {
90
+ member_id: read_string(Bindings.rd_kafka_MemberDescription_consumer_id(member_ptr)),
91
+ client_id: read_string(Bindings.rd_kafka_MemberDescription_client_id(member_ptr)),
92
+ group_instance_id: read_string(Bindings.rd_kafka_MemberDescription_group_instance_id(member_ptr)),
93
+ host: read_string(Bindings.rd_kafka_MemberDescription_host(member_ptr)),
94
+ assignment: build_assignment(Bindings.rd_kafka_MemberDescription_assignment(member_ptr)),
95
+ target_assignment: build_target_assignment(member_ptr)
96
+ }
97
+ end
98
+
99
+ # @param assignment_ptr [FFI::Pointer] pointer to the `rd_kafka_MemberAssignment_t`
100
+ # @return [Hash{String => Array<Integer>}]
101
+ def build_assignment(assignment_ptr)
102
+ return {} if assignment_ptr.null?
103
+
104
+ tpl_ptr = Bindings.rd_kafka_MemberAssignment_partitions(assignment_ptr)
105
+
106
+ return {} if tpl_ptr.null?
107
+
108
+ Consumer::TopicPartitionList
109
+ .from_native_tpl(tpl_ptr)
110
+ .to_h
111
+ .transform_values { |partitions| (partitions || []).map(&:partition) }
112
+ end
113
+
114
+ # @param member_ptr [FFI::Pointer] pointer to the `rd_kafka_MemberDescription_t`
115
+ # @return [Hash{String => Array<Integer>}, nil]
116
+ def build_target_assignment(member_ptr)
117
+ assignment_ptr = Bindings.rd_kafka_MemberDescription_target_assignment(member_ptr)
118
+
119
+ assignment_ptr.null? ? nil : build_assignment(assignment_ptr)
120
+ end
121
+
122
+ # @param group_ptr [FFI::Pointer] pointer to the `rd_kafka_ConsumerGroupDescription_t`
123
+ # @return [Array<Integer>, nil]
124
+ def build_authorized_operations(group_ptr)
125
+ count_ptr = FFI::MemoryPointer.new(:size_t)
126
+
127
+ extract_authorized_operations(
128
+ Bindings.rd_kafka_ConsumerGroupDescription_authorized_operations(group_ptr, count_ptr),
129
+ count_ptr
130
+ )
131
+ end
132
+
133
+ # @param error_ptr [FFI::Pointer] pointer to the `rd_kafka_error_t`
134
+ # @return [RdkafkaError, nil]
135
+ def build_error(error_ptr)
136
+ return nil if error_ptr.null?
137
+
138
+ RdkafkaError.new(
139
+ Bindings.rd_kafka_error_code(error_ptr),
140
+ broker_message: read_string(Bindings.rd_kafka_error_string(error_ptr))
141
+ )
142
+ end
143
+
144
+ # @param ptr [FFI::Pointer] pointer to a C string
145
+ # @return [String, nil]
146
+ def read_string(ptr)
147
+ ptr.null? ? nil : ptr.read_string
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Helpers shared by the describe-topics and describe-cluster reports to copy nodes and
6
+ # authorized operations out of event-owned memory.
7
+ # @private
8
+ module DescribeResultParsing
9
+ private
10
+
11
+ # @param node_ptr [FFI::Pointer] pointer to a `rd_kafka_Node_t`
12
+ # @return [Hash, nil] node with `:id`, `:host`, `:port` and `:rack` or nil when NULL
13
+ def extract_node(node_ptr)
14
+ return nil if node_ptr.null?
15
+
16
+ host_ptr = Bindings.rd_kafka_Node_host(node_ptr)
17
+ rack_ptr = Bindings.rd_kafka_Node_rack(node_ptr)
18
+
19
+ {
20
+ id: Bindings.rd_kafka_Node_id(node_ptr),
21
+ host: host_ptr.null? ? nil : host_ptr.read_string,
22
+ port: Bindings.rd_kafka_Node_port(node_ptr),
23
+ rack: rack_ptr.null? ? nil : rack_ptr.read_string
24
+ }
25
+ end
26
+
27
+ # @param array_ptr [FFI::Pointer] pointer to a `rd_kafka_Node_t*` array
28
+ # @param count_ptr [FFI::MemoryPointer] size_t holding the array length
29
+ # @return [Array<Hash>] nodes as returned by {#extract_node}
30
+ def extract_nodes(array_ptr, count_ptr)
31
+ return [] if array_ptr.null?
32
+
33
+ array_ptr.read_array_of_pointer(count_ptr.read(:size_t)).map { |node_ptr| extract_node(node_ptr) }
34
+ end
35
+
36
+ # @param array_ptr [FFI::Pointer] pointer to a `rd_kafka_AclOperation_t` array
37
+ # @param count_ptr [FFI::MemoryPointer] size_t holding the array length
38
+ # @return [Array<Integer>, nil] `Bindings::RD_KAFKA_ACL_OPERATION_*` codes or nil when the
39
+ # authorized operations were not requested
40
+ def extract_authorized_operations(array_ptr, count_ptr)
41
+ return nil if array_ptr.null?
42
+
43
+ array_ptr.read_array_of_int(count_ptr.read(:size_t))
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Handle for describe topics operation
6
+ class DescribeTopicsHandle < AbstractHandle
7
+ layout :pending, :bool,
8
+ :response, :int
9
+
10
+ # @return [String] the name of the operation
11
+ def operation_name
12
+ "describe topics"
13
+ end
14
+
15
+ # @return [DescribeTopicsReport] report prepared by the background event callback, with
16
+ # the described topics.
17
+ def create_result
18
+ prepared_result
19
+ end
20
+
21
+ # Raises an error if the operation failed
22
+ # @raise [RdkafkaError]
23
+ def raise_error
24
+ raise RdkafkaError.new(
25
+ self[:response],
26
+ broker_message: broker_message
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Admin
5
+ # Report for describe topics operation result
6
+ class DescribeTopicsReport
7
+ include DescribeResultParsing
8
+
9
+ # Described topics, one per requested topic name. Each entry is a hash with:
10
+ # - `:name` [String] the topic name
11
+ # - `:topic_id` [String, nil] the topic id (KIP-516), base64 encoded, or `nil` when the
12
+ # topic could not be described
13
+ # - `:is_internal` [Boolean] whether the topic is internal (e.g. `__consumer_offsets`)
14
+ # - `:partitions` [Array<Hash>] each with `:partition` [Integer], `:leader` [Hash, nil] and
15
+ # `:replicas` / `:isr` [Array<Hash>]; nodes have `:id`, `:host`, `:port` and `:rack`
16
+ # - `:authorized_operations` [Array<Integer>, nil] `Bindings::RD_KAFKA_ACL_OPERATION_*`
17
+ # codes, `nil` unless requested with `include_authorized_operations: true`
18
+ # - `:error` [RdkafkaError, nil] per-topic error (e.g. `unknown_topic_or_part`), `nil`
19
+ # on success
20
+ # @return [Array<Hash>]
21
+ attr_reader :topics
22
+
23
+ # @param result_ptr [FFI::Pointer] pointer to the `rd_kafka_DescribeTopics_result_t`
24
+ def initialize(result_ptr)
25
+ @topics = []
26
+
27
+ return if result_ptr.null?
28
+
29
+ count_ptr = FFI::MemoryPointer.new(:size_t)
30
+ array_ptr = Bindings.rd_kafka_DescribeTopics_result_topics(result_ptr, count_ptr)
31
+
32
+ return if array_ptr.null?
33
+
34
+ array_ptr.read_array_of_pointer(count_ptr.read(:size_t)).each do |topic_ptr|
35
+ @topics << extract_topic(topic_ptr)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @param topic_ptr [FFI::Pointer] pointer to a `rd_kafka_TopicDescription_t`
42
+ # @return [Hash]
43
+ def extract_topic(topic_ptr)
44
+ name_ptr = Bindings.rd_kafka_TopicDescription_name(topic_ptr)
45
+ operations_count_ptr = FFI::MemoryPointer.new(:size_t)
46
+
47
+ {
48
+ name: name_ptr.null? ? nil : name_ptr.read_string,
49
+ topic_id: extract_topic_id(topic_ptr),
50
+ is_internal: Bindings.rd_kafka_TopicDescription_is_internal(topic_ptr) != 0,
51
+ partitions: extract_partitions(topic_ptr),
52
+ authorized_operations: extract_authorized_operations(
53
+ Bindings.rd_kafka_TopicDescription_authorized_operations(topic_ptr, operations_count_ptr),
54
+ operations_count_ptr
55
+ ),
56
+ error: extract_error(topic_ptr)
57
+ }
58
+ end
59
+
60
+ # librdkafka reports the zero UUID for a topic it could not describe
61
+ # @param topic_ptr [FFI::Pointer] pointer to a `rd_kafka_TopicDescription_t`
62
+ # @return [String, nil]
63
+ def extract_topic_id(topic_ptr)
64
+ uuid_ptr = Bindings.rd_kafka_TopicDescription_topic_id(topic_ptr)
65
+
66
+ return nil if uuid_ptr.null?
67
+ return nil if Bindings.rd_kafka_Uuid_most_significant_bits(uuid_ptr).zero? &&
68
+ Bindings.rd_kafka_Uuid_least_significant_bits(uuid_ptr).zero?
69
+
70
+ string_ptr = Bindings.rd_kafka_Uuid_base64str(uuid_ptr)
71
+ string_ptr.null? ? nil : string_ptr.read_string
72
+ end
73
+
74
+ # @param topic_ptr [FFI::Pointer] pointer to a `rd_kafka_TopicDescription_t`
75
+ # @return [Array<Hash>]
76
+ def extract_partitions(topic_ptr)
77
+ count_ptr = FFI::MemoryPointer.new(:size_t)
78
+ array_ptr = Bindings.rd_kafka_TopicDescription_partitions(topic_ptr, count_ptr)
79
+
80
+ return [] if array_ptr.null?
81
+
82
+ array_ptr.read_array_of_pointer(count_ptr.read(:size_t)).map do |partition_ptr|
83
+ replicas_count_ptr = FFI::MemoryPointer.new(:size_t)
84
+ isr_count_ptr = FFI::MemoryPointer.new(:size_t)
85
+
86
+ {
87
+ partition: Bindings.rd_kafka_TopicPartitionInfo_partition(partition_ptr),
88
+ leader: extract_node(Bindings.rd_kafka_TopicPartitionInfo_leader(partition_ptr)),
89
+ replicas: extract_nodes(
90
+ Bindings.rd_kafka_TopicPartitionInfo_replicas(partition_ptr, replicas_count_ptr),
91
+ replicas_count_ptr
92
+ ),
93
+ isr: extract_nodes(
94
+ Bindings.rd_kafka_TopicPartitionInfo_isr(partition_ptr, isr_count_ptr),
95
+ isr_count_ptr
96
+ )
97
+ }
98
+ end
99
+ end
100
+
101
+ # The error is owned by the result, so it is copied and not destroyed here.
102
+ # @param topic_ptr [FFI::Pointer] pointer to a `rd_kafka_TopicDescription_t`
103
+ # @return [RdkafkaError, nil]
104
+ def extract_error(topic_ptr)
105
+ error_ptr = Bindings.rd_kafka_TopicDescription_error(topic_ptr)
106
+
107
+ return nil if error_ptr.null?
108
+
109
+ code = Bindings.rd_kafka_error_code(error_ptr)
110
+
111
+ return nil if code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
112
+
113
+ string_ptr = Bindings.rd_kafka_error_string(error_ptr)
114
+ RdkafkaError.new(code, broker_message: string_ptr.null? ? nil : string_ptr.read_string)
115
+ end
116
+ end
117
+ end
118
+ end