rdkafka 0.29.0-aarch64-linux-gnu → 0.29.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.
@@ -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,6 +199,11 @@ 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)
@@ -290,6 +297,9 @@ module Rdkafka
290
297
  # Uses `rd_kafka_conf_dump` to retrieve every property (including defaults and
291
298
  # internal properties like `client.software.name`) as a flat Hash.
292
299
  #
300
+ # @return [Hash{Symbol => String}] property names mapped to their current values
301
+ #
302
+ # @raise [ConfigError] When the configuration contains invalid options
293
303
  # @note The librdkafka C API does not distinguish between producer-only, consumer-only,
294
304
  # and global properties at the configuration level. All properties are returned
295
305
  # regardless of the intended client type.
@@ -297,10 +307,6 @@ module Rdkafka
297
307
  # @note The returned Hash may include sensitive values such as authentication
298
308
  # credentials and key passwords. Do not log or serialize the returned data
299
309
  # 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
310
  def describe_properties
305
311
  config = nil
306
312
  dump_ptr = nil
@@ -325,7 +331,8 @@ module Rdkafka
325
331
  Rdkafka::Bindings.rd_kafka_conf_destroy(config) if config
326
332
  end
327
333
 
328
- # Error that is returned by the underlying rdkafka error if an invalid configuration option is present.
334
+ # Error that is returned by the underlying rdkafka error if an invalid configuration option is
335
+ # present.
329
336
  class ConfigError < RuntimeError; end
330
337
 
331
338
  # Error that is returned by the underlying rdkafka library if the client cannot be created.
@@ -6,7 +6,8 @@ module Rdkafka
6
6
  class TopicPartitionList
7
7
  # Create a topic partition list.
8
8
  #
9
- # @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an empty list
9
+ # @param data [Hash{String => nil,Partition}] The topic and partition data or nil to create an
10
+ # empty list
10
11
  #
11
12
  # @return [TopicPartitionList]
12
13
  def initialize(data = nil)
@@ -34,10 +35,12 @@ module Rdkafka
34
35
  end
35
36
 
36
37
  # Add a topic with optionally partitions to the list.
37
- # Calling this method multiple times for the same topic will overwrite the previous configuraton.
38
+ # Calling this method multiple times for the same topic will overwrite the previous
39
+ # configuraton.
38
40
  #
39
41
  # @param topic [String] The topic's name
40
- # @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or partition count
42
+ # @param partitions [Array<Integer>, Range<Integer>, Integer] The topic's partitions or
43
+ # partition count
41
44
  #
42
45
  # @return [nil]
43
46
  #
@@ -61,11 +64,12 @@ module Rdkafka
61
64
  end
62
65
 
63
66
  # Add a topic with partitions and offsets set to the list
64
- # Calling this method multiple times for the same topic will overwrite the previous configuraton.
67
+ # Calling this method multiple times for the same topic will overwrite the previous
68
+ # configuraton.
65
69
  #
66
70
  # @param topic [String] The topic's name
67
- # @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The topic's
68
- # partitions and offsets (Hash) or partitions with offsets and metadata (Array)
71
+ # @param partitions_with_offsets [Hash{Integer => Integer}, Array<Consumer::Partition>] The
72
+ # topic's partitions and offsets (Hash) or partitions with offsets and metadata (Array)
69
73
  # @return [nil]
70
74
  def add_topic_and_partitions_with_offsets(topic, partitions_with_offsets)
71
75
  @data[topic.to_s] = partitions_with_offsets.map do |p, o|
@@ -73,7 +77,8 @@ module Rdkafka
73
77
  end
74
78
  end
75
79
 
76
- # Return a `Hash` with the topics as keys and and an array of partition information as the value if present.
80
+ # Return a `Hash` with the topics as keys and and an array of partition information as the
81
+ # value if present.
77
82
  #
78
83
  # @return [Hash{String => Array<Partition>,nil}]
79
84
  def to_h
@@ -97,7 +102,8 @@ module Rdkafka
97
102
  #
98
103
  # @private
99
104
  #
100
- # @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will be copied.
105
+ # @param pointer [FFI::Pointer] Optional pointer to an existing native list. Its contents will
106
+ # be copied.
101
107
  #
102
108
  # @return [TopicPartitionList]
103
109
  def self.from_native_tpl(pointer)
@@ -15,6 +15,7 @@ module Rdkafka
15
15
  include Helpers::Time
16
16
  include Helpers::OAuth
17
17
  include Helpers::Metadata
18
+ include Helpers::ListOffsets
18
19
 
19
20
  # @private
20
21
  # @param native_kafka [NativeKafka] wrapper around the native Kafka consumer handle
@@ -34,11 +35,13 @@ module Rdkafka
34
35
  # consumer-queue reference, then destroy the native client. The default `NativeKafka#finalizer`
35
36
  # went straight to `rd_kafka_destroy`, leaving the consumer-queue reference (from
36
37
  # `rd_kafka_queue_get_consumer`, taken by `poll_batch`) dangling - which can make
37
- # `rd_kafka_destroy` block inside the finalizer (process hang at GC/shutdown) or leak the handle.
38
+ # `rd_kafka_destroy` block inside the finalizer (process hang at GC/shutdown) or leak the
39
+ # handle.
38
40
  #
39
41
  # @private
40
42
  # @param native_kafka [NativeKafka] the wrapped native client
41
- # @param queue_holder [Array] single-element holder carrying the consumer queue pointer (or empty)
43
+ # @param queue_holder [Array] single-element holder carrying the consumer queue pointer
44
+ # (or empty)
42
45
  # @return [Proc] finalizer proc that must not reference the consumer instance
43
46
  def self.finalizer(native_kafka, queue_holder)
44
47
  proc do
@@ -124,11 +127,6 @@ module Rdkafka
124
127
  # @return [nil]
125
128
  # @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
126
129
  #
127
- # @note This method holds the inner lock until the queue is empty or `:stop` is returned.
128
- # Other consumer operations will wait until this method returns.
129
- # @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
130
- # @note Do NOT use this if `consumer_poll_set` was set to `true`
131
- #
132
130
  # @example Drain all pending events
133
131
  # consumer.events_poll_nb_each { |_count| }
134
132
  #
@@ -137,6 +135,10 @@ module Rdkafka
137
135
  # consumer.events_poll_nb_each do |_count|
138
136
  # :stop if monotonic_now >= deadline
139
137
  # end
138
+ # @note This method holds the inner lock until the queue is empty or `:stop` is returned.
139
+ # Other consumer operations will wait until this method returns.
140
+ # @note This method is thread-safe as it uses @native_kafka.with_inner synchronization
141
+ # @note Do NOT use this if `consumer_poll_set` was set to `true`
140
142
  def events_poll_nb_each
141
143
  closed_consumer_check(__method__)
142
144
 
@@ -162,14 +164,6 @@ module Rdkafka
162
164
  # @raise [Rdkafka::ClosedConsumerError] if called on a closed consumer
163
165
  # @raise [Rdkafka::RdkafkaError] if a Kafka error occurs while polling
164
166
  #
165
- # @note This method uses `rd_kafka_consumer_poll` to fetch messages, unlike
166
- # `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
167
- # statistics, etc.). For consumers, use this method to receive messages and
168
- # `events_poll_nb_each` for processing background events.
169
- # @note This method holds the inner lock for the duration. Other consumer operations
170
- # will wait until this method returns.
171
- # @note Timeout/max_messages logic should be implemented by the caller
172
- #
173
167
  # @example Process messages until queue is empty
174
168
  # consumer.poll_nb_each do |message|
175
169
  # process(message)
@@ -182,6 +176,13 @@ module Rdkafka
182
176
  # count += 1
183
177
  # :stop if count >= 10
184
178
  # end
179
+ # @note This method uses `rd_kafka_consumer_poll` to fetch messages, unlike
180
+ # `events_poll_nb_each` which uses `rd_kafka_poll` for event callbacks (delivery reports,
181
+ # statistics, etc.). For consumers, use this method to receive messages and
182
+ # `events_poll_nb_each` for processing background events.
183
+ # @note This method holds the inner lock for the duration. Other consumer operations
184
+ # will wait until this method returns.
185
+ # @note Timeout/max_messages logic should be implemented by the caller
185
186
  def poll_nb_each
186
187
  closed_consumer_check(__method__)
187
188
 
@@ -446,9 +447,11 @@ module Rdkafka
446
447
  end
447
448
 
448
449
  # Return the current positions (offsets) for topics and partitions.
449
- # The offset field of each requested partition will be set to the offset of the last consumed message + 1, or nil in case there was no previous message.
450
+ # The offset field of each requested partition will be set to the offset of the last consumed
451
+ # message + 1, or nil in case there was no previous message.
450
452
  #
451
- # @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil to use the current subscription.
453
+ # @param list [TopicPartitionList, nil] The topic with partitions to get the offsets for or nil
454
+ # to use the current subscription.
452
455
  #
453
456
  # @return [TopicPartitionList]
454
457
  #
@@ -515,28 +518,65 @@ module Rdkafka
515
518
  # possible to create one yourself, in this case you have to provide a list that
516
519
  # already contains all the partitions you need the lag for.
517
520
  #
521
+ # The end offsets of all requested partitions are fetched in a single batched
522
+ # {#list_offsets} query - librdkafka fans it out to the involved partition leaders
523
+ # internally and concurrently - instead of one blocking {#query_watermark_offsets}
524
+ # broker roundtrip per partition.
525
+ #
518
526
  # @param topic_partition_list [TopicPartitionList] The list to calculate lag for.
519
- # @param watermark_timeout_ms [Integer] The timeout for each query watermark call.
527
+ # @param watermark_timeout_ms [Integer] The timeout for the batched end-offsets query.
520
528
  # @return [Hash{String => Hash{Integer => Integer}}] A hash containing all topics with the lag
521
529
  # per partition
530
+ # @raise [ClosedConsumerError] when the consumer is closed
522
531
  # @raise [RdkafkaError] When querying the broker fails.
523
532
  def lag(topic_partition_list, watermark_timeout_ms = Defaults::CONSUMER_LAG_TIMEOUT_MS)
533
+ closed_consumer_check(__method__)
534
+
524
535
  out = {}
536
+ request = {}
537
+ partitions_by_topic = topic_partition_list.to_h
538
+
539
+ partitions_by_topic.each do |topic, partitions|
540
+ out[topic] = {}
525
541
 
526
- topic_partition_list.to_h.each do |topic, partitions|
527
- # Query high watermarks for this topic's partitions and compare to the offset in the list.
528
- topic_out = {}
529
542
  partitions.each do |p|
530
543
  next if p.offset.nil?
531
- _low, high = query_watermark_offsets(
532
- topic,
533
- p.partition,
534
- watermark_timeout_ms
535
- )
536
- topic_out[p.partition] = high - p.offset
544
+
545
+ (request[topic] ||= []) << { partition: p.partition, offset: :latest }
546
+ end
547
+ end
548
+
549
+ return out if request.empty?
550
+
551
+ report = begin
552
+ # The isolation level is forwarded so the end offsets match what the old per-partition
553
+ # watermark query returned: librdkafka resolves that query with the consumer's configured
554
+ # `isolation.level` (LSO for the default read_committed), while the admin-style batched
555
+ # query would otherwise default to read_uncommitted (true high watermark).
556
+ list_offsets(request, isolation_level: isolation_level)
557
+ .wait(max_wait_timeout_ms: watermark_timeout_ms)
558
+ rescue AbstractHandle::WaitTimeoutError
559
+ # Keep the pre-batching contract: a slow broker surfaced as a timed-out RdkafkaError
560
+ # from the per-partition watermark query, not as a handle wait timeout.
561
+ raise RdkafkaError.new(
562
+ Rdkafka::Bindings::RD_KAFKA_RESP_ERR__TIMED_OUT,
563
+ "Error querying watermark offsets of '#{request.keys.join(", ")}'"
564
+ )
565
+ end
566
+
567
+ end_offsets = {}
568
+ report.offsets.each do |result|
569
+ (end_offsets[result[:topic]] ||= {})[result[:partition]] = result[:offset]
570
+ end
571
+
572
+ partitions_by_topic.each do |topic, partitions|
573
+ partitions.each do |p|
574
+ next if p.offset.nil?
575
+
576
+ out[topic][p.partition] = end_offsets.fetch(topic).fetch(p.partition) - p.offset
537
577
  end
538
- out[topic] = topic_out
539
578
  end
579
+
540
580
  out
541
581
  end
542
582
 
@@ -846,7 +886,7 @@ module Rdkafka
846
886
  # returns without further waiting.
847
887
  #
848
888
  # Error events (e.g. `:partition_eof`) are returned inline as {RdkafkaError} objects
849
- # rather than raised, so callers receive the complete batch both messages and errors
889
+ # rather than raised, so callers receive the complete batch - both messages and errors -
850
890
  # and can decide how to handle each. This is particularly useful when multiple partitions
851
891
  # signal EOF simultaneously: all signals appear in the returned array rather than only
852
892
  # the first one being raised and the rest silently discarded.
@@ -921,15 +961,15 @@ module Rdkafka
921
961
  # particularly useful in fiber scheduler contexts where GVL release/reacquire
922
962
  # overhead is wasteful since we don't expect to wait.
923
963
  #
964
+ # @param timeout_ms [Integer] Timeout waiting for the first message
965
+ # (default: 0 for non-blocking)
966
+ # @param max_items [Integer] Maximum number of messages to return per call
967
+ # @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
968
+ # @raise [ClosedConsumerError] When called on a closed consumer
924
969
  # @note Since the GVL is not released, a non-zero timeout_ms will block all Ruby
925
970
  # threads/fibers for the duration. Use {#poll_batch} if you need a blocking wait.
926
971
  #
927
972
  # Error events are returned inline as {RdkafkaError} objects; see {#poll_batch} for details.
928
- #
929
- # @param timeout_ms [Integer] Timeout waiting for the first message (default: 0 for non-blocking)
930
- # @param max_items [Integer] Maximum number of messages to return per call
931
- # @return [Array<Message, RdkafkaError>] Batch of messages and/or error events in arrival order
932
- # @raise [ClosedConsumerError] When called on a closed consumer
933
973
  def poll_batch_nb(timeout_ms = 0, max_items: 100)
934
974
  closed_consumer_check(__method__)
935
975
 
@@ -1081,6 +1121,35 @@ module Rdkafka
1081
1121
  Rdkafka::Bindings.rd_kafka_mem_free(inner, ptr) unless ptr.null?
1082
1122
  end
1083
1123
 
1124
+ # Reads this consumer's effective `isolation.level` from the live librdkafka configuration
1125
+ # and maps it to the numeric isolation level constant. Memoized: the value cannot change
1126
+ # after client creation.
1127
+ #
1128
+ # @return [Integer] `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` or
1129
+ # `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED`
1130
+ # @raise [Rdkafka::Config::ConfigError] when the property cannot be read
1131
+ def isolation_level
1132
+ @isolation_level ||= @native_kafka.with_inner do |inner|
1133
+ conf = Rdkafka::Bindings.rd_kafka_conf(inner)
1134
+
1135
+ size_ptr = Rdkafka::Bindings::SizePtr.new
1136
+ size_ptr[:value] = 64
1137
+ value_ptr = FFI::MemoryPointer.new(:char, 64)
1138
+
1139
+ result = Rdkafka::Bindings.rd_kafka_conf_get(conf, "isolation.level", value_ptr, size_ptr)
1140
+
1141
+ if result != :config_ok
1142
+ raise Rdkafka::Config::ConfigError.new("Could not read isolation.level: #{result}")
1143
+ end
1144
+
1145
+ if value_ptr.read_string == "read_committed"
1146
+ Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED
1147
+ else
1148
+ Rdkafka::Bindings::RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED
1149
+ end
1150
+ end
1151
+ end
1152
+
1084
1153
  # Returns the consumer queue pointer, lazily initialized
1085
1154
  # @return [FFI::Pointer] consumer queue handle
1086
1155
  def consumer_queue
@@ -3,19 +3,17 @@
3
3
  module Rdkafka
4
4
  # Default timeout and timing values used throughout rdkafka-ruby.
5
5
  #
6
- # All timeout values can be overridden per-call via method parameters.
7
- # These constants provide a central place to understand and reference
8
- # the default values used across the library.
9
- #
10
- # @note These are rdkafka-ruby defaults, not librdkafka configuration options.
11
- # For librdkafka options, see:
12
- # https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md
6
+ # All timeout values can be overridden per-call via method parameters. These constants provide
7
+ # a central place to understand and reference the default values used across the library.
13
8
  #
14
9
  # @example Overriding a timeout per-call
15
10
  # consumer.committed(timeout_ms: 5_000) # Use 5 seconds instead of default 2 seconds
16
11
  #
17
12
  # @example Checking the default value
18
13
  # Rdkafka::Defaults::CONSUMER_COMMITTED_TIMEOUT_MS # => 2000
14
+ # @note These are rdkafka-ruby defaults, not librdkafka configuration options.
15
+ # For librdkafka options, see:
16
+ # https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md
19
17
  module Defaults
20
18
  # Consumer timeouts (in milliseconds)
21
19
 
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Helpers
5
+ # Shared `#list_offsets` implementation for Admin and Consumer.
6
+ #
7
+ # The librdkafka Admin API is handle-agnostic: `rd_kafka_ListOffsets()` can be issued on any
8
+ # `rd_kafka_t` instance regardless of its type, so this Ruby-level implementation is identical
9
+ # for admin and consumer clients. Includers must provide a private `#closed_check(method)`
10
+ # that raises their own `Closed*Error`, and must have the background event callback
11
+ # registered on their native config (see `Config#admin` and `Config#consumer`) so the result
12
+ # event is dispatched back to the returned handle.
13
+ module ListOffsets
14
+ # Queries partition offsets by specification (earliest, latest, max_timestamp, or by
15
+ # timestamp) without requiring a consumer group.
16
+ #
17
+ # The query is batched: all requested partitions are carried in one `ListOffsets` request
18
+ # that librdkafka fans out to the involved partition leaders internally and concurrently.
19
+ #
20
+ # @param topic_partition_offsets [Hash{String => Array<Hash>}] hash mapping topic names to
21
+ # arrays of partition offset specifications. Each specification is a hash with:
22
+ # - `:partition` [Integer] partition number
23
+ # - `:offset` [Symbol, Integer] offset specification - `:earliest`, `:latest`,
24
+ # `:max_timestamp`, or an integer timestamp in milliseconds
25
+ # @param isolation_level [Integer, nil] optional isolation level:
26
+ # - `RD_KAFKA_ISOLATION_LEVEL_READ_UNCOMMITTED` (0) - default
27
+ # - `RD_KAFKA_ISOLATION_LEVEL_READ_COMMITTED` (1)
28
+ #
29
+ # @return [Admin::ListOffsetsHandle] handle that can be used to wait for the result
30
+ #
31
+ # @raise [ClosedAdminError, ClosedConsumerError] when the client is closed
32
+ # @raise [ConfigError] when the background queue is unavailable
33
+ #
34
+ # @example Query earliest and latest offsets
35
+ # handle = client.list_offsets(
36
+ # { "my_topic" => [
37
+ # { partition: 0, offset: :earliest },
38
+ # { partition: 1, offset: :latest }
39
+ # ] }
40
+ # )
41
+ # report = handle.wait(max_wait_timeout_ms: 15_000)
42
+ # report.offsets
43
+ # # => [{ topic: "my_topic", partition: 0, offset: 0, ... }, ...]
44
+ def list_offsets(topic_partition_offsets, isolation_level: nil)
45
+ closed_check(__method__)
46
+
47
+ # Parse and validate every offset spec before allocating the native list, so a missing key
48
+ # or an unknown offset specification raises with nothing to clean up. Previously the
49
+ # ArgumentError (or KeyError) was raised after `rd_kafka_topic_partition_list_new`, leaking
50
+ # the native list.
51
+ parsed = topic_partition_offsets.flat_map do |topic, partitions|
52
+ partitions.map do |spec|
53
+ offset = spec.fetch(:offset)
54
+
55
+ native_offset = case offset
56
+ when :earliest then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_EARLIEST
57
+ when :latest then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_LATEST
58
+ when :max_timestamp then Rdkafka::Bindings::RD_KAFKA_OFFSET_SPEC_MAX_TIMESTAMP
59
+ when Integer then offset
60
+ else
61
+ raise ArgumentError, "Unknown offset specification: #{offset.inspect}"
62
+ end
63
+
64
+ [topic, spec.fetch(:partition), native_offset]
65
+ end
66
+ end
67
+
68
+ # Build native topic partition list
69
+ tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(parsed.size)
70
+
71
+ parsed.each do |topic, partition, native_offset|
72
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, partition)
73
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_set_offset(tpl, topic, partition, native_offset)
74
+ end
75
+
76
+ # Get a pointer to the queue that our request will be enqueued on
77
+ queue_ptr = @native_kafka.with_inner do |inner|
78
+ Rdkafka::Bindings.rd_kafka_queue_get_background(inner)
79
+ end
80
+
81
+ if queue_ptr.null?
82
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
83
+ raise Rdkafka::Config::ConfigError.new("rd_kafka_queue_get_background was NULL")
84
+ end
85
+
86
+ # Create and register the handle we will return to the caller
87
+ handle = Admin::ListOffsetsHandle.new
88
+ handle[:pending] = true
89
+ handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
90
+
91
+ admin_options_ptr = @native_kafka.with_inner do |inner|
92
+ Rdkafka::Bindings.rd_kafka_AdminOptions_new(
93
+ inner,
94
+ Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_LISTOFFSETS
95
+ )
96
+ end
97
+
98
+ if isolation_level
99
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_isolation_level(admin_options_ptr, isolation_level)
100
+ end
101
+
102
+ Admin::ListOffsetsHandle.register(handle)
103
+ Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, handle.to_ptr)
104
+
105
+ begin
106
+ @native_kafka.with_inner do |inner|
107
+ Rdkafka::Bindings.rd_kafka_ListOffsets(
108
+ inner,
109
+ tpl,
110
+ admin_options_ptr,
111
+ queue_ptr
112
+ )
113
+ end
114
+ rescue Exception
115
+ Admin::ListOffsetsHandle.remove(handle.to_ptr.address)
116
+ raise
117
+ ensure
118
+ Rdkafka::Bindings.rd_kafka_AdminOptions_destroy(admin_options_ptr)
119
+ Rdkafka::Bindings.rd_kafka_queue_destroy(queue_ptr)
120
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
121
+ end
122
+
123
+ handle
124
+ end
125
+ end
126
+ end
127
+ end
@@ -4,10 +4,14 @@ module Rdkafka
4
4
  module OAuth
5
5
  # Set the OAuthBearer token
6
6
  #
7
- # @param token [String] the mandatory token value to set, often (but not necessarily) a JWS compact serialization as per https://tools.ietf.org/html/rfc7515#section-3.1.
8
- # @param lifetime_ms [Integer] when the token expires, in terms of the number of milliseconds since the epoch. See https://currentmillis.com/.
7
+ # @param token [String] the mandatory token value to set, often (but not necessarily) a JWS
8
+ # compact serialization as per https://tools.ietf.org/html/rfc7515#section-3.1.
9
+ # @param lifetime_ms [Integer] when the token expires, in terms of the number of milliseconds
10
+ # since the epoch. See https://currentmillis.com/.
9
11
  # @param principal_name [String] the mandatory Kafka principal name associated with the token.
10
- # @param extensions [Hash] optional SASL extensions key-value pairs to be communicated to the broker as additional key-value pairs during the initial client response as per https://tools.ietf.org/html/rfc7628#section-3.1.
12
+ # @param extensions [Hash] optional SASL extensions key-value pairs to be communicated to the
13
+ # broker as additional key-value pairs during the initial client response as per
14
+ # https://tools.ietf.org/html/rfc7628#section-3.1.
11
15
  # @return [Integer] 0 on success
12
16
  def oauthbearer_set_token(token:, lifetime_ms:, principal_name:, extensions: nil)
13
17
  error_buffer = FFI::MemoryPointer.from_string(" " * 256)
@@ -49,7 +53,8 @@ module Rdkafka
49
53
  # Convert extensions hash to FFI::MemoryPointer (`const char **`).
50
54
  #
51
55
  # @param extensions [Hash, nil] extension key-value pairs
52
- # @return [Array<FFI::MemoryPointer, Array<FFI::MemoryPointer>>] array pointer and string pointers
56
+ # @return [Array<FFI::MemoryPointer, Array<FFI::MemoryPointer>>] array pointer and string
57
+ # pointers
53
58
  # @note The returned pointers must be freed manually (autorelease = false).
54
59
  def map_extensions(extensions)
55
60
  return [nil, nil] if extensions.nil? || extensions.empty?
@@ -80,7 +85,7 @@ module Rdkafka
80
85
  #
81
86
  # @param extensions [Hash, nil] extension key-value pairs
82
87
  # @return [Integer] non-negative even number representing keys + values count
83
- # @see https://github.com/confluentinc/librdkafka/blob/master/src/rdkafka_sasl_oauthbearer.c#L327-L347
88
+ # @see github.com/confluentinc/librdkafka/blob/master/src/rdkafka_sasl_oauthbearer.c#L327-L347
84
89
  def extension_size(extensions)
85
90
  return 0 unless extensions
86
91
  extensions.size * 2
@@ -13,6 +13,14 @@ module Rdkafka
13
13
  def initialize(inner, run_polling_thread:, opaque:, auto_start: true, timeout_ms: Defaults::NATIVE_KAFKA_POLL_TIMEOUT_MS)
14
14
  @inner = inner
15
15
  @opaque = opaque
16
+ # Process that owns `@inner`. librdkafka is not fork-safe: `fork` copies only the calling
17
+ # thread, so the background/broker threads backing this handle do not exist in a child
18
+ # process. An inherited handle therefore must not be polled or destroyed in the child -
19
+ # `rd_kafka_destroy` would walk thread state that no longer exists (segfault) and the
20
+ # inherited mutexes may have been copied in a locked state (deadlock). We record the creator
21
+ # pid so a forked child can recognise an inherited handle and leave its teardown to the
22
+ # parent, which still owns the running threads.
23
+ @creator_pid = Process.pid
16
24
  # Lock around external access
17
25
  @access_mutex = Mutex.new
18
26
  # Lock around internal polling
@@ -115,17 +123,21 @@ module Rdkafka
115
123
  end
116
124
 
117
125
  # Returns whether this native Kafka handle is closed or closing
126
+ #
127
+ # A handle inherited across `fork` is reported as closed in the child: it belongs to another
128
+ # process whose threads back the native client, so it is not usable here and must not be
129
+ # destroyed here (see the `@creator_pid` note in `#initialize`). This makes every `#close`
130
+ # path - including the GC finalizers that run during a child's exit - skip the native teardown
131
+ # for inherited handles, which is what would otherwise segfault the child.
132
+ #
118
133
  # @return [Boolean] true if closed or closing
119
134
  def closed?
120
- @closing || @inner.nil?
135
+ @closing || @inner.nil? || @creator_pid != Process.pid
121
136
  end
122
137
 
123
138
  # Enable IO event notifications on the main queue
124
139
  # Librdkafka will write to your FD when the queue transitions from empty to non-empty
125
140
  #
126
- # @note This method is incompatible with background polling threads.
127
- # If background polling is enabled, use manual polling instead (e.g., consumer.poll)
128
- #
129
141
  # @param fd [Integer] your file descriptor (from IO.pipe or eventfd)
130
142
  # @param payload [String] data to write to fd when queue has data (default: "\x01")
131
143
  # @return [nil]
@@ -142,6 +154,8 @@ module Rdkafka
142
154
  # if readable
143
155
  # consumer.poll(0) # Get messages
144
156
  # end
157
+ # @note This method is incompatible with background polling threads.
158
+ # If background polling is enabled, use manual polling instead (e.g., consumer.poll)
145
159
  def enable_main_queue_io_events(fd, payload = "\x01")
146
160
  if @run_polling_thread
147
161
  raise "Cannot enable IO events while background polling thread is active. " \
@@ -159,14 +173,13 @@ module Rdkafka
159
173
  # Enable IO event notifications on the background queue
160
174
  # Librdkafka will write to your FD when the background queue transitions from empty to non-empty
161
175
  #
162
- # @note This method is incompatible with background polling threads.
163
- # If background polling is enabled, use manual polling instead (e.g., consumer.poll)
164
- #
165
176
  # @param fd [Integer] your file descriptor (from IO.pipe or eventfd)
166
177
  # @param payload [String] data to write to fd when queue has data (default: "\x01")
167
178
  # @return [nil]
168
179
  # @raise [ClosedInnerError] when the handle is closed
169
180
  # @raise [RuntimeError] when background polling thread is active
181
+ # @note This method is incompatible with background polling threads.
182
+ # If background polling is enabled, use manual polling instead (e.g., consumer.poll)
170
183
  def enable_background_queue_io_events(fd, payload = "\x01")
171
184
  if @run_polling_thread
172
185
  raise "Cannot enable IO events while background polling thread is active. " \
@@ -2,8 +2,7 @@
2
2
 
3
3
  module Rdkafka
4
4
  class Producer
5
- # Handle to wait for a delivery report which is returned when
6
- # producing a message.
5
+ # Handle to wait for a delivery report which is returned when producing a message.
7
6
  class DeliveryHandle < Rdkafka::AbstractHandle
8
7
  layout :pending, :bool,
9
8
  :response, :int,