rdkafka 0.28.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +3 -2
  4. data/bin/verify_kafka_warnings +5 -0
  5. data/docker-compose-ssl.yml +4 -1
  6. data/docker-compose.yml +4 -1
  7. data/ext/librdkafka.so +0 -0
  8. data/lib/rdkafka/abstract_handle.rb +31 -2
  9. data/lib/rdkafka/admin/config_binding_result.rb +2 -2
  10. data/lib/rdkafka/admin/create_acl_handle.rb +6 -5
  11. data/lib/rdkafka/admin/create_acl_report.rb +2 -1
  12. data/lib/rdkafka/admin/create_partitions_handle.rb +4 -6
  13. data/lib/rdkafka/admin/create_topic_handle.rb +4 -6
  14. data/lib/rdkafka/admin/delete_acl_handle.rb +5 -7
  15. data/lib/rdkafka/admin/delete_groups_handle.rb +4 -6
  16. data/lib/rdkafka/admin/delete_records_handle.rb +31 -0
  17. data/lib/rdkafka/admin/delete_records_report.rb +25 -0
  18. data/lib/rdkafka/admin/delete_topic_handle.rb +4 -6
  19. data/lib/rdkafka/admin/describe_acl_handle.rb +5 -7
  20. data/lib/rdkafka/admin/describe_acl_report.rb +2 -1
  21. data/lib/rdkafka/admin/describe_configs_handle.rb +4 -10
  22. data/lib/rdkafka/admin/describe_configs_report.rb +1 -5
  23. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +4 -10
  24. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +1 -5
  25. data/lib/rdkafka/admin/list_consumer_groups_handle.rb +31 -0
  26. data/lib/rdkafka/admin/list_consumer_groups_report.rb +83 -0
  27. data/lib/rdkafka/admin/list_offsets_handle.rb +5 -10
  28. data/lib/rdkafka/admin/list_offsets_report.rb +5 -2
  29. data/lib/rdkafka/admin.rb +238 -145
  30. data/lib/rdkafka/bindings.rb +61 -16
  31. data/lib/rdkafka/callbacks/base_handler.rb +62 -0
  32. data/lib/rdkafka/callbacks/create_acl_handler.rb +37 -0
  33. data/lib/rdkafka/callbacks/create_partitions_handler.rb +37 -0
  34. data/lib/rdkafka/callbacks/create_topic_handler.rb +37 -0
  35. data/lib/rdkafka/callbacks/delete_acl_handler.rb +42 -0
  36. data/lib/rdkafka/callbacks/delete_groups_handler.rb +37 -0
  37. data/lib/rdkafka/callbacks/delete_records_handler.rb +38 -0
  38. data/lib/rdkafka/callbacks/delete_topic_handler.rb +37 -0
  39. data/lib/rdkafka/callbacks/describe_acl_handler.rb +35 -0
  40. data/lib/rdkafka/callbacks/describe_configs_handler.rb +42 -0
  41. data/lib/rdkafka/callbacks/incremental_alter_configs_handler.rb +42 -0
  42. data/lib/rdkafka/callbacks/list_consumer_groups_handler.rb +39 -0
  43. data/lib/rdkafka/callbacks/list_offsets_handler.rb +42 -0
  44. data/lib/rdkafka/callbacks.rb +54 -246
  45. data/lib/rdkafka/config.rb +69 -50
  46. data/lib/rdkafka/consumer/headers.rb +19 -5
  47. data/lib/rdkafka/consumer/partition.rb +8 -1
  48. data/lib/rdkafka/consumer/topic_partition_list.rb +58 -30
  49. data/lib/rdkafka/consumer.rb +211 -52
  50. data/lib/rdkafka/defaults.rb +24 -7
  51. data/lib/rdkafka/helpers/list_offsets.rb +127 -0
  52. data/lib/rdkafka/helpers/metadata.rb +29 -0
  53. data/lib/rdkafka/helpers/oauth.rb +10 -5
  54. data/lib/rdkafka/metadata.rb +86 -19
  55. data/lib/rdkafka/native_kafka.rb +22 -11
  56. data/lib/rdkafka/producer/delivery_handle.rb +4 -5
  57. data/lib/rdkafka/producer/partitions_count_cache.rb +24 -38
  58. data/lib/rdkafka/producer.rb +81 -61
  59. data/lib/rdkafka/version.rb +3 -3
  60. data/lib/rdkafka.rb +19 -0
  61. data/package-lock.json +3 -3
  62. data/rdkafka.gemspec +1 -0
  63. metadata +21 -5
  64. data/Gemfile +0 -13
  65. data/Gemfile.lint +0 -14
  66. data/Gemfile.lint.lock +0 -123
@@ -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
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ module Helpers
5
+ # Shared `#metadata` implementation for Admin, Consumer and Producer.
6
+ #
7
+ # `rd_kafka_metadata()` is handle-agnostic in librdkafka - it works on any `rd_kafka_t`
8
+ # (consumer, producer or admin) - so this Ruby-level implementation is identical across all
9
+ # three client types. Includers must provide a private `#closed_check(method)` that raises
10
+ # their own `Closed*Error`.
11
+ module Metadata
12
+ # Performs the metadata request using this client
13
+ #
14
+ # @param topic_name [String, nil] metadata about particular topic or all if nil
15
+ # @param timeout_ms [Integer] metadata request timeout
16
+ # @return [Rdkafka::Metadata] requested metadata
17
+ def metadata(topic_name = nil, timeout_ms = Defaults::METADATA_TIMEOUT_MS)
18
+ closed_check(__method__)
19
+
20
+ @native_kafka.with_inner do |inner|
21
+ # Must stay fully qualified: this module is itself named `Metadata`, so a bare
22
+ # `Metadata.new` here would resolve to `Rdkafka::Helpers::Metadata` (no `.new`) instead
23
+ # of this class.
24
+ Rdkafka::Metadata.new(inner, topic_name, timeout_ms)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ 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
@@ -23,12 +23,57 @@ module Rdkafka
23
23
  # @param timeout_ms [Integer] timeout in milliseconds
24
24
  # @raise [RdkafkaError] when metadata fetch fails
25
25
  def initialize(native_client, topic_name = nil, timeout_ms = Defaults::METADATA_TIMEOUT_MS)
26
- attempt ||= 0
27
- attempt += 1
28
-
29
- native_topic = if topic_name
30
- Rdkafka::Bindings.rd_kafka_topic_new(native_client, topic_name, nil)
26
+ attempt = 0
27
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) +
28
+ Defaults::METADATA_RETRY_BUDGET_MS / 1_000.0
29
+
30
+ begin
31
+ attempt += 1
32
+ fetch_metadata(native_client, topic_name, timeout_ms)
33
+ rescue ::Rdkafka::RdkafkaError => e
34
+ raise unless RETRIED_ERRORS.include?(e.code)
35
+ raise if attempt > Defaults::METADATA_MAX_RETRIES
36
+
37
+ # Stop once the wall-clock retry budget is spent, but only after at least
38
+ # METADATA_MIN_ATTEMPTS tries so a slow broker (whose requests each consume the full
39
+ # timeout) still gets a few tries rather than being cut off after one or two.
40
+ raise if attempt >= Defaults::METADATA_MIN_ATTEMPTS &&
41
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) >= deadline
42
+
43
+ # Exponential backoff between attempts, capped so a long retry sequence cannot block for
44
+ # minutes. The request timeout (`timeout_ms`) is intentionally left unchanged: it used to be
45
+ # overwritten with the backoff value, which shrank the first retries below the configured
46
+ # timeout (near-guaranteeing another timeout) and then inflated later ones to ~100s.
47
+ backoff_ms = [
48
+ (2**attempt) * Defaults::METADATA_RETRY_BACKOFF_BASE_MS,
49
+ Defaults::METADATA_RETRY_BACKOFF_MAX_MS
50
+ ].min
51
+
52
+ sleep(backoff_ms / 1_000.0)
53
+
54
+ retry
31
55
  end
56
+ end
57
+
58
+ private
59
+
60
+ # Performs a single metadata fetch attempt, freeing this attempt's native resources.
61
+ #
62
+ # Kept separate from {#initialize} so each retried attempt frees its own `native_topic` and
63
+ # metadata struct. `retry` re-enters `initialize`'s `begin` without running an `ensure` placed
64
+ # there, so doing the cleanup per attempt here is what prevents the per-retry native leak. The
65
+ # metadata struct is only allocated on success, so it is read and destroyed only after the
66
+ # result has been validated (avoids destroying a NULL/garbage pointer on a failed fetch).
67
+ #
68
+ # @param native_client [FFI::Pointer] pointer to the native Kafka client
69
+ # @param topic_name [String, nil] specific topic to fetch metadata for, or nil for all topics
70
+ # @param timeout_ms [Integer] timeout in milliseconds
71
+ # @raise [RdkafkaError] when the metadata fetch fails
72
+ def fetch_metadata(native_client, topic_name, timeout_ms)
73
+ native_topic = nil
74
+ metadata_ptr = nil
75
+
76
+ native_topic = Rdkafka::Bindings.rd_kafka_topic_new(native_client, topic_name, nil) if topic_name
32
77
 
33
78
  ptr = FFI::MemoryPointer.new(:pointer)
34
79
 
@@ -42,24 +87,16 @@ module Rdkafka
42
87
  # Error Handling
43
88
  raise Rdkafka::RdkafkaError.new(result) unless result.zero?
44
89
 
45
- metadata_from_native(ptr.read_pointer)
46
- rescue ::Rdkafka::RdkafkaError => e
47
- raise unless RETRIED_ERRORS.include?(e.code)
48
- raise if attempt > Defaults::METADATA_MAX_RETRIES
90
+ # rd_kafka_metadata only allocates the struct on success, so we read the pointer to destroy
91
+ # only after the result has been confirmed successful.
92
+ metadata_ptr = ptr.read_pointer
49
93
 
50
- backoff_factor = 2**attempt
51
- timeout_ms = backoff_factor * Defaults::METADATA_RETRY_BACKOFF_BASE_MS
52
-
53
- sleep(timeout_ms / 1000.0)
54
-
55
- retry
94
+ metadata_from_native(metadata_ptr)
56
95
  ensure
57
- Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if topic_name
58
- Rdkafka::Bindings.rd_kafka_metadata_destroy(ptr.read_pointer)
96
+ Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic) if native_topic
97
+ Rdkafka::Bindings.rd_kafka_metadata_destroy(metadata_ptr) if metadata_ptr && !metadata_ptr.null?
59
98
  end
60
99
 
61
- private
62
-
63
100
  # Extracts metadata from native pointer
64
101
  # @param ptr [FFI::Pointer] pointer to native metadata
65
102
  def metadata_from_native(ptr)
@@ -134,6 +171,36 @@ module Rdkafka
134
171
  :replicas, :pointer,
135
172
  :in_sync_replica_brokers, :int,
136
173
  :isrs, :pointer
174
+
175
+ # The base `#to_h` skips FFI pointer members, which would drop the replica and in-sync
176
+ # replica assignments entirely. We dereference those pointers here so the partition hash
177
+ # exposes the broker ids backing the partition (needed e.g. to plan replication changes).
178
+ #
179
+ # @return [Hash{Symbol => Integer, Array<Integer>}] partition metadata:
180
+ # * +:partition_id+ (Integer) - partition id
181
+ # * +:leader+ (Integer) - broker id of the partition leader
182
+ # * +:replica_count+ (Integer) - number of assigned replicas
183
+ # * +:in_sync_replica_brokers+ (Integer) - number of in-sync replicas
184
+ # * +:replicas+ (Array<Integer>) - broker ids of the assigned replicas
185
+ # * +:isrs+ (Array<Integer>) - broker ids of the in-sync replicas
186
+ def to_h
187
+ super.merge(
188
+ replicas: read_broker_ids(self[:replicas], self[:replica_count]),
189
+ isrs: read_broker_ids(self[:isrs], self[:in_sync_replica_brokers])
190
+ )
191
+ end
192
+
193
+ private
194
+
195
+ # Reads `count` broker ids (int32) from a replicas/isrs pointer.
196
+ # @param pointer [FFI::Pointer] pointer to the broker ids array
197
+ # @param count [Integer] number of broker ids to read
198
+ # @return [Array<Integer>] broker ids (empty when there are none)
199
+ def read_broker_ids(pointer, count)
200
+ return [] if count.zero? || pointer.null?
201
+
202
+ pointer.read_array_of_int32(count)
203
+ end
137
204
  end
138
205
  end
139
206
  end
@@ -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
@@ -55,8 +63,7 @@ module Rdkafka
55
63
  Rdkafka::Bindings.rd_kafka_poll(@inner, 0)
56
64
 
57
65
  if @run_polling_thread
58
- # Start thread to poll client for delivery callbacks,
59
- # not used in consumer.
66
+ # Start thread to poll client for delivery callbacks, not used in consumer.
60
67
  @polling_thread = Thread.new do
61
68
  loop do
62
69
  @poll_mutex.synchronize do
@@ -116,17 +123,21 @@ module Rdkafka
116
123
  end
117
124
 
118
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
+ #
119
133
  # @return [Boolean] true if closed or closing
120
134
  def closed?
121
- @closing || @inner.nil?
135
+ @closing || @inner.nil? || @creator_pid != Process.pid
122
136
  end
123
137
 
124
138
  # Enable IO event notifications on the main queue
125
139
  # Librdkafka will write to your FD when the queue transitions from empty to non-empty
126
140
  #
127
- # @note This method is incompatible with background polling threads.
128
- # If background polling is enabled, use manual polling instead (e.g., consumer.poll)
129
- #
130
141
  # @param fd [Integer] your file descriptor (from IO.pipe or eventfd)
131
142
  # @param payload [String] data to write to fd when queue has data (default: "\x01")
132
143
  # @return [nil]
@@ -143,6 +154,8 @@ module Rdkafka
143
154
  # if readable
144
155
  # consumer.poll(0) # Get messages
145
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)
146
159
  def enable_main_queue_io_events(fd, payload = "\x01")
147
160
  if @run_polling_thread
148
161
  raise "Cannot enable IO events while background polling thread is active. " \
@@ -160,14 +173,13 @@ module Rdkafka
160
173
  # Enable IO event notifications on the background queue
161
174
  # Librdkafka will write to your FD when the background queue transitions from empty to non-empty
162
175
  #
163
- # @note This method is incompatible with background polling threads.
164
- # If background polling is enabled, use manual polling instead (e.g., consumer.poll)
165
- #
166
176
  # @param fd [Integer] your file descriptor (from IO.pipe or eventfd)
167
177
  # @param payload [String] data to write to fd when queue has data (default: "\x01")
168
178
  # @return [nil]
169
179
  # @raise [ClosedInnerError] when the handle is closed
170
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)
171
183
  def enable_background_queue_io_events(fd, payload = "\x01")
172
184
  if @run_polling_thread
173
185
  raise "Cannot enable IO events while background polling thread is active. " \
@@ -197,8 +209,7 @@ module Rdkafka
197
209
  # Indicate to polling thread that we're closing
198
210
  @polling_thread[:closing] = true
199
211
 
200
- # Wait for the polling thread to finish up,
201
- # this can be aborted in practice if this
212
+ # Wait for the polling thread to finish up, this can be aborted in practice if this
202
213
  # code runs from a finalizer.
203
214
  @polling_thread.join
204
215
  end
@@ -2,20 +2,19 @@
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,
10
9
  :partition, :int,
11
- :offset, :int64,
12
- :topic_name, :pointer
10
+ :offset, :int64
13
11
 
14
12
  # @return [Object, nil] label set during message production or nil by default
15
13
  attr_accessor :label
16
14
 
17
15
  # @return [String] topic where we are trying to send the message
18
- # We use this instead of reading from `topic_name` pointer to save on memory allocations
16
+ # Set in `#produce`, where the topic is known upfront. Keeping it as a Ruby attribute
17
+ # spares a per-message native string copy in the delivery callback.
19
18
  attr_accessor :topic
20
19
 
21
20
  # @return [String] the name of the operation (e.g. "delivery")
@@ -37,12 +37,11 @@ module Rdkafka
37
37
  # contention in multi-threaded environments while ensuring data consistency.
38
38
  #
39
39
  # 6. Topic recreation handling
40
- # If a topic is deleted and recreated with fewer partitions, the cache will continue to
41
- # report the higher count until either the TTL expires or the process is restarted. This
42
- # design choice simplifies the implementation while relying on librdkafka's error handling
43
- # for edge cases. In production environments, topic recreation with different partition
44
- # counts is typically accompanied by application restarts to handle structural changes.
45
- # This also aligns with the previous cache implementation.
40
+ # If a topic is deleted and recreated with fewer partitions, the cache keeps reporting the
41
+ # higher count only until the entry's TTL expires. The first refresh after expiry performs
42
+ # an authoritative metadata read and adopts the lower count. Within the TTL window a lower
43
+ # value is still ignored, so a transient or racy lower read cannot clobber a correct higher
44
+ # count.
46
45
  class PartitionsCountCache
47
46
  include Helpers::Time
48
47
 
@@ -91,28 +90,14 @@ module Rdkafka
91
90
  current_info = @counts[topic]
92
91
 
93
92
  if current_info.nil? || expired?(current_info[0])
93
+ # The cached entry is missing or expired, so the block performs an authoritative metadata
94
+ # read. We hand it to `set`, which adopts a higher count always and a lower count once the
95
+ # entry has expired (e.g. the topic was recreated with fewer partitions). We then return
96
+ # whatever `set` settled on so a concurrent refresh that wrote a higher value still wins.
94
97
  new_count = yield
98
+ set(topic, new_count)
95
99
 
96
- if current_info.nil?
97
- # No existing data, create a new entry with mutex
98
- set(topic, new_count)
99
-
100
- return new_count
101
- else
102
- current_count = current_info[1]
103
-
104
- if new_count > current_count
105
- # Higher value needs mutex to update both timestamp and count
106
- set(topic, new_count)
107
-
108
- return new_count
109
- else
110
- # Same or lower value, just update timestamp without mutex
111
- refresh_timestamp(topic)
112
-
113
- return current_count
114
- end
115
- end
100
+ return @counts[topic][1]
116
101
  end
117
102
 
118
103
  current_info[1]
@@ -133,8 +118,11 @@ module Rdkafka
133
118
  # First check outside mutex to avoid unnecessary locking
134
119
  current_info = @counts[topic]
135
120
 
136
- # For lower values, we don't update count but might need to refresh timestamp
137
- if current_info && new_count < current_info[1]
121
+ # Within the TTL window a lower value is treated as a stale/racy read and ignored, since
122
+ # partition counts only grow during normal operation. Once the entry has expired a lower
123
+ # value is an authoritative refresh (e.g. the topic was recreated with fewer partitions),
124
+ # so we fall through and adopt it below.
125
+ if current_info && new_count < current_info[1] && !expired?(current_info[0])
138
126
  refresh_timestamp(topic)
139
127
 
140
128
  return
@@ -148,17 +136,15 @@ module Rdkafka
148
136
  if current_info.nil?
149
137
  # Create new entry
150
138
  @counts[topic] = [monotonic_now_ms, new_count]
139
+ elsif new_count > current_info[1] || expired?(current_info[0])
140
+ # A higher count always wins; a lower count is accepted only when the existing entry
141
+ # has expired, so a concurrent fresh higher value (which reset the timestamp) is never
142
+ # clobbered by a stale lower one.
143
+ current_info[0] = monotonic_now_ms
144
+ current_info[1] = new_count
151
145
  else
152
- current_count = current_info[1]
153
-
154
- if new_count > current_count
155
- # Update to higher count value
156
- current_info[0] = monotonic_now_ms
157
- current_info[1] = new_count
158
- else
159
- # Same or lower count, update timestamp only
160
- current_info[0] = monotonic_now_ms
161
- end
146
+ # Same or lower count within the TTL window: refresh the timestamp only
147
+ current_info[0] = monotonic_now_ms
162
148
  end
163
149
  end
164
150
  end