rdkafka 0.29.0-aarch64-linux-gnu → 0.29.2-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.
@@ -58,6 +58,7 @@ module Rdkafka
58
58
  end
59
59
  end
60
60
 
61
+ RD_KAFKA_RESP_ERR__TIMED_OUT = -185
61
62
  RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS = -175
62
63
  RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS = -174
63
64
  RD_KAFKA_RESP_ERR__STATE = -172
@@ -81,7 +82,7 @@ module Rdkafka
81
82
 
82
83
  # This function comes from our patch on top of librdkafka. It allows os to load all the
83
84
  # librdkafka components without initializing the client
84
- # @see https://github.com/confluentinc/librdkafka/issues/4590
85
+ # See: https://github.com/confluentinc/librdkafka/issues/4590
85
86
  attach_function :rd_kafka_global_init, [], :void
86
87
 
87
88
  # Polling
@@ -160,6 +161,7 @@ module Rdkafka
160
161
 
161
162
  attach_function :rd_kafka_DescribeConfigs, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
162
163
  attach_function :rd_kafka_ConfigResource_new, [:int32, :pointer], :pointer
164
+ attach_function :rd_kafka_ConfigResource_destroy, [:pointer], :void
163
165
  attach_function :rd_kafka_ConfigResource_destroy_array, [:pointer, :int32], :void
164
166
  attach_function :rd_kafka_event_DescribeConfigs_result, [:pointer], :pointer
165
167
  attach_function :rd_kafka_DescribeConfigs_result_resources, [:pointer, :pointer], :pointer
@@ -235,6 +237,8 @@ module Rdkafka
235
237
 
236
238
  attach_function :rd_kafka_conf_new, [], :pointer
237
239
  attach_function :rd_kafka_conf_set, [:pointer, :string, :string, :pointer, :int], :kafka_config_response
240
+ attach_function :rd_kafka_conf_get, [:pointer, :string, :pointer, :pointer], :kafka_config_response
241
+ attach_function :rd_kafka_conf, [:pointer], :pointer
238
242
  attach_function :rd_kafka_conf_dump, [:pointer, :pointer], :pointer
239
243
  attach_function :rd_kafka_conf_dump_free, [:pointer, :size_t], :void
240
244
  attach_function :rd_kafka_conf_destroy, [:pointer], :void
@@ -257,10 +261,11 @@ module Rdkafka
257
261
 
258
262
  # Queue IO Event Support - for fiber scheduler integration
259
263
  # Enables notifications to a custom FD when queue transitions from empty to non-empty
260
- # @param queue rd_kafka_queue_t* - the queue to monitor
261
- # @param fd int - file descriptor to write to (provide your own pipe/eventfd)
262
- # @param payload const void* - data to write to fd
263
- # @param size size_t - size of payload
264
+ # Arguments:
265
+ # - queue (rd_kafka_queue_t*) - the queue to monitor
266
+ # - fd (int) - file descriptor to write to (provide your own pipe/eventfd)
267
+ # - payload (const void*) - data to write to fd
268
+ # - size (size_t) - size of payload
264
269
  attach_function :rd_kafka_queue_io_event_enable, [:pointer, :int, :pointer, :size_t], :void
265
270
  # Per topic configs
266
271
  attach_function :rd_kafka_topic_conf_new, [], :pointer
@@ -335,9 +340,10 @@ module Rdkafka
335
340
  end
336
341
  end
337
342
 
338
- # The OAuth callback is currently global and contextless.
339
- # This means that the callback will be called for all instances, and the callback must be able to determine to which instance it is associated.
340
- # The instance name will be provided in the callback, allowing the callback to reference the correct instance.
343
+ # The OAuth callback is currently global and contextless. This means that the callback will be
344
+ # called for all instances, and the callback must be able to determine to which instance it is
345
+ # associated. The instance name will be provided in the callback, allowing the callback to
346
+ # reference the correct instance.
341
347
  #
342
348
  # An example of how to use the instance name in the callback is given below.
343
349
  # The `refresh_token` is configured as the `oauthbearer_token_refresh_callback`.
@@ -541,6 +547,37 @@ module Rdkafka
541
547
  attach_function :rd_kafka_event_DeleteGroups_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_DeleteGroups_result_t*
542
548
  attach_function :rd_kafka_DeleteGroups_result_groups, [:pointer, :pointer], :pointer, blocking: true # rd_kafka_DeleteGroups_result_t*, size_t* => rd_kafka_group_result_t**
543
549
 
550
+ # Delete Records
551
+ RD_KAFKA_ADMIN_OP_DELETERECORDS = 6 # rd_kafka_admin_op_t
552
+ RD_KAFKA_EVENT_DELETERECORDS_RESULT = 105 # rd_kafka_event_type_t
553
+
554
+ attach_function :rd_kafka_DeleteRecords, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
555
+ attach_function :rd_kafka_DeleteRecords_new, [:pointer], :pointer, blocking: true
556
+ attach_function :rd_kafka_DeleteRecords_destroy, [:pointer], :void, blocking: true
557
+ attach_function :rd_kafka_event_DeleteRecords_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_DeleteRecords_result_t*
558
+ attach_function :rd_kafka_DeleteRecords_result_offsets, [:pointer], :pointer, blocking: true # rd_kafka_DeleteRecords_result_t* => rd_kafka_topic_partition_list_t*
559
+
560
+ # List Consumer Groups
561
+ RD_KAFKA_ADMIN_OP_LISTCONSUMERGROUPS = 12 # rd_kafka_admin_op_t
562
+ RD_KAFKA_EVENT_LISTCONSUMERGROUPS_RESULT = 0x2000 # rd_kafka_event_type_t
563
+
564
+ # Consumer group states (rd_kafka_consumer_group_state_t)
565
+ RD_KAFKA_CONSUMER_GROUP_STATE_UNKNOWN = 0
566
+ RD_KAFKA_CONSUMER_GROUP_STATE_PREPARING_REBALANCE = 1
567
+ RD_KAFKA_CONSUMER_GROUP_STATE_COMPLETING_REBALANCE = 2
568
+ RD_KAFKA_CONSUMER_GROUP_STATE_STABLE = 3
569
+ RD_KAFKA_CONSUMER_GROUP_STATE_DEAD = 4
570
+ RD_KAFKA_CONSUMER_GROUP_STATE_EMPTY = 5
571
+
572
+ attach_function :rd_kafka_ListConsumerGroups, [:pointer, :pointer, :pointer], :void, blocking: true
573
+ attach_function :rd_kafka_event_ListConsumerGroups_result, [:pointer], :pointer, blocking: true # rd_kafka_event_t* => rd_kafka_ListConsumerGroups_result_t*
574
+ attach_function :rd_kafka_ListConsumerGroups_result_valid, [:pointer, :pointer], :pointer, blocking: true # result*, size_t* => rd_kafka_ConsumerGroupListing_t**
575
+ attach_function :rd_kafka_ListConsumerGroups_result_errors, [:pointer, :pointer], :pointer, blocking: true # result*, size_t* => rd_kafka_error_t**
576
+ attach_function :rd_kafka_ConsumerGroupListing_group_id, [:pointer], :pointer, blocking: true # => const char*
577
+ attach_function :rd_kafka_ConsumerGroupListing_is_simple_consumer_group, [:pointer], :int, blocking: true
578
+ attach_function :rd_kafka_ConsumerGroupListing_state, [:pointer], :int, blocking: true
579
+ attach_function :rd_kafka_consumer_group_state_name, [:int], :pointer, blocking: true # => const char*
580
+
544
581
  # Background Queue and Callback
545
582
 
546
583
  attach_function :rd_kafka_conf_set_background_event_cb, [:pointer, :pointer], :void
@@ -603,21 +640,24 @@ module Rdkafka
603
640
  attach_function :rd_kafka_AclBindingFilter_new, [:int32, :pointer, :int32, :pointer, :pointer, :int32, :int32, :pointer, :size_t], :pointer
604
641
  attach_function :rd_kafka_AclBinding_destroy, [:pointer], :void
605
642
 
606
- # rd_kafka_ResourceType_t - https://github.com/confluentinc/librdkafka/blob/292d2a66b9921b783f08147807992e603c7af059/src/rdkafka.h#L7307
643
+ # rd_kafka_ResourceType_t -
644
+ # https://github.com/confluentinc/librdkafka/blob/292d2a66b992/src/rdkafka.h#L7307
607
645
  RD_KAFKA_RESOURCE_ANY = 1
608
646
  RD_KAFKA_RESOURCE_TOPIC = 2
609
647
  RD_KAFKA_RESOURCE_GROUP = 3
610
648
  RD_KAFKA_RESOURCE_BROKER = 4
611
649
  RD_KAFKA_RESOURCE_TRANSACTIONAL_ID = 5
612
650
 
613
- # rd_kafka_ResourcePatternType_t - https://github.com/confluentinc/librdkafka/blob/292d2a66b9921b783f08147807992e603c7af059/src/rdkafka.h#L7320
651
+ # rd_kafka_ResourcePatternType_t -
652
+ # https://github.com/confluentinc/librdkafka/blob/292d2a66b992/src/rdkafka.h#L7320
614
653
  RD_KAFKA_RESOURCE_PATTERN_UNKNOWN = 0
615
654
  RD_KAFKA_RESOURCE_PATTERN_ANY = 1
616
655
  RD_KAFKA_RESOURCE_PATTERN_MATCH = 2
617
656
  RD_KAFKA_RESOURCE_PATTERN_LITERAL = 3
618
657
  RD_KAFKA_RESOURCE_PATTERN_PREFIXED = 4
619
658
 
620
- # rd_kafka_AclOperation_t - https://github.com/confluentinc/librdkafka/blob/292d2a66b9921b783f08147807992e603c7af059/src/rdkafka.h#L8403
659
+ # rd_kafka_AclOperation_t -
660
+ # https://github.com/confluentinc/librdkafka/blob/292d2a66b992/src/rdkafka.h#L8403
621
661
  RD_KAFKA_ACL_OPERATION_ANY = 1
622
662
  RD_KAFKA_ACL_OPERATION_ALL = 2
623
663
  RD_KAFKA_ACL_OPERATION_READ = 3
@@ -631,7 +671,8 @@ module Rdkafka
631
671
  RD_KAFKA_ACL_OPERATION_ALTER_CONFIGS = 11
632
672
  RD_KAFKA_ACL_OPERATION_IDEMPOTENT_WRITE = 12
633
673
 
634
- # rd_kafka_AclPermissionType_t - https://github.com/confluentinc/librdkafka/blob/292d2a66b9921b783f08147807992e603c7af059/src/rdkafka.h#L8435
674
+ # rd_kafka_AclPermissionType_t -
675
+ # https://github.com/confluentinc/librdkafka/blob/292d2a66b992/src/rdkafka.h#L8435
635
676
  RD_KAFKA_ACL_PERMISSION_TYPE_ANY = 1
636
677
  RD_KAFKA_ACL_PERMISSION_TYPE_DENY = 2
637
678
  RD_KAFKA_ACL_PERMISSION_TYPE_ALLOW = 3
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Callbacks
5
+ # Handles `RD_KAFKA_EVENT_DELETERECORDS_RESULT` events
6
+ # @private
7
+ class DeleteRecordsHandler < BaseHandler
8
+ class << self
9
+ # Resolves the delete-records 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_ptr = Rdkafka::Bindings.rd_kafka_event_DeleteRecords_result(event_ptr)
14
+ handle_ptr = Rdkafka::Bindings.rd_kafka_event_opaque(event_ptr)
15
+
16
+ return unless (handle = Rdkafka::Admin::DeleteRecordsHandle.remove(handle_ptr.address))
17
+
18
+ # An operation-level error (e.g. timeout or a closed client) is delivered on the event
19
+ # itself, with no per-partition result to parse.
20
+ return if resolve_operation_error(event_ptr, handle)
21
+
22
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
23
+
24
+ # Parsing must copy everything out of event-owned memory before the event is destroyed.
25
+ # An exception here is captured and re-raised on the waiting thread, since it cannot
26
+ # unwind through librdkafka native frames.
27
+ handle.result = begin
28
+ Rdkafka::Admin::DeleteRecordsReport.new(result_ptr)
29
+ rescue => e
30
+ e
31
+ end
32
+
33
+ handle.unlock
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Callbacks
5
+ # Handles `RD_KAFKA_EVENT_LISTCONSUMERGROUPS_RESULT` events
6
+ # @private
7
+ class ListConsumerGroupsHandler < BaseHandler
8
+ class << self
9
+ # Resolves the list-consumer-groups 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_ptr = Rdkafka::Bindings.rd_kafka_event_ListConsumerGroups_result(event_ptr)
14
+ handle_ptr = Rdkafka::Bindings.rd_kafka_event_opaque(event_ptr)
15
+
16
+ return unless (handle = Rdkafka::Admin::ListConsumerGroupsHandle.remove(handle_ptr.address))
17
+
18
+ # An operation-level error (e.g. timeout or a closed client) is delivered on the event
19
+ # itself; per the source feature's all-or-nothing contract we surface it as the result
20
+ # rather than returning a partial listing.
21
+ return if resolve_operation_error(event_ptr, handle)
22
+
23
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
24
+
25
+ # Parsing must copy everything out of event-owned memory before the event is destroyed.
26
+ # An exception here is captured and re-raised on the waiting thread, since it cannot
27
+ # unwind through librdkafka native frames.
28
+ handle.result = begin
29
+ Rdkafka::Admin::ListConsumerGroupsReport.new(result_ptr)
30
+ rescue => e
31
+ e
32
+ end
33
+
34
+ handle.unlock
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -213,12 +213,14 @@ module Rdkafka
213
213
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETETOPICS_RESULT then DeleteTopicHandler
214
214
  when Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_CREATEPARTITIONS_RESULT then CreatePartitionsHandler
215
215
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETEGROUPS_RESULT then DeleteGroupsHandler
216
+ when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETERECORDS_RESULT then DeleteRecordsHandler
216
217
  when Rdkafka::Bindings::RD_KAFKA_EVENT_CREATEACLS_RESULT then CreateAclHandler
217
218
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DELETEACLS_RESULT then DeleteAclHandler
218
219
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DESCRIBEACLS_RESULT then DescribeAclHandler
219
220
  when Rdkafka::Bindings::RD_KAFKA_EVENT_DESCRIBECONFIGS_RESULT then DescribeConfigsHandler
220
221
  when Rdkafka::Bindings::RD_KAFKA_EVENT_INCREMENTALALTERCONFIGS_RESULT then IncrementalAlterConfigsHandler
221
222
  when Rdkafka::Bindings::RD_KAFKA_EVENT_LISTOFFSETS_RESULT then ListOffsetsHandler
223
+ when Rdkafka::Bindings::RD_KAFKA_EVENT_LISTCONSUMERGROUPS_RESULT then ListConsumerGroupsHandler
222
224
  end
223
225
 
224
226
  handler&.call(event_ptr)
@@ -280,9 +282,9 @@ module Rdkafka
280
282
  end
281
283
  end
282
284
 
283
- # @private
285
+ # @!visibility private
284
286
  @@mutex = Mutex.new
285
- # @private
287
+ # @!visibility private
286
288
  @@current_pid = nil
287
289
 
288
290
  class << self
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ # Tracks live public clients so they can be closed before Ruby begins forced finalization.
5
+ # @private
6
+ module Clients
7
+ # Ruby rewrote `ObjectSpace::WeakMap` in 3.3. On 3.2 and earlier its `[]` can hand back a
8
+ # foreign or already-freed object for a key, which raises `NoMethodError` or segfaults the VM
9
+ # the moment the value is touched. Track weakly only where that is safe and fall back to a
10
+ # strong registry - pruned as clients close - on older Rubies. The strong registry holds an
11
+ # open, un-closed client until shutdown, which is exactly when this cleanup needs it anyway.
12
+ WEAK_TRACKING = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.3")
13
+
14
+ @pid = Process.pid
15
+ @mutex = Mutex.new
16
+
17
+ if WEAK_TRACKING
18
+ @clients = ObjectSpace::WeakMap.new
19
+ @tokens = [].freeze
20
+ else
21
+ @clients = {}.freeze
22
+ end
23
+
24
+ # Registers a successfully constructed client so it can be closed during process shutdown.
25
+ # @param client [Consumer, Producer, Admin] client to close during process shutdown
26
+ # @return [Consumer, Producer, Admin] the registered client
27
+ def self.register(client)
28
+ reset_after_fork
29
+
30
+ @mutex.synchronize do
31
+ if WEAK_TRACKING
32
+ live_clients
33
+
34
+ # The strongly-held token is the key and the client is the weak value, so registering a
35
+ # client never keeps it alive. Enumerating weak keys can expose reclaimed slots, so we
36
+ # look the value up by token instead.
37
+ token = Object.new
38
+ @clients[token] = client
39
+ @tokens = (@tokens + [token]).freeze
40
+ else
41
+ # Strong references, so drop clients that have already been closed to bound growth.
42
+ live = @clients.reject { |_id, existing| existing.closed? }
43
+ live[client.object_id] = client
44
+ @clients = live.freeze
45
+ end
46
+ end
47
+
48
+ client
49
+ end
50
+
51
+ # Closes each live client registered in this process.
52
+ # @return [nil]
53
+ # @raise [StandardError] the first close error, after attempting every client
54
+ def self.close_all
55
+ return unless @pid == Process.pid
56
+
57
+ clients = @mutex.synchronize { current_clients }
58
+ first_error = nil
59
+
60
+ clients.each do |client|
61
+ client.close unless client.closed?
62
+ rescue => error
63
+ first_error ||= error
64
+ end
65
+
66
+ raise first_error if first_error
67
+ end
68
+
69
+ # Returns the currently registered live clients.
70
+ # @return [Array<Consumer, Producer, Admin>] live clients
71
+ # @private
72
+ def self.current_clients
73
+ WEAK_TRACKING ? live_clients : @clients.values
74
+ end
75
+ private_class_method :current_clients
76
+
77
+ # Returns the live clients and releases tokens whose clients have been collected.
78
+ # Weak-tracking Rubies only.
79
+ # @return [Array<Consumer, Producer, Admin>] live clients
80
+ # @private
81
+ def self.live_clients
82
+ clients = []
83
+ tokens = []
84
+
85
+ @tokens.each do |token|
86
+ client = @clients[token]
87
+ clients << client if client
88
+ tokens << token if client
89
+ end
90
+
91
+ @tokens = tokens.freeze
92
+ clients
93
+ end
94
+ private_class_method :live_clients
95
+
96
+ # Replaces inherited state before registering a client in a forked child.
97
+ # @return [nil]
98
+ # @private
99
+ def self.reset_after_fork
100
+ return if @pid == Process.pid
101
+
102
+ if WEAK_TRACKING
103
+ @clients = ObjectSpace::WeakMap.new
104
+ @tokens = [].freeze
105
+ else
106
+ @clients = {}.freeze
107
+ end
108
+
109
+ @pid = Process.pid
110
+ @mutex = Mutex.new
111
+ end
112
+ private_class_method :reset_after_fork
113
+ end
114
+ end
@@ -3,24 +3,24 @@
3
3
  module Rdkafka
4
4
  # Configuration for a Kafka consumer or producer. You can create an instance and use
5
5
  # the consumer and producer methods to create a client. Documentation of the available
6
- # configuration options is available on https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md.
6
+ # configuration options is available on
7
+ # https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md.
7
8
  class Config
8
- # @private
9
+ # @!visibility private
9
10
  @@logger = Logger.new($stdout)
10
- # @private
11
+ # @!visibility private
11
12
  @@statistics_callback = nil
12
- # @private
13
+ # @!visibility private
13
14
  @@error_callback = nil
14
- # @private
15
+ # @!visibility private
15
16
  @@opaques = ObjectSpace::WeakMap.new
16
- # @private
17
+ # @!visibility private
17
18
  @@log_queue = Queue.new
18
- # We memoize thread on the first log flush
19
- # This allows us also to restart logger thread on forks
19
+ # We memoize thread on first log flush. This allows us also to restart logger thread on forks
20
20
  @@log_thread = nil
21
- # @private
21
+ # @!visibility private
22
22
  @@log_mutex = Mutex.new
23
- # @private
23
+ # @!visibility private
24
24
  @@oauthbearer_token_refresh_callback = nil
25
25
 
26
26
  # Returns the current logger, by default this is a logger to stdout.
@@ -69,7 +69,8 @@ module Rdkafka
69
69
 
70
70
  # Set a callback that will be called every time the underlying client emits statistics.
71
71
  # You can configure if and how often this happens using `statistics.interval.ms`.
72
- # The callback is called with a hash that's documented here: https://github.com/confluentinc/librdkafka/blob/master/STATISTICS.md
72
+ # The callback is called with a hash that's documented here:
73
+ # https://github.com/confluentinc/librdkafka/blob/master/STATISTICS.md
73
74
  #
74
75
  # @param callback [Proc, #call, nil] callable object or nil to clear
75
76
  # @return [nil]
@@ -86,7 +87,8 @@ module Rdkafka
86
87
  end
87
88
 
88
89
  # Set a callback that will be called every time the underlying client emits an error.
89
- # If this callback is not set, global errors such as brokers becoming unavailable will only be sent to the logger, as defined by librdkafka.
90
+ # If this callback is not set, global errors such as brokers becoming unavailable will only be
91
+ # sent to the logger, as defined by librdkafka.
90
92
  # The callback is called with an instance of RdKafka::Error.
91
93
  #
92
94
  # @param callback [Proc, #call, nil] callable object to handle errors or nil to clear
@@ -197,26 +199,26 @@ module Rdkafka
197
199
  opaque = Opaque.new
198
200
  config = native_config(opaque)
199
201
 
202
+ # Set callback to receive background events, so admin-style operations issued on the
203
+ # consumer handle (e.g. Consumer#list_offsets) get their results dispatched back to their
204
+ # handles. librdkafka spawns its internally-managed background thread because of this.
205
+ Rdkafka::Bindings.rd_kafka_conf_set_background_event_cb(config, Rdkafka::Callbacks::BackgroundEventCallbackFunction)
206
+
200
207
  if @consumer_rebalance_listener
201
208
  opaque.consumer_rebalance_listener = @consumer_rebalance_listener
202
209
  Rdkafka::Bindings.rd_kafka_conf_set_rebalance_cb(config, Rdkafka::Bindings::RebalanceCallback)
203
210
  end
204
211
 
205
- # Create native client
206
- kafka = native_kafka(config, :rd_kafka_consumer)
207
-
208
- # Redirect the main queue to the consumer queue
209
- Rdkafka::Bindings.rd_kafka_poll_set_consumer(kafka) if @consumer_poll_set
210
-
211
- # Return consumer with Kafka client
212
- Rdkafka::Consumer.new(
213
- Rdkafka::NativeKafka.new(
214
- kafka,
215
- run_polling_thread: false,
216
- opaque: opaque,
217
- auto_start: native_kafka_auto_start
218
- )
219
- )
212
+ build_native_client(
213
+ config,
214
+ :rd_kafka_consumer,
215
+ opaque: opaque,
216
+ run_polling_thread: false,
217
+ auto_start: native_kafka_auto_start
218
+ ) do |kafka, native_kafka|
219
+ Rdkafka::Bindings.rd_kafka_poll_set_consumer(kafka) if @consumer_poll_set
220
+ Rdkafka::Consumer.new(native_kafka)
221
+ end
220
222
  end
221
223
 
222
224
  # Create a producer with this configuration.
@@ -240,19 +242,17 @@ module Rdkafka
240
242
  # Return producer with Kafka client
241
243
  partitioner_name = self[:partitioner] || self["partitioner"]
242
244
 
243
- kafka = native_kafka(config, :rd_kafka_producer)
244
-
245
- Rdkafka::Producer.new(
246
- Rdkafka::NativeKafka.new(
247
- kafka,
248
- run_polling_thread: run_polling_thread,
249
- opaque: opaque,
250
- auto_start: native_kafka_auto_start,
251
- timeout_ms: native_kafka_poll_timeout_ms
252
- ),
253
- partitioner_name
254
- ).tap do |producer|
255
- opaque.producer = producer
245
+ build_native_client(
246
+ config,
247
+ :rd_kafka_producer,
248
+ opaque: opaque,
249
+ run_polling_thread: run_polling_thread,
250
+ auto_start: native_kafka_auto_start,
251
+ timeout_ms: native_kafka_poll_timeout_ms
252
+ ) do |_kafka, native_kafka|
253
+ Rdkafka::Producer.new(native_kafka, partitioner_name).tap do |producer|
254
+ opaque.producer = producer
255
+ end
256
256
  end
257
257
  end
258
258
 
@@ -272,17 +272,16 @@ module Rdkafka
272
272
  config = native_config(opaque)
273
273
  Rdkafka::Bindings.rd_kafka_conf_set_background_event_cb(config, Rdkafka::Callbacks::BackgroundEventCallbackFunction)
274
274
 
275
- kafka = native_kafka(config, :rd_kafka_producer)
276
-
277
- Rdkafka::Admin.new(
278
- Rdkafka::NativeKafka.new(
279
- kafka,
280
- run_polling_thread: run_polling_thread,
281
- opaque: opaque,
282
- auto_start: native_kafka_auto_start,
283
- timeout_ms: native_kafka_poll_timeout_ms
284
- )
285
- )
275
+ build_native_client(
276
+ config,
277
+ :rd_kafka_producer,
278
+ opaque: opaque,
279
+ run_polling_thread: run_polling_thread,
280
+ auto_start: native_kafka_auto_start,
281
+ timeout_ms: native_kafka_poll_timeout_ms
282
+ ) do |_kafka, native_kafka|
283
+ Rdkafka::Admin.new(native_kafka)
284
+ end
286
285
  end
287
286
 
288
287
  # Returns all configuration properties and their current values for this config.
@@ -290,6 +289,9 @@ module Rdkafka
290
289
  # Uses `rd_kafka_conf_dump` to retrieve every property (including defaults and
291
290
  # internal properties like `client.software.name`) as a flat Hash.
292
291
  #
292
+ # @return [Hash{Symbol => String}] property names mapped to their current values
293
+ #
294
+ # @raise [ConfigError] When the configuration contains invalid options
293
295
  # @note The librdkafka C API does not distinguish between producer-only, consumer-only,
294
296
  # and global properties at the configuration level. All properties are returned
295
297
  # regardless of the intended client type.
@@ -297,10 +299,6 @@ module Rdkafka
297
299
  # @note The returned Hash may include sensitive values such as authentication
298
300
  # credentials and key passwords. Do not log or serialize the returned data
299
301
  # unless you have explicitly redacted secret entries.
300
- #
301
- # @return [Hash{Symbol => String}] property names mapped to their current values
302
- #
303
- # @raise [ConfigError] When the configuration contains invalid options
304
302
  def describe_properties
305
303
  config = nil
306
304
  dump_ptr = nil
@@ -325,7 +323,8 @@ module Rdkafka
325
323
  Rdkafka::Bindings.rd_kafka_conf_destroy(config) if config
326
324
  end
327
325
 
328
- # Error that is returned by the underlying rdkafka error if an invalid configuration option is present.
326
+ # Error that is returned by the underlying rdkafka error if an invalid configuration option is
327
+ # present.
329
328
  class ConfigError < RuntimeError; end
330
329
 
331
330
  # Error that is returned by the underlying rdkafka library if the client cannot be created.
@@ -336,6 +335,56 @@ module Rdkafka
336
335
 
337
336
  private
338
337
 
338
+ # Builds a public client around a native Kafka handle, destroying the current owner if setup
339
+ # fails before ownership is transferred to the caller.
340
+ # @param config [FFI::Pointer] native rdkafka configuration
341
+ # @param type [Symbol] native client type
342
+ # @param opaque [Object] callback context retained for the native client
343
+ # @param run_polling_thread [Boolean] whether to run a Ruby polling thread
344
+ # @param auto_start [Boolean] whether to start the polling thread after wrapping the client
345
+ # @param timeout_ms [Integer] polling timeout in milliseconds
346
+ # @yieldparam kafka [FFI::Pointer] native Kafka handle
347
+ # @yieldparam native_kafka [NativeKafka] Ruby owner of the native handle
348
+ # @return [Consumer, Producer, Admin] the constructed public client
349
+ # @private
350
+ def build_native_client(
351
+ config,
352
+ type,
353
+ opaque:,
354
+ run_polling_thread:,
355
+ auto_start:,
356
+ timeout_ms: Defaults::NATIVE_KAFKA_POLL_TIMEOUT_MS
357
+ )
358
+ kafka = native_kafka(config, type)
359
+ native_kafka = nil
360
+ client = nil
361
+
362
+ begin
363
+ native_kafka = Rdkafka::NativeKafka.new(
364
+ kafka,
365
+ run_polling_thread: run_polling_thread,
366
+ opaque: opaque,
367
+ auto_start: false,
368
+ timeout_ms: timeout_ms
369
+ )
370
+ client = yield(kafka, native_kafka)
371
+ native_kafka.start if auto_start
372
+ Rdkafka::Clients.register(client)
373
+ rescue Exception
374
+ owner = client || native_kafka
375
+
376
+ begin
377
+ owner ? owner.close : Rdkafka::Bindings.rd_kafka_destroy(kafka)
378
+ rescue Exception => err
379
+ # Never let a teardown failure replace the error that caused it: that error is what the
380
+ # caller needs to see, and it is re-raised below regardless.
381
+ Rdkafka::Config.logger.error("Unhandled exception: #{err.class} - #{err.message}")
382
+ end
383
+
384
+ raise
385
+ end
386
+ end
387
+
339
388
  # This method is only intended to be used to create a client,
340
389
  # using it in another way will leak memory.
341
390
  #
@@ -422,6 +471,9 @@ module Rdkafka
422
471
 
423
472
  # Return handle which should be closed using rd_kafka_destroy after usage.
424
473
  handle
474
+ rescue Exception
475
+ Rdkafka::Bindings.rd_kafka_destroy(handle) if handle && !handle.null?
476
+ raise
425
477
  end
426
478
  end
427
479
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Consumer
5
+ # Creates FFI pointers from the process-global allocator so that memory handed to librdkafka
6
+ # can be released by it with the matching `free`.
7
+ module Allocator
8
+ extend FFI::Library
9
+
10
+ ffi_lib FFI::CURRENT_PROCESS # use process-global malloc not FFI::Library::LIBC
11
+ attach_function :malloc, [:size_t], :pointer
12
+
13
+ class << self
14
+ # Return an FFI pointer to a string allocated from the global allocator
15
+ # so that it can be released by another library.
16
+ #
17
+ # @private
18
+ #
19
+ # @param string [String] Ruby string that will be copied into the new
20
+ # string pointer.
21
+ #
22
+ # @return [FFI::Pointer]
23
+ def string_pointer(string)
24
+ bytes = string.to_s
25
+ pointer = malloc(bytes.bytesize + 1)
26
+ Kernel.raise(NoMemoryError, "malloc failed") if pointer.null?
27
+
28
+ pointer.put_bytes(0, bytes)
29
+ pointer.put_char(bytes.bytesize, 0) # NUL terminator
30
+
31
+ pointer.autorelease = false
32
+
33
+ pointer
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end