waterdrop 2.10.8 → 2.10.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7681d5f6c3333372a6349ddd24a1e1214a3474135755f918aa554064689976be
4
- data.tar.gz: 2f9d99fff24a56acaf5575e539d647682b305f303206a75bbf10b128dd8112c8
3
+ metadata.gz: 0e6efd47232839d14e444c45df4cc5fae39661dd7e8ef646639f00d41f62d3b3
4
+ data.tar.gz: edd8560d05bb3742d8592a1b90f2dc784410e7150045e8a8d4a0ebf1c0d82e81
5
5
  SHA512:
6
- metadata.gz: 63fcee865ef2c371bea98931f0b0ae1b5a9c118462acfb56746195547201741bba4cbdc6b6efc5feb1ec100e3c65fea8e6879da5774668f4d83691f4865cebe6
7
- data.tar.gz: dc4c8e69e8ed7e6312a0aa4b4ccd7eeec3d233113b2b6b80b4308dd6602e40a7a55c3a29842ef80e258d850b3ad93482be5b68f328feebccc36d6809e5509092
6
+ metadata.gz: 1532bf1ecbb02ec192bdd8a410b5e7e7579337e11515fe828fb88bd8cf2a2168e31ef669e9d3ba1c334e64720c148069d26b5259d7c3f913d80258d5ddbb711b
7
+ data.tar.gz: 87178e647ac42309e0cdbef6c48af8c0f7a902ee32238c3c5cf8cbca2ee7ae80396895bb3a70417c912f98f669834e19ce1066c596a7a5432c7245b29a537248
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.7
1
+ 4.0.9
data/CHANGELOG.md CHANGED
@@ -1,13 +1,23 @@
1
1
  # WaterDrop changelog
2
2
 
3
+ ## 2.10.9 (2026-11-02)
4
+ - [Maintenance] Rename the positional topic name parameters of `Producer#partition_count`, `Clients::Buffered#messages_for` and the Datadog metrics listener to `topic_name`.
5
+ - [Maintenance] Rename the positional topic name parameter of `Clients::Dummy::Handle#initialize` to `topic_name`.
6
+ - [Maintenance] Declare `rake` in the `Gemfile`. It came only through `karafka-rdkafka`, which will stop depending on it for precompiled platform gems.
7
+ - [Fix] Copy the buffer in `Producer#messages` (and so the `buffer.flushed_async`/`buffer.flushed_sync` payloads) under a dedicated lock, so reading it while other threads buffer no longer raises `ConcurrencyError` on JRuby.
8
+ - [Fix] Re-buffer the undispatched rest of a `#flush` batch when a concurrent `#close` stops it, and expose the handles dispatched before the close as `ProducerClosedError#dispatched`.
9
+ - [Fix] Copy the buffer in `Producer#messages` (and so the `buffer.flushed_async`/`buffer.flushed_sync` payloads) under a dedicated lock, so reading it while other threads buffer no longer raises `ConcurrencyError` on JRuby.
10
+
3
11
  ## 2.10.8 (2026-10-25)
4
12
  - [Maintenance] Document that a transaction timeout also raises `:fenced`, indistinguishable from a takeover, and when removing `:fenced` from `non_reloadable_errors` is safe.
13
+ - [Maintenance] Drop unused `byebug` from the test bundle.
14
+ - [Maintenance] Fix test races and skip the fork test on JRuby, resolving unstable behavior in `test/` only.
5
15
  - [Fix] Stop `Producer#close` from raising `Errno::EPIPE` on JRuby when its close signal races the poller closing the pipe.
6
- - [Fix] Abort a failed transaction only once: raise an abortable commit error as is instead of a follow-up `Operation not valid in state Ready` error, and do not abort after a failed `begin_transaction`.
16
+ - [Fix] Abort a failed transaction only once: raise the abortable commit error instead of a follow-up `Operation not valid in state Ready` error, and do not abort after a failed `begin_transaction`.
7
17
 
8
18
  ## 2.10.7 (2026-10-19)
9
19
  - [Change] Require `karafka-rdkafka` `>=` `0.31.0` so delivery reports carry the message persistence status, latency and broker id.
10
- - [Maintenance] Stop the transaction purge instrumentation spec from flaking when a slow runner lets librdkafka deliver the message before the abort, which leaves nothing to purge.
20
+ - [Maintenance] Fix a flaky transaction purge instrumentation spec on slow runners.
11
21
 
12
22
  ## 2.10.6 (2026-10-12)
13
23
  - **[EOL]** Drop Ruby 3.2 support; require Ruby 3.3+.
@@ -20,10 +30,10 @@
20
30
  - [Fix] Restore both buffers when `#flush` fails before anything is dispatched, instead of discarding the whole batch.
21
31
 
22
32
  ## 2.10.4 (2026-08-26)
33
+ - [Maintenance] Stop the `#partition_count when topic does not exist` spec from flaking on slow CI runners by waiting for authoritative broker metadata before asserting the count.
23
34
  - [Fix] Avoid a `FrozenError` on `Producer#close` when the producer is configured with a frozen string id (for example a frozen string literal, `config.id = "rspec"`).
24
35
  - [Fix] Emit `connection_pool.shutdown` only once when shutting down the global connection pool.
25
36
  - [Fix] Retry a produce that races a concurrent idempotent fatal-error reload instead of leaking a raw `Rdkafka::ClosedProducerError`/`Rdkafka::ClosedInnerError`.
26
- - [Maintenance] Stop the `#partition_count when topic does not exist` spec from flaking on slow CI runners by waiting for authoritative broker metadata before asserting the count.
27
37
 
28
38
  ## 2.10.3 (2026-07-15)
29
39
  - [Feature] Add `wait_timeout_on_transaction_abort` (disabled by default) to avoid a fatal `INVALID_TXN_STATE` on abort ([librdkafka#4849](https://github.com/confluentinc/librdkafka/issues/4849)). When enabled, the awaited message is delivered as aborted instead of purged.
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem "rake"
7
8
  gem "zeitwerk", "~> 2.8.0"
8
9
 
9
10
  group :test do
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.10.8)
4
+ waterdrop (2.10.9)
5
5
  karafka-core (>= 2.5.12, < 3.0.0)
6
6
  karafka-rdkafka (>= 0.31.0)
7
7
  zeitwerk (~> 2.3)
@@ -45,6 +45,7 @@ DEPENDENCIES
45
45
  minitest
46
46
  mocha
47
47
  ostruct
48
+ rake
48
49
  simplecov
49
50
  warning
50
51
  waterdrop!
@@ -67,7 +68,7 @@ CHECKSUMS
67
68
  ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
68
69
  simplecov (1.3.2) sha256=2a22ec1759f1b8db60881a2e038758956fc69ef93ed669be0bde0678de6d0e85
69
70
  warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e
70
- waterdrop (2.10.8)
71
+ waterdrop (2.10.9)
71
72
  zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
72
73
 
73
74
  BUNDLED WITH
@@ -75,9 +75,9 @@ module WaterDrop
75
75
  end
76
76
 
77
77
  # Returns messages produced to a given topic
78
- # @param topic [String]
79
- def messages_for(topic)
80
- @topics[topic]
78
+ # @param topic_name [String]
79
+ def messages_for(topic_name)
80
+ @topics[topic_name]
81
81
  end
82
82
 
83
83
  # Clears internal buffer. Used in between specs so messages do not leak out
@@ -10,11 +10,11 @@ module WaterDrop
10
10
  class Dummy
11
11
  # `::Rdkafka::Producer::DeliveryHandle` object API compatible dummy object
12
12
  class Handle < ::Rdkafka::Producer::DeliveryHandle
13
- # @param topic [String] topic where we want to dispatch message
13
+ # @param topic_name [String] topic where we want to dispatch message
14
14
  # @param partition [Integer] target partition
15
15
  # @param offset [Integer] offset assigned by our fake "Kafka"
16
- def initialize(topic, partition, offset)
17
- @topic = topic
16
+ def initialize(topic_name, partition, offset)
17
+ @topic = topic_name
18
18
  @partition = partition
19
19
  @offset = offset
20
20
  end
@@ -23,7 +23,17 @@ module WaterDrop
23
23
  ProducerUsedInParentProcess = Class.new(BaseError)
24
24
 
25
25
  # Raised when there was an attempt to use a closed producer
26
- ProducerClosedError = Class.new(BaseError)
26
+ class ProducerClosedError < BaseError
27
+ attr_reader :dispatched
28
+
29
+ # @param message [String, nil] error message
30
+ # @param dispatched [Array<Rdkafka::Producer::DeliveryHandle>] handlers of the messages of a
31
+ # batch that we've dispatched before the producer got closed. Empty outside of a batch.
32
+ def initialize(message = nil, dispatched: [])
33
+ super(message)
34
+ @dispatched = dispatched
35
+ end
36
+ end
27
37
 
28
38
  # Raised if you attempt to close the producer from within a transaction. This is not allowed.
29
39
  ProducerTransactionalCloseAttemptError = Class.new(BaseError)
@@ -190,10 +190,10 @@ module WaterDrop
190
190
  end
191
191
 
192
192
  # Report that a message has been produced to a topic.
193
- # @param topic [String] Kafka topic
193
+ # @param topic_name [String] Kafka topic
194
194
  # @param method_name [Symbol] method from which this message operation comes
195
- def report_message(topic, method_name)
196
- increment(method_name, tags: default_tags + ["topic:#{topic}"])
195
+ def report_message(topic_name, method_name)
196
+ increment(method_name, tags: default_tags + ["topic:#{topic_name}"])
197
197
  end
198
198
 
199
199
  # Wraps metric name in listener's namespace
@@ -53,6 +53,8 @@ module WaterDrop
53
53
  # @raise [Rdkafka::RdkafkaError] When adding the messages to rdkafka's queue failed
54
54
  # @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
55
55
  # and the message could not be sent to Kafka
56
+ # @raise [Errors::ProducerClosedError] When the producer got closed mid-batch. Its
57
+ # `#dispatched` holds the handlers of the messages dispatched before that
56
58
  def produce_many_async(messages, run_middleware: true)
57
59
  dispatched = []
58
60
  messages = middleware.run_many(messages) if run_middleware
@@ -84,6 +86,8 @@ module WaterDrop
84
86
  )
85
87
 
86
88
  raise re_raised
89
+ rescue Errors::ProducerClosedError => e
90
+ raise Errors::ProducerClosedError.new(e.message, dispatched: dispatched)
87
91
  end
88
92
  end
89
93
  end
@@ -28,7 +28,7 @@ module WaterDrop
28
28
  ) do
29
29
  @buffer_mutex.synchronize do
30
30
  ensure_active!
31
- @messages << message
31
+ @messages_mutex.synchronize { @messages << message }
32
32
  end
33
33
  end
34
34
  end
@@ -55,7 +55,7 @@ module WaterDrop
55
55
  ) do
56
56
  @buffer_mutex.synchronize do
57
57
  ensure_active!
58
- @messages.concat(messages)
58
+ @messages_mutex.synchronize { @messages.concat(messages) }
59
59
  end
60
60
 
61
61
  messages
@@ -91,6 +91,7 @@ module WaterDrop
91
91
  # @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles (in final state for
92
92
  # sync, pending for async)
93
93
  # @raise [Errors::ProduceManyError] when there was a failure in flushing
94
+ # @raise [Errors::ProducerClosedError] when the producer got closed during flushing
94
95
  # @note We use this method underneath to provide a different instrumentation for sync and
95
96
  # async flushing within the public API
96
97
  def flush(sync)
@@ -99,10 +100,12 @@ module WaterDrop
99
100
  data_for_dispatch = []
100
101
 
101
102
  @buffer_mutex.synchronize do
102
- requeued = @requeued
103
- @requeued = []
104
- fresh = @messages
105
- @messages = []
103
+ @messages_mutex.synchronize do
104
+ requeued = @requeued
105
+ @requeued = []
106
+ fresh = @messages
107
+ @messages = []
108
+ end
106
109
  end
107
110
 
108
111
  # Middleware runs exactly once per message: on the fresh messages here, never again on the
@@ -141,12 +144,13 @@ module WaterDrop
141
144
  else
142
145
  produce_many_async(data_for_dispatch, run_middleware: false)
143
146
  end
144
- rescue Errors::ProduceManyError => e
145
- # A dispatch failed partway through the batch. Re-buffer the messages that never reached
146
- # librdkafka so a partial failure does not silently drop valid buffered messages. For a
147
- # transactional producer the whole batch is rolled back (nothing is visible to consumers),
148
- # so all of it is restored; for a regular producer `e.dispatched` holds the handles already
149
- # created, so only the remainder is restored.
147
+ rescue Errors::ProduceManyError, Errors::ProducerClosedError => e
148
+ # A dispatch failed partway through the batch, either on an inline error or because a
149
+ # concurrent close stopped it. Re-buffer the messages that never reached librdkafka so a
150
+ # partial failure does not silently drop valid buffered messages. For a transactional
151
+ # producer the whole batch is rolled back (nothing is visible to consumers), so all of it is
152
+ # restored; for a regular producer `e.dispatched` holds the handles already created, so only
153
+ # the remainder is restored.
150
154
  requeue_unflushed(transactional? ? data_for_dispatch : data_for_dispatch.drop(e.dispatched.size))
151
155
 
152
156
  raise
@@ -166,8 +170,10 @@ module WaterDrop
166
170
  # @param requeued [Array<Hash>] already middleware-processed messages
167
171
  def restore_undispatched(fresh, requeued)
168
172
  @buffer_mutex.synchronize do
169
- @messages.unshift(*fresh)
170
- @requeued.unshift(*requeued)
173
+ @messages_mutex.synchronize do
174
+ @messages.unshift(*fresh)
175
+ @requeued.unshift(*requeued)
176
+ end
171
177
  end
172
178
  end
173
179
 
@@ -178,7 +184,9 @@ module WaterDrop
178
184
  def requeue_unflushed(messages)
179
185
  return if messages.empty?
180
186
 
181
- @buffer_mutex.synchronize { @requeued.unshift(*messages) }
187
+ @buffer_mutex.synchronize do
188
+ @messages_mutex.synchronize { @requeued.unshift(*messages) }
189
+ end
182
190
  end
183
191
  end
184
192
  end
@@ -60,6 +60,8 @@ module WaterDrop
60
60
  # handles are still pending
61
61
  # @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
62
62
  # and the message could not be sent to Kafka
63
+ # @raise [Errors::ProducerClosedError] When the producer got closed mid-batch. Its
64
+ # `#dispatched` holds the handlers of the messages dispatched before that
63
65
  #
64
66
  # @note We return delivery handles instead of delivery reports to ensure consistent return
65
67
  # types for both success and error flows. In case of partial failures (e.g., mid-batch
@@ -67,10 +69,10 @@ module WaterDrop
67
69
  # allowing uniform error handling. Each handle is in its final state after this method
68
70
  # returns, so you can call `handle.create_result` to obtain the delivery report if needed.
69
71
  def produce_many_sync(messages, run_middleware: true)
72
+ dispatched = []
70
73
  messages = middleware.run_many(messages) if run_middleware
71
74
  messages.each { |message| validate_message!(message) }
72
75
 
73
- dispatched = []
74
76
  inline_error = nil
75
77
 
76
78
  @monitor.instrument("messages.produced_sync", producer_id: id, messages: messages) do
@@ -128,6 +130,8 @@ module WaterDrop
128
130
  )
129
131
 
130
132
  raise re_raised
133
+ rescue Errors::ProducerClosedError => e
134
+ raise Errors::ProducerClosedError.new(e.message, dispatched: dispatched)
131
135
  end
132
136
  end
133
137
  end
@@ -50,6 +50,11 @@ module WaterDrop
50
50
  def initialize(&block)
51
51
  @operations_in_progress = Helpers::Counter.new
52
52
  @buffer_mutex = Mutex.new
53
+ # Guards only the in-place changes of @messages and @requeued and the copy in #messages. It
54
+ # is always taken inside @buffer_mutex and held for one array operation, never around
55
+ # callbacks, so #messages does not wait for a @buffer_mutex holder (e.g. #disconnect waiting
56
+ # on the poller thread whose callbacks may call #messages).
57
+ @messages_mutex = Mutex.new
53
58
  @connecting_mutex = Mutex.new
54
59
  @operating_mutex = Mutex.new
55
60
  @transaction_mutex = Mutex.new
@@ -182,6 +187,12 @@ module WaterDrop
182
187
  # @return [Array] internal messages buffer awaiting a flush. Includes both freshly buffered
183
188
  # messages and any messages re-buffered after a failed flush that are waiting to be retried.
184
189
  def messages
190
+ @messages_mutex.synchronize { @requeued + @messages }
191
+ rescue ThreadError => e
192
+ # Mutex#synchronize raises in a signal trap, so there the copy is taken without the lock.
193
+ # It is not safe against concurrent buffering without a GVL (JRuby).
194
+ raise unless e.message == "can't be called from trap context"
195
+
185
196
  @requeued + @messages
186
197
  end
187
198
 
@@ -222,13 +233,13 @@ module WaterDrop
222
233
 
223
234
  # Fetches and caches the partition count of a topic
224
235
  #
225
- # @param topic [String] topic for which we want to get the number of partitions
236
+ # @param topic_name [String] topic for which we want to get the number of partitions
226
237
  # @return [Integer] number of partitions of the requested topic or -1 if number could not be
227
238
  # retrieved.
228
239
  #
229
240
  # @note It uses the underlying `rdkafka-ruby` partition count fetch and cache.
230
- def partition_count(topic)
231
- client.partition_count(topic.to_s)
241
+ def partition_count(topic_name)
242
+ client.partition_count(topic_name.to_s)
232
243
  end
233
244
 
234
245
  # Purges data from both the buffer queue as well as the librdkafka queue.
@@ -239,8 +250,10 @@ module WaterDrop
239
250
  def purge
240
251
  @monitor.instrument("buffer.purged", producer_id: id) do
241
252
  @buffer_mutex.synchronize do
242
- @messages = []
243
- @requeued = []
253
+ @messages_mutex.synchronize do
254
+ @messages = []
255
+ @requeued = []
256
+ end
244
257
  end
245
258
 
246
259
  # We should not purge if there is no client initialized
@@ -2,5 +2,5 @@
2
2
 
3
3
  module WaterDrop
4
4
  # Current WaterDrop version
5
- VERSION = "2.10.8"
5
+ VERSION = "2.10.9"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waterdrop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.10.8
4
+ version: 2.10.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld