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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +13 -3
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -2
- data/lib/waterdrop/clients/buffered.rb +3 -3
- data/lib/waterdrop/clients/dummy.rb +3 -3
- data/lib/waterdrop/errors.rb +11 -1
- data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +3 -3
- data/lib/waterdrop/producer/async.rb +4 -0
- data/lib/waterdrop/producer/buffer.rb +23 -15
- data/lib/waterdrop/producer/sync.rb +5 -1
- data/lib/waterdrop/producer.rb +18 -5
- data/lib/waterdrop/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e6efd47232839d14e444c45df4cc5fae39661dd7e8ef646639f00d41f62d3b3
|
|
4
|
+
data.tar.gz: edd8560d05bb3742d8592a1b90f2dc784410e7150045e8a8d4a0ebf1c0d82e81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1532bf1ecbb02ec192bdd8a410b5e7e7579337e11515fe828fb88bd8cf2a2168e31ef669e9d3ba1c334e64720c148069d26b5259d7c3f913d80258d5ddbb711b
|
|
7
|
+
data.tar.gz: 87178e647ac42309e0cdbef6c48af8c0f7a902ee32238c3c5cf8cbca2ee7ae80396895bb3a70417c912f98f669834e19ce1066c596a7a5432c7245b29a537248
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
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
|
|
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]
|
|
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
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
waterdrop (2.10.
|
|
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.
|
|
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
|
|
79
|
-
def messages_for(
|
|
80
|
-
@topics[
|
|
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
|
|
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(
|
|
17
|
-
@topic =
|
|
16
|
+
def initialize(topic_name, partition, offset)
|
|
17
|
+
@topic = topic_name
|
|
18
18
|
@partition = partition
|
|
19
19
|
@offset = offset
|
|
20
20
|
end
|
data/lib/waterdrop/errors.rb
CHANGED
|
@@ -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
|
|
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
|
|
193
|
+
# @param topic_name [String] Kafka topic
|
|
194
194
|
# @param method_name [Symbol] method from which this message operation comes
|
|
195
|
-
def report_message(
|
|
196
|
-
increment(method_name, tags: default_tags + ["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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
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
|
-
@
|
|
170
|
-
|
|
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
|
|
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
|
data/lib/waterdrop/producer.rb
CHANGED
|
@@ -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
|
|
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(
|
|
231
|
-
client.partition_count(
|
|
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
|
-
@
|
|
243
|
-
|
|
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
|
data/lib/waterdrop/version.rb
CHANGED