karafka-rdkafka 0.29.0-aarch64-linux-gnu → 0.30.1-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.
@@ -201,7 +201,7 @@ module Rdkafka
201
201
  RD_KAFKA_ALTER_CONFIG_OP_TYPE_SUBTRACT = 3
202
202
 
203
203
  # List Offsets
204
- RD_KAFKA_ADMIN_OP_LISTOFFSETS = 20
204
+ RD_KAFKA_ADMIN_OP_LISTOFFSETS = 21
205
205
  RD_KAFKA_EVENT_LISTOFFSETS_RESULT = 0x400000
206
206
 
207
207
  # rd_kafka_IsolationLevel_t
@@ -412,9 +412,17 @@ module Rdkafka
412
412
  # ```
413
413
  OAuthbearerTokenRefreshCallback = FFI::Function.new(
414
414
  :void, [:pointer, :string, :pointer]
415
- ) do |client_ptr, config, _opaque|
415
+ ) do |client_ptr, config, opaque_ptr|
416
416
  if Rdkafka::Config.oauthbearer_token_refresh_callback && !client_ptr.null?
417
- Rdkafka::Config.oauthbearer_token_refresh_callback.call(config, Rdkafka::Bindings.rd_kafka_name(client_ptr))
417
+ client_name = Rdkafka::Bindings.rd_kafka_name(client_ptr)
418
+
419
+ # Share consumers have no way to query their librdkafka client name (the share handle
420
+ # exposes no name accessor), so hand it over here - where librdkafka provides it -
421
+ # letting applications correlate this callback's client name with a ShareConsumer via
422
+ # ShareConsumer#name.
423
+ Rdkafka::Config.opaques[opaque_ptr.to_i]&.capture_client_name(client_name)
424
+
425
+ Rdkafka::Config.oauthbearer_token_refresh_callback.call(config, client_name)
418
426
  end
419
427
  end
420
428
 
@@ -503,6 +511,68 @@ module Rdkafka
503
511
  end
504
512
  end
505
513
 
514
+ # Share consumer (KIP-932, preview)
515
+ #
516
+ # The share consumer is a distinct librdkafka handle type (rd_kafka_share_t) created with
517
+ # rd_kafka_share_consumer_new instead of rd_kafka_new. It is single-threaded by design and
518
+ # librdkafka itself rejects concurrent access with RD_KAFKA_RESP_ERR__CONFLICT.
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
+
532
+ # Acknowledge types for share consumer records
533
+ RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_ACCEPT = 1
534
+ RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_RELEASE = 2
535
+ RD_KAFKA_SHARE_ACKNOWLEDGE_TYPE_REJECT = 3
536
+
537
+ attach_function :rd_kafka_share_consumer_new, [:pointer, :pointer, :size_t], :pointer
538
+ attach_function :rd_kafka_share_consumer_close, [:pointer], :pointer, blocking: true
539
+ attach_function :rd_kafka_share_consumer_closed, [:pointer], :int
540
+ attach_function :rd_kafka_share_destroy, [:pointer], :pointer, blocking: true
541
+ attach_function :rd_kafka_share_set_log_queue, [:pointer, :pointer], :pointer
542
+
543
+ attach_function :rd_kafka_share_subscribe, [:pointer, :pointer], :int, blocking: true
544
+ attach_function :rd_kafka_share_unsubscribe, [:pointer], :int, blocking: true
545
+ attach_function :rd_kafka_share_subscription, [:pointer, :pointer], :int, blocking: true
546
+
547
+ attach_function :rd_kafka_share_poll, [:pointer, :int, :pointer], :pointer, blocking: true
548
+ attach_function :rd_kafka_messages_count, [:pointer], :size_t
549
+ attach_function :rd_kafka_messages_get, [:pointer, :size_t], :pointer
550
+ attach_function :rd_kafka_messages_destroy, [:pointer], :void
551
+ attach_function :rd_kafka_message_delivery_count, [:pointer], :int16
552
+
553
+ # The by-coordinates acknowledge variant is the only one we bind: Ruby messages are copies
554
+ # that do not retain the native rd_kafka_message_t pointer, so the message-pointer variants
555
+ # (rd_kafka_share_acknowledge / rd_kafka_share_acknowledge_type) cannot be used safely here.
556
+ attach_function :rd_kafka_share_acknowledge_offset, [:pointer, :string, :int32, :int64, :int], :int
557
+
558
+ attach_function :rd_kafka_share_commit_sync, [:pointer, :int, :pointer], :pointer, blocking: true
559
+ attach_function :rd_kafka_share_commit_async, [:pointer], :pointer
560
+
561
+ callback :share_acknowledgement_commit_cb, [:pointer, :pointer, :int, :pointer], :void
562
+ attach_function :rd_kafka_share_set_acknowledgement_commit_cb, [:pointer, :share_acknowledgement_commit_cb, :pointer], :pointer
563
+
564
+ # Accessors for the rd_kafka_share_partition_offsets_list_t handed to the acknowledgement
565
+ # commit callback. The list is owned by librdkafka for the duration of the callback: read it
566
+ # inside the callback only, never destroy or retain it.
567
+ attach_function :rd_kafka_share_partition_offsets_list_count, [:pointer], :size_t
568
+ attach_function :rd_kafka_share_partition_offsets_list_get, [:pointer, :size_t], :pointer
569
+ attach_function :rd_kafka_share_partition_offsets_partition, [:pointer], :pointer
570
+ attach_function :rd_kafka_share_partition_offsets_offsets, [:pointer], :pointer
571
+ attach_function :rd_kafka_share_partition_offsets_offsets_cnt, [:pointer], :size_t
572
+
573
+ attach_function :rd_kafka_share_oauthbearer_set_token, [:pointer, :string, :int64, :string, :pointer, :size_t, :pointer, :size_t], :int
574
+ attach_function :rd_kafka_share_oauthbearer_set_token_failure, [:pointer, :string], :int
575
+
506
576
  # Stats
507
577
 
508
578
  attach_function :rd_kafka_query_watermark_offsets, [:pointer, :string, :int, :pointer, :pointer, :int], :int, blocking: true
@@ -559,9 +629,31 @@ module Rdkafka
559
629
  raise Rdkafka::Config::ConfigError.new("Unknown partitioner: #{partitioner}")
560
630
  end
561
631
 
562
- public_send(method_name, topic_ptr, str_ptr, str.size, partition_count, nil, nil)
632
+ public_send(method_name, topic_ptr, str_ptr, partition_key_length(str), partition_count, nil, nil)
633
+ end
634
+
635
+ # Partition key length as the character count (legacy default).
636
+ #
637
+ # @param str [String] the partition key string
638
+ # @return [Integer]
639
+ def self.partition_key_size(str)
640
+ str.size
563
641
  end
564
642
 
643
+ # Partition key length as the byte count, which is what librdkafka hashes (UTF-8 bytes copied
644
+ # into the key pointer) and what other Kafka clients use.
645
+ #
646
+ # @param str [String] the partition key string
647
+ # @return [Integer]
648
+ def self.partition_key_bytesize(str)
649
+ str.bytesize
650
+ end
651
+
652
+ # Length of the partition key passed to librdkafka. Aliased to one of the methods above by
653
+ # `Rdkafka::Config.partitioner_key_uses_bytesize=`, so the per-message path does not check the
654
+ # setting on every call.
655
+ singleton_class.alias_method :partition_key_length, :partition_key_size
656
+
565
657
  # Create Topics
566
658
  RD_KAFKA_ADMIN_OP_CREATETOPICS = 1 # rd_kafka_admin_op_t
567
659
  RD_KAFKA_EVENT_CREATETOPICS_RESULT = 100 # rd_kafka_event_type_t
@@ -603,6 +695,26 @@ module Rdkafka
603
695
  attach_function :rd_kafka_event_DeleteGroups_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_DeleteGroups_result_t*
604
696
  attach_function :rd_kafka_DeleteGroups_result_groups, [:pointer, :pointer], :pointer, blocking: true # rd_kafka_DeleteGroups_result_t*, size_t* => rd_kafka_group_result_t**
605
697
 
698
+ # Alter Consumer Group Offsets
699
+ RD_KAFKA_ADMIN_OP_ALTERCONSUMERGROUPOFFSETS = 15 # rd_kafka_admin_op_t
700
+ RD_KAFKA_EVENT_ALTERCONSUMERGROUPOFFSETS_RESULT = 0x10000 # rd_kafka_event_type_t
701
+
702
+ attach_function :rd_kafka_AlterConsumerGroupOffsets, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
703
+ attach_function :rd_kafka_AlterConsumerGroupOffsets_new, [:pointer, :pointer], :pointer, blocking: true # const char*, const rd_kafka_topic_partition_list_t* => rd_kafka_AlterConsumerGroupOffsets_t*
704
+ attach_function :rd_kafka_AlterConsumerGroupOffsets_destroy, [:pointer], :void, blocking: true
705
+ attach_function :rd_kafka_event_AlterConsumerGroupOffsets_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_AlterConsumerGroupOffsets_result_t*
706
+ attach_function :rd_kafka_AlterConsumerGroupOffsets_result_groups, [:pointer, :pointer], :pointer, blocking: true # rd_kafka_AlterConsumerGroupOffsets_result_t*, size_t* => rd_kafka_group_result_t**
707
+
708
+ # Delete Consumer Group Offsets
709
+ RD_KAFKA_ADMIN_OP_DELETECONSUMERGROUPOFFSETS = 8 # rd_kafka_admin_op_t
710
+ RD_KAFKA_EVENT_DELETECONSUMERGROUPOFFSETS_RESULT = 107 # rd_kafka_event_type_t
711
+
712
+ attach_function :rd_kafka_DeleteConsumerGroupOffsets, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
713
+ attach_function :rd_kafka_DeleteConsumerGroupOffsets_new, [:pointer, :pointer], :pointer, blocking: true # const char*, const rd_kafka_topic_partition_list_t* => rd_kafka_DeleteConsumerGroupOffsets_t*
714
+ attach_function :rd_kafka_DeleteConsumerGroupOffsets_destroy, [:pointer], :void, blocking: true
715
+ attach_function :rd_kafka_event_DeleteConsumerGroupOffsets_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_DeleteConsumerGroupOffsets_result_t*
716
+ attach_function :rd_kafka_DeleteConsumerGroupOffsets_result_groups, [:pointer, :pointer], :pointer, blocking: true # rd_kafka_DeleteConsumerGroupOffsets_result_t*, size_t* => rd_kafka_group_result_t**
717
+
606
718
  # Delete Records
607
719
  RD_KAFKA_ADMIN_OP_DELETERECORDS = 6 # rd_kafka_admin_op_t
608
720
  RD_KAFKA_EVENT_DELETERECORDS_RESULT = 105 # rd_kafka_event_type_t
@@ -752,5 +864,6 @@ module Rdkafka
752
864
 
753
865
  attach_function :rd_kafka_group_result_error, [:pointer], NativeError.by_ref # rd_kafka_group_result_t* => rd_kafka_error_t*
754
866
  attach_function :rd_kafka_group_result_name, [:pointer], :pointer
867
+ attach_function :rd_kafka_group_result_partitions, [:pointer], :pointer # rd_kafka_group_result_t* => rd_kafka_topic_partition_list_t*
755
868
  end
756
869
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Callbacks
5
+ # Handles `RD_KAFKA_EVENT_ALTERCONSUMERGROUPOFFSETS_RESULT` events
6
+ # @private
7
+ class AlterConsumerGroupOffsetsHandler < BaseHandler
8
+ class << self
9
+ # Resolves the alter consumer group offsets handle from its result event
10
+ # @param event_ptr [FFI::Pointer] pointer to the event
11
+ # @return [void]
12
+ def call(event_ptr)
13
+ result = Rdkafka::Bindings.rd_kafka_event_AlterConsumerGroupOffsets_result(event_ptr)
14
+
15
+ pointer_to_size_t = FFI::MemoryPointer.new(:size_t)
16
+ group_result_array = Rdkafka::Bindings.rd_kafka_AlterConsumerGroupOffsets_result_groups(result, pointer_to_size_t)
17
+ group_results = GroupResult.create_group_results_from_array(pointer_to_size_t.read_int, group_result_array)
18
+ handle_ptr = Rdkafka::Bindings.rd_kafka_event_opaque(event_ptr)
19
+
20
+ if (handle = Rdkafka::Admin::AlterConsumerGroupOffsetsHandle.remove(handle_ptr.address))
21
+ unless resolve_operation_error(event_ptr, handle)
22
+ group_result = group_results[0]
23
+
24
+ # librdkafka only supports one group per invocation, so a missing group result
25
+ # means the broker returned nothing to act on rather than a partial success.
26
+ if group_result.nil?
27
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
28
+ handle.result = Rdkafka::Admin::AlterConsumerGroupOffsetsReport.new(
29
+ FFI::Pointer::NULL,
30
+ FFI::Pointer::NULL
31
+ )
32
+ else
33
+ report = Rdkafka::Admin::AlterConsumerGroupOffsetsReport.new(
34
+ group_result.error_string,
35
+ group_result.result_name,
36
+ Rdkafka::Bindings.rd_kafka_group_result_partitions(group_result_array.read_pointer)
37
+ )
38
+ handle.result = report
39
+
40
+ # Kafka reports a rejected offset change per partition, leaving the group level
41
+ # error unset - a group with live members comes back with the group error NULL
42
+ # and `unknown_member_id` on each partition. Resolving on the group error alone
43
+ # would hand back a report that looks like success, so any failed partition has
44
+ # to fail the handle too.
45
+ failed = report.partitions.find { |partition| partition[:error] }
46
+
47
+ if !group_result.result_error.zero?
48
+ handle[:response] = group_result.result_error
49
+ handle.broker_message = report.error_string
50
+ elsif failed
51
+ handle[:response] = failed[:error].rdkafka_response
52
+ handle.broker_message = "Failed for partition #{failed[:partition]} of '#{failed[:topic]}'"
53
+ else
54
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
55
+ end
56
+ end
57
+
58
+ handle.unlock
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Callbacks
5
+ # Handles `RD_KAFKA_EVENT_DELETECONSUMERGROUPOFFSETS_RESULT` events
6
+ # @private
7
+ class DeleteConsumerGroupOffsetsHandler < BaseHandler
8
+ class << self
9
+ # Resolves the delete consumer group offsets handle from its result event
10
+ # @param event_ptr [FFI::Pointer] pointer to the event
11
+ # @return [void]
12
+ def call(event_ptr)
13
+ result = Rdkafka::Bindings.rd_kafka_event_DeleteConsumerGroupOffsets_result(event_ptr)
14
+
15
+ pointer_to_size_t = FFI::MemoryPointer.new(:size_t)
16
+ group_result_array = Rdkafka::Bindings.rd_kafka_DeleteConsumerGroupOffsets_result_groups(result, pointer_to_size_t)
17
+ group_results = GroupResult.create_group_results_from_array(pointer_to_size_t.read_int, group_result_array)
18
+ handle_ptr = Rdkafka::Bindings.rd_kafka_event_opaque(event_ptr)
19
+
20
+ if (handle = Rdkafka::Admin::DeleteConsumerGroupOffsetsHandle.remove(handle_ptr.address))
21
+ unless resolve_operation_error(event_ptr, handle)
22
+ group_result = group_results[0]
23
+
24
+ # librdkafka only supports one group per invocation, so a missing group result
25
+ # means the broker returned nothing to act on rather than a partial success.
26
+ if group_result.nil?
27
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
28
+ handle.result = Rdkafka::Admin::DeleteConsumerGroupOffsetsReport.new(
29
+ FFI::Pointer::NULL,
30
+ FFI::Pointer::NULL
31
+ )
32
+ else
33
+ report = Rdkafka::Admin::DeleteConsumerGroupOffsetsReport.new(
34
+ group_result.error_string,
35
+ group_result.result_name,
36
+ Rdkafka::Bindings.rd_kafka_group_result_partitions(group_result_array.read_pointer)
37
+ )
38
+ handle.result = report
39
+
40
+ # Kafka reports a rejected offset change per partition, leaving the group level
41
+ # error unset - a group with live members comes back with the group error NULL
42
+ # and `unknown_member_id` on each partition. Resolving on the group error alone
43
+ # would hand back a report that looks like success, so any failed partition has
44
+ # to fail the handle too.
45
+ failed = report.partitions.find { |partition| partition[:error] }
46
+
47
+ if !group_result.result_error.zero?
48
+ handle[:response] = group_result.result_error
49
+ handle.broker_message = report.error_string
50
+ elsif failed
51
+ handle[:response] = failed[:error].rdkafka_response
52
+ handle.broker_message = "Failed for partition #{failed[:partition]} of '#{failed[:topic]}'"
53
+ else
54
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
55
+ end
56
+ end
57
+
58
+ handle.unlock
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -21,10 +21,9 @@ module Rdkafka
21
21
  # @param array_pointer [FFI::Pointer] pointer to the results array
22
22
  # @return [Array<TopicResult>] array of topic results
23
23
  def self.create_topic_results_from_array(count, array_pointer)
24
- (1..count).map do |index|
25
- result_pointer = (array_pointer + (index - 1)).read_pointer
26
- new(result_pointer)
27
- end
24
+ return [] if count.zero?
25
+
26
+ array_pointer.read_array_of_pointer(count).map { |result_pointer| new(result_pointer) }
28
27
  end
29
28
  end
30
29
 
@@ -53,10 +52,9 @@ module Rdkafka
53
52
  # @param array_pointer [FFI::Pointer] pointer to the results array
54
53
  # @return [Array<GroupResult>] array of group results
55
54
  def self.create_group_results_from_array(count, array_pointer)
56
- (1..count).map do |index|
57
- result_pointer = (array_pointer + (index - 1)).read_pointer
58
- new(result_pointer)
59
- end
55
+ return [] if count.zero?
56
+
57
+ array_pointer.read_array_of_pointer(count).map { |result_pointer| new(result_pointer) }
60
58
  end
61
59
  end
62
60
 
@@ -77,10 +75,9 @@ module Rdkafka
77
75
  # @param array_pointer [FFI::Pointer] pointer to the results array
78
76
  # @return [Array<CreateAclResult>] array of ACL results
79
77
  def self.create_acl_results_from_array(count, array_pointer)
80
- (1..count).map do |index|
81
- result_pointer = (array_pointer + (index - 1)).read_pointer
82
- new(result_pointer)
83
- end
78
+ return [] if count.zero?
79
+
80
+ array_pointer.read_array_of_pointer(count).map { |result_pointer| new(result_pointer) }
84
81
  end
85
82
  end
86
83
 
@@ -108,10 +105,9 @@ module Rdkafka
108
105
  # @param array_pointer [FFI::Pointer] pointer to the results array
109
106
  # @return [Array<DeleteAclResult>] array of delete ACL results
110
107
  def self.delete_acl_results_from_array(count, array_pointer)
111
- (1..count).map do |index|
112
- result_pointer = (array_pointer + (index - 1)).read_pointer
113
- new(result_pointer)
114
- end
108
+ return [] if count.zero?
109
+
110
+ array_pointer.read_array_of_pointer(count).map { |result_pointer| new(result_pointer) }
115
111
  end
116
112
  end
117
113
 
@@ -219,6 +215,8 @@ module Rdkafka
219
215
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETETOPICS_RESULT then DeleteTopicHandler
220
216
  when Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_CREATEPARTITIONS_RESULT then CreatePartitionsHandler
221
217
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETEGROUPS_RESULT then DeleteGroupsHandler
218
+ when Rdkafka::Bindings::RD_KAFKA_EVENT_ALTERCONSUMERGROUPOFFSETS_RESULT then AlterConsumerGroupOffsetsHandler
219
+ when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETECONSUMERGROUPOFFSETS_RESULT then DeleteConsumerGroupOffsetsHandler
222
220
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETERECORDS_RESULT then DeleteRecordsHandler
223
221
  when Rdkafka::Bindings::RD_KAFKA_EVENT_CREATEACLS_RESULT then CreateAclHandler
224
222
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETEACLS_RESULT then DeleteAclHandler
@@ -22,6 +22,12 @@ module Rdkafka
22
22
  @@log_mutex = Mutex.new
23
23
  # @!visibility private
24
24
  @@oauthbearer_token_refresh_callback = nil
25
+ # @!visibility private
26
+ # Whether the partition key length passed to the partitioner is the byte length (`bytesize`)
27
+ # rather than the character length (`size`). `false` (character length) is the legacy default
28
+ # to avoid changing partition assignment for existing multibyte/binary keys; set to `true` to
29
+ # opt into the correct, other-client-consistent byte-length hashing.
30
+ @@partitioner_key_uses_bytesize = false
25
31
 
26
32
  # Returns the current logger, by default this is a logger to stdout.
27
33
  #
@@ -122,6 +128,34 @@ module Rdkafka
122
128
  @@oauthbearer_token_refresh_callback
123
129
  end
124
130
 
131
+ # Whether the partitioner hashes the partition key by its byte length instead of its character
132
+ # length. Defaults to `false` (character length) to preserve existing partition assignment for
133
+ # multibyte/binary keys. Passing the byte length is the correct, other-client-consistent
134
+ # behavior; enable it (and be aware it can move such keys to a different partition) with
135
+ # `Rdkafka::Config.partitioner_key_uses_bytesize = true`. This default is expected to change in
136
+ # a future major version.
137
+ #
138
+ # @return [Boolean]
139
+ def self.partitioner_key_uses_bytesize
140
+ @@partitioner_key_uses_bytesize
141
+ end
142
+
143
+ # Sets whether the partitioner hashes the partition key by its byte length rather than its
144
+ # character length. The key length method is resolved here once rather than on every produced
145
+ # message, and each call re-resolves it, so changing the setting at runtime takes effect
146
+ # immediately.
147
+ #
148
+ # @param value [Boolean] true to hash by byte length, false (default) for character length
149
+ # @return [void]
150
+ def self.partitioner_key_uses_bytesize=(value)
151
+ @@partitioner_key_uses_bytesize = value
152
+
153
+ Rdkafka::Bindings.singleton_class.alias_method(
154
+ :partition_key_length,
155
+ value ? :partition_key_bytesize : :partition_key_size
156
+ )
157
+ end
158
+
125
159
  # @private
126
160
  def self.opaques
127
161
  @@opaques
@@ -221,6 +255,73 @@ module Rdkafka
221
255
  end
222
256
  end
223
257
 
258
+ # Creates a KIP-932 share group consumer with this configuration.
259
+ #
260
+ # `:"group.id"` is required. The acknowledgement mode is selected with the optional
261
+ # `:"share.acknowledgement.mode"` property (`implicit` by default, or `explicit`). librdkafka
262
+ # rejects a number of regular-consumer properties for share consumers (for example
263
+ # `enable.auto.commit`, `auto.offset.reset` or `partition.assignment.strategy`) with a
264
+ # {ClientCreationError}.
265
+ #
266
+ # @return [ShareConsumer] The created share consumer
267
+ #
268
+ # @raise [ConfigError] When the configuration contains invalid options or a rebalance
269
+ # listener is set (share group assignment is broker-driven and has no rebalances)
270
+ # @raise [ClientCreationError] When the native client cannot be created
271
+ #
272
+ # @note The share consumer is a preview librdkafka feature and requires a broker with share
273
+ # groups enabled (Apache Kafka 4.2.0+).
274
+ def share_consumer
275
+ if @consumer_rebalance_listener
276
+ raise ConfigError.new("`consumer_rebalance_listener` is not supported for share consumers")
277
+ end
278
+
279
+ opaque = Opaque.new
280
+ config = native_config(opaque)
281
+
282
+ handle = create_native_handle(config) do |error_buffer|
283
+ Rdkafka::Bindings.rd_kafka_share_consumer_new(config, error_buffer, 256)
284
+ end
285
+
286
+ share_consumer = nil
287
+
288
+ # Share consumers cannot go through `build_native_client`: they own the native handle
289
+ # directly rather than a `NativeKafka` wrapper (single-threaded by design, no polling
290
+ # thread). They still need both of the things that factory provides - registration, so the
291
+ # `at_exit` hook closes them before Ruby's shutdown finalization rather than letting
292
+ # librdkafka be `dlclose`d with its threads still running, and a teardown that destroys the
293
+ # handle when construction fails part-way - so both are done here.
294
+ begin
295
+ # Forward the log queue (enabled via REQUIRED_CONFIG) to the main queue, which
296
+ # rd_kafka_share_poll drains. There is no background polling thread for share consumers:
297
+ # log, statistics and error callbacks are all serviced from within ShareConsumer#poll.
298
+ log_queue_error = Rdkafka::Bindings.rd_kafka_share_set_log_queue(handle, FFI::Pointer::NULL)
299
+ Rdkafka::Bindings.rd_kafka_error_destroy(log_queue_error) unless log_queue_error.null?
300
+
301
+ share_consumer = Rdkafka::ShareConsumer.new(handle, opaque: opaque)
302
+ opaque.share_consumer = share_consumer
303
+
304
+ Rdkafka::Clients.register(share_consumer)
305
+ rescue Exception
306
+ begin
307
+ if share_consumer
308
+ share_consumer.close
309
+ else
310
+ destroy_error = Rdkafka::Bindings.rd_kafka_share_destroy(handle)
311
+ Rdkafka::Bindings.rd_kafka_error_destroy(destroy_error) unless destroy_error.null?
312
+ end
313
+ rescue Exception => err
314
+ # Never let a teardown failure replace the error that caused it: that error is what the
315
+ # caller needs to see, and it is re-raised below regardless.
316
+ Rdkafka::Config.logger.error("Unhandled exception: #{err.class} - #{err.message}")
317
+ end
318
+
319
+ raise
320
+ end
321
+
322
+ share_consumer
323
+ end
324
+
224
325
  # Create a producer with this configuration.
225
326
  #
226
327
  # @param native_kafka_auto_start [Boolean] should the native kafka operations be started
@@ -442,28 +543,44 @@ module Rdkafka
442
543
  raise
443
544
  end
444
545
 
445
- # Creates a native Kafka handle
546
+ # Creates a native client handle via the given creation block, enforcing the conf ownership
547
+ # rule shared by all librdkafka constructors: on success librdkafka takes ownership of the
548
+ # conf and frees it itself; on failure the application keeps ownership (see rdkafka.c), so
549
+ # it must be destroyed here to avoid leaking the conf on every failed client creation.
550
+ #
446
551
  # @param config [FFI::Pointer] pointer to the native config
447
- # @param type [Symbol] type of client (:rd_kafka_producer or :rd_kafka_consumer)
448
- # @return [FFI::Pointer] pointer to the native Kafka handle
552
+ # @yield [error_buffer] performs the native creation call
553
+ # @yieldparam error_buffer [FFI::MemoryPointer] 256-byte buffer for the error string
554
+ # @return [FFI::Pointer] non-NULL native handle
555
+ # @raise [ClientCreationError] When the native client cannot be created
449
556
  # @private
450
- def native_kafka(config, type)
451
- error_buffer = FFI::MemoryPointer.from_string(" " * 256)
452
- handle = Rdkafka::Bindings.rd_kafka_new(
453
- type,
454
- config,
455
- error_buffer,
456
- 256
457
- )
557
+ def create_native_handle(config)
558
+ error_buffer = FFI::MemoryPointer.new(:char, 256)
559
+ handle = yield(error_buffer)
458
560
 
459
561
  if handle.null?
460
- # On success rd_kafka_new takes ownership of the conf and frees it itself; on failure
461
- # librdkafka keeps application ownership (see rdkafka.c), so we must destroy it here to
462
- # avoid leaking the conf on every failed client creation.
463
562
  Rdkafka::Bindings.rd_kafka_conf_destroy(config)
464
563
  raise ClientCreationError.new(error_buffer.read_string)
465
564
  end
466
565
 
566
+ handle
567
+ end
568
+
569
+ # Creates a native Kafka handle
570
+ # @param config [FFI::Pointer] pointer to the native config
571
+ # @param type [Symbol] type of client (:rd_kafka_producer or :rd_kafka_consumer)
572
+ # @return [FFI::Pointer] pointer to the native Kafka handle
573
+ # @private
574
+ def native_kafka(config, type)
575
+ handle = create_native_handle(config) do |error_buffer|
576
+ Rdkafka::Bindings.rd_kafka_new(
577
+ type,
578
+ config,
579
+ error_buffer,
580
+ 256
581
+ )
582
+ end
583
+
467
584
  # Redirect log to handle's queue
468
585
  main_queue = Rdkafka::Bindings.rd_kafka_queue_get_main(handle)
469
586
  Rdkafka::Bindings.rd_kafka_set_log_queue(handle, main_queue)
@@ -481,6 +598,18 @@ module Rdkafka
481
598
  class Opaque
482
599
  attr_accessor :producer
483
600
  attr_accessor :consumer_rebalance_listener
601
+ # Backreference set for share consumers so conf-level callbacks (which receive the opaque)
602
+ # can hand instance-scoped data - currently the librdkafka client name - back to the
603
+ # ShareConsumer, which has no native accessor for it
604
+ attr_accessor :share_consumer
605
+
606
+ # Forwards the librdkafka client name to clients that cannot query it themselves. Regular
607
+ # clients resolve their name lazily via rd_kafka_name on their own handle, so only the
608
+ # share consumer (whose handle exposes no name accessor) consumes this.
609
+ # @param name [String] librdkafka client name (e.g. "rdkafka#consumer-1")
610
+ def capture_client_name(name)
611
+ share_consumer&.name = name
612
+ end
484
613
 
485
614
  # Invokes the delivery callback on the producer if one is set
486
615
  # @param delivery_report [Rdkafka::Producer::DeliveryReport] the delivery report
@@ -47,6 +47,14 @@ module Rdkafka
47
47
  # @see Consumer#events_poll
48
48
  CONSUMER_EVENTS_POLL_TIMEOUT_MS = 0
49
49
 
50
+ # Share consumer timeout for poll operations (used in each iteration)
51
+ # @see ShareConsumer#poll
52
+ SHARE_CONSUMER_POLL_TIMEOUT_MS = 250
53
+
54
+ # Share consumer timeout for synchronous acknowledgement commits
55
+ # @see ShareConsumer#commit_sync
56
+ SHARE_CONSUMER_COMMIT_SYNC_TIMEOUT_MS = 5_000
57
+
50
58
  # Producer timeout for flush operations
51
59
  # @see Producer#flush
52
60
  PRODUCER_FLUSH_TIMEOUT_MS = 5_000
data/lib/rdkafka/error.rb CHANGED
@@ -40,9 +40,20 @@ module Rdkafka
40
40
  def build_from_c(response_ptr, message_prefix = nil, broker_message: nil, instance_name: nil)
41
41
  code = Rdkafka::Bindings.rd_kafka_error_code(response_ptr)
42
42
 
43
- return false if code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
43
+ if code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
44
+ # A no-op error object still has to be freed (librdkafka can construct code-0 errors)
45
+ Rdkafka::Bindings.rd_kafka_error_destroy(response_ptr) unless response_ptr.null?
44
46
 
45
- message = broker_message || Rdkafka::Bindings.rd_kafka_err2str(code)
47
+ return false
48
+ end
49
+
50
+ # Prefer the human readable error string attached to this error object; librdkafka falls
51
+ # back to the generic per-code description internally when none was attached
52
+ message = broker_message
53
+ if message.nil?
54
+ message_ptr = Rdkafka::Bindings.rd_kafka_error_string(response_ptr)
55
+ message = message_ptr.null? ? Rdkafka::Bindings.rd_kafka_err2str(code) : message_ptr.read_string
56
+ end
46
57
  fatal = !Rdkafka::Bindings.rd_kafka_error_is_fatal(response_ptr).zero?
47
58
  retryable = !Rdkafka::Bindings.rd_kafka_error_is_retriable(response_ptr).zero?
48
59
  abortable = !Rdkafka::Bindings.rd_kafka_error_txn_requires_abort(response_ptr).zero?
@@ -18,12 +18,10 @@ module Rdkafka
18
18
  extensions_ptr, extensions_str_ptrs = map_extensions(extensions)
19
19
 
20
20
  begin
21
- response = @native_kafka.with_inner do |inner|
22
- Rdkafka::Bindings.rd_kafka_oauthbearer_set_token(
23
- inner, token, lifetime_ms, principal_name,
24
- extensions_ptr, extension_size(extensions), error_buffer, 256
25
- )
26
- end
21
+ response = oauthbearer_native_set_token(
22
+ token, lifetime_ms, principal_name,
23
+ extensions_ptr, extension_size(extensions), error_buffer
24
+ )
27
25
  ensure
28
26
  extensions_str_ptrs&.each { |ptr| ptr.free }
29
27
  extensions_ptr&.free
@@ -50,6 +48,26 @@ module Rdkafka
50
48
 
51
49
  private
52
50
 
51
+ # Performs the native token set call. The default targets the regular client handle;
52
+ # clients with a different handle type (e.g. the share consumer) override just this hook
53
+ # while reusing the buffer/extension plumbing above.
54
+ #
55
+ # @param token [String] the token value
56
+ # @param lifetime_ms [Integer] token expiry, in milliseconds since the epoch
57
+ # @param principal_name [String] the Kafka principal name associated with the token
58
+ # @param extensions_ptr [FFI::Pointer, nil] `const char **` built by {#map_extensions}
59
+ # @param extensions_size [Integer] number of key-value pairs pointed to by `extensions_ptr`
60
+ # @param error_buffer [FFI::MemoryPointer] 256-byte buffer for the error string
61
+ # @return [Integer] 0 on success
62
+ def oauthbearer_native_set_token(token, lifetime_ms, principal_name, extensions_ptr, extensions_size, error_buffer)
63
+ @native_kafka.with_inner do |inner|
64
+ Rdkafka::Bindings.rd_kafka_oauthbearer_set_token(
65
+ inner, token, lifetime_ms, principal_name,
66
+ extensions_ptr, extensions_size, error_buffer, 256
67
+ )
68
+ end
69
+ end
70
+
53
71
  # Convert extensions hash to FFI::MemoryPointer (`const char **`).
54
72
  #
55
73
  # @param extensions [Hash, nil] extension key-value pairs