waterdrop 2.10.7 → 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 +22 -6
- data/Gemfile +1 -4
- data/Gemfile.lock +3 -11
- data/lib/waterdrop/clients/buffered.rb +3 -3
- data/lib/waterdrop/clients/dummy.rb +3 -3
- data/lib/waterdrop/config.rb +9 -4
- data/lib/waterdrop/errors.rb +11 -1
- data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +3 -3
- data/lib/waterdrop/polling/queue_pipe.rb +2 -2
- 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/transactions.rb +19 -1
- data/lib/waterdrop/producer.rb +19 -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,7 +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
|
+
|
|
11
|
+
## 2.10.8 (2026-10-25)
|
|
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.
|
|
15
|
+
- [Fix] Stop `Producer#close` from raising `Errno::EPIPE` on JRuby when its close signal races the poller closing the pipe.
|
|
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`.
|
|
17
|
+
|
|
3
18
|
## 2.10.7 (2026-10-19)
|
|
4
19
|
- [Change] Require `karafka-rdkafka` `>=` `0.31.0` so delivery reports carry the message persistence status, latency and broker id.
|
|
20
|
+
- [Maintenance] Fix a flaky transaction purge instrumentation spec on slow runners.
|
|
5
21
|
|
|
6
22
|
## 2.10.6 (2026-10-12)
|
|
7
23
|
- **[EOL]** Drop Ruby 3.2 support; require Ruby 3.3+.
|
|
@@ -14,10 +30,10 @@
|
|
|
14
30
|
- [Fix] Restore both buffers when `#flush` fails before anything is dispatched, instead of discarding the whole batch.
|
|
15
31
|
|
|
16
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.
|
|
17
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"`).
|
|
18
35
|
- [Fix] Emit `connection_pool.shutdown` only once when shutting down the global connection pool.
|
|
19
36
|
- [Fix] Retry a produce that races a concurrent idempotent fatal-error reload instead of leaking a raw `Rdkafka::ClosedProducerError`/`Rdkafka::ClosedInnerError`.
|
|
20
|
-
- [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.
|
|
21
37
|
|
|
22
38
|
## 2.10.3 (2026-07-15)
|
|
23
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.
|
|
@@ -48,7 +64,7 @@
|
|
|
48
64
|
## 2.10.0 (2026-05-07)
|
|
49
65
|
- [Fix] Clean up the native client, instrumentation callbacks and poller registration when `init_transactions` fails during producer construction, so failed attempts no longer leak threads and file descriptors.
|
|
50
66
|
- **[Breaking]** Skip librdkafka statistics when nothing subscribes to `statistics.emitted` before the rdkafka client is built, to save allocations. To use statistics, subscribe a listener BEFORE the first producer use.
|
|
51
|
-
- **[Breaking]** Raise `WaterDrop::Errors::StatisticsNotEnabledError` when
|
|
67
|
+
- **[Breaking]** Raise `WaterDrop::Errors::StatisticsNotEnabledError` when subscribing to `statistics.emitted` on a monitor whose librdkafka statistics were disabled at client build time, instead of silently doing nothing.
|
|
52
68
|
- [Feature] Add tombstone API (`#tombstone_sync`, `#tombstone_async`, `#tombstone_many_sync`, `#tombstone_many_async`) for producing tombstone records (nil-payload messages) with required key and partition validation. Works with variants.
|
|
53
69
|
- [Fix] Add `ensure_same_process!` to `Poller#unregister` for fork safety, so a child that inherited a pre-fork producer no longer deadlocks on `producer.close`.
|
|
54
70
|
|
|
@@ -57,7 +73,7 @@
|
|
|
57
73
|
- [Fix] Exclude test files, `.github/`, and `log/` directories from gem releases to reduce package size.
|
|
58
74
|
- **[Breaking]** Switch default polling mode from `:thread` to `:fd`. If you experience any issues, you can revert to the previous behavior by setting `config.polling.mode = :thread`. The `:thread` mode will be deprecated in 2.10 and removed in 2.11.
|
|
59
75
|
- **[Breaking]** Decorate only the statistics keys used by the built-in Datadog listener (`tx`, `txretries`, `txerrs`, `rxerrs`). If you use other `_d` or `_fd` keys, set a custom `config.statistics_decorator`.
|
|
60
|
-
- [Feature] Add `config.statistics_decorator` setting to
|
|
76
|
+
- [Feature] Add a `config.statistics_decorator` setting to use a custom `StatisticsDecorator` (e.g. with different `only_keys` and `excluded_keys`).
|
|
61
77
|
- [Change] Upscale default timeout values 3x closer to librdkafka defaults to prevent intermediate timeouts during node recovery (`message.timeout.ms`: 50s → 150s, `transaction.timeout.ms`: 55s → 165s, `max_wait_timeout`: 60s → 180s).
|
|
62
78
|
|
|
63
79
|
**Upgrade Notes**: https://karafka.io/docs/Upgrades-WaterDrop-2.9/
|
|
@@ -65,7 +81,7 @@
|
|
|
65
81
|
## 2.8.16 (2026-02-25)
|
|
66
82
|
- [Feature] Add FD-based polling (`config.polling.mode = :fd`): a single Ruby thread with `IO.select` instead of the default thread mode, with 39-54% higher throughput, lower memory usage and fewer threads.
|
|
67
83
|
- [Feature] Add `config.polling.fd.max_time` setting (default: 100ms) to control maximum polling time per producer per cycle. This enables per-producer priority differentiation.
|
|
68
|
-
- [Feature] Add `Producer#queue_size` (alias: `#queue_length`) to return the count of messages
|
|
84
|
+
- [Feature] Add `Producer#queue_size` (alias: `#queue_length`) to return the count of messages dispatched to librdkafka but not yet sent to the Kafka broker.
|
|
69
85
|
- [Enhancement] Add `poll_nb` and `poll_drain` methods to rdkafka Producer for efficient non-blocking polling without GVL release overhead.
|
|
70
86
|
- [Enhancement] Add `poller.producer_registered` and `poller.producer_unregistered` instrumentation events for FD mode.
|
|
71
87
|
- [Change] Update to use `max_wait_timeout_ms` parameter in rdkafka `AbstractHandle#wait` for consistency with librdkafka conventions.
|
|
@@ -108,10 +124,10 @@
|
|
|
108
124
|
|
|
109
125
|
## 2.8.10 (2025-09-25)
|
|
110
126
|
- [Enhancement] Add `#close` alias for `WaterDrop::ConnectionPool#shutdown` to align with producer API for consistent interface across both individual producers and connection pools.
|
|
111
|
-
- [Enhancement] Add `WaterDrop.monitor`
|
|
127
|
+
- [Enhancement] Add `WaterDrop.monitor` as the preferred alias for `WaterDrop.instrumentation`, matching the per-producer API. `instrumentation` remains as a deprecated alias.
|
|
112
128
|
|
|
113
129
|
## 2.8.9 (2025-09-23)
|
|
114
|
-
- [Enhancement] Add connection pool lifecycle events to global instrumentation
|
|
130
|
+
- [Enhancement] Add connection pool lifecycle events to global instrumentation: `connection_pool.created`, `connection_pool.setup`, `connection_pool.shutdown`, `connection_pool.reload` and `connection_pool.reloaded`.
|
|
115
131
|
- [Enhancement] Add default connection pool transactional direct API.
|
|
116
132
|
|
|
117
133
|
## 2.8.8 (2025-09-23)
|
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)
|
|
@@ -9,12 +9,9 @@ PATH
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
|
-
byebug (13.0.0)
|
|
13
|
-
reline (>= 0.6.0)
|
|
14
12
|
connection_pool (3.0.2)
|
|
15
13
|
drb (2.2.3)
|
|
16
14
|
ffi (1.17.4)
|
|
17
|
-
io-console (0.9.4)
|
|
18
15
|
json (3.0.2)
|
|
19
16
|
karafka-core (2.6.4)
|
|
20
17
|
karafka-rdkafka (>= 0.20.0)
|
|
@@ -35,8 +32,6 @@ GEM
|
|
|
35
32
|
ostruct (0.6.3)
|
|
36
33
|
prism (1.9.0)
|
|
37
34
|
rake (13.4.2)
|
|
38
|
-
reline (0.7.0)
|
|
39
|
-
io-console (~> 0.5)
|
|
40
35
|
ruby2_keywords (0.0.5)
|
|
41
36
|
simplecov (1.3.2)
|
|
42
37
|
warning (1.6.0)
|
|
@@ -46,22 +41,20 @@ PLATFORMS
|
|
|
46
41
|
ruby
|
|
47
42
|
|
|
48
43
|
DEPENDENCIES
|
|
49
|
-
byebug
|
|
50
44
|
connection_pool
|
|
51
45
|
minitest
|
|
52
46
|
mocha
|
|
53
47
|
ostruct
|
|
48
|
+
rake
|
|
54
49
|
simplecov
|
|
55
50
|
warning
|
|
56
51
|
waterdrop!
|
|
57
52
|
zeitwerk (~> 2.8.0)
|
|
58
53
|
|
|
59
54
|
CHECKSUMS
|
|
60
|
-
byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
|
|
61
55
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
62
56
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
63
57
|
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
|
64
|
-
io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1
|
|
65
58
|
json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
|
|
66
59
|
karafka-core (2.6.4) sha256=e08166ca7ef12fadf0500f2a3aba63b5ba918a98533c3feaac9f43c90f159363
|
|
67
60
|
karafka-rdkafka (0.31.0) sha256=7d21274122d66ee8d08b8e56a0d06084933deb2306cf2a6e4bd0d073c14e7f9f
|
|
@@ -72,11 +65,10 @@ CHECKSUMS
|
|
|
72
65
|
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
|
73
66
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
74
67
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
75
|
-
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
|
76
68
|
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
|
77
69
|
simplecov (1.3.2) sha256=2a22ec1759f1b8db60881a2e038758956fc69ef93ed669be0bde0678de6d0e85
|
|
78
70
|
warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e
|
|
79
|
-
waterdrop (2.10.
|
|
71
|
+
waterdrop (2.10.9)
|
|
80
72
|
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
|
|
81
73
|
|
|
82
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/config.rb
CHANGED
|
@@ -130,13 +130,18 @@ module WaterDrop
|
|
|
130
130
|
# WARNING: Modifying this setting can cause infinite reload loops if not properly understood.
|
|
131
131
|
# The default includes :fenced errors because:
|
|
132
132
|
# - Fencing occurs when another producer with the same transactional.id takes over
|
|
133
|
-
# -
|
|
133
|
+
# - Fencing also occurs when the broker aborts a transaction that exceeded
|
|
134
|
+
# `transaction.timeout.ms`. Both cases raise the same :fenced error, so WaterDrop cannot
|
|
135
|
+
# tell them apart
|
|
136
|
+
# - After a takeover this is an unrecoverable state - reloading won't help as the other
|
|
137
|
+
# producer is active
|
|
134
138
|
# - Attempting to reload on fenced errors creates: produce -> fenced -> reload -> produce ->
|
|
135
139
|
# fenced -> reload (infinite loop)
|
|
136
140
|
#
|
|
137
|
-
# Only remove :fenced from this list if
|
|
138
|
-
#
|
|
139
|
-
#
|
|
141
|
+
# Only remove :fenced from this list if each transactional.id is used by one producer at a
|
|
142
|
+
# time (e.g., coordinated transactional.id assignment). Otherwise a reload fences the newer
|
|
143
|
+
# producer and the two can keep fencing each other for as long as both run. In most cases,
|
|
144
|
+
# you should keep the default value.
|
|
140
145
|
setting :non_reloadable_errors, default: %i[fenced]
|
|
141
146
|
# option [Integer] Idle disconnect timeout in milliseconds. When set to 0, idle disconnection
|
|
142
147
|
# is disabled. When set to a positive value, WaterDrop will automatically disconnect
|
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
|
|
@@ -29,8 +29,8 @@ module WaterDrop
|
|
|
29
29
|
# Thread-safe and non-blocking; silently ignores errors
|
|
30
30
|
def signal
|
|
31
31
|
@writer.write_nonblock("W", exception: false)
|
|
32
|
-
rescue IOError, Errno::EBADF
|
|
33
|
-
# Pipe closed
|
|
32
|
+
rescue IOError, Errno::EBADF, Errno::EPIPE
|
|
33
|
+
# Pipe closed. EPIPE: the reader is already closed, so there is no one left to wake
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Drains all pending bytes from the pipe
|
|
@@ -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
|
|
@@ -65,7 +65,12 @@ module WaterDrop
|
|
|
65
65
|
|
|
66
66
|
transactional_instrument(:finished) do
|
|
67
67
|
with_transactional_error_handling(:begin) do
|
|
68
|
-
transactional_instrument(:started)
|
|
68
|
+
transactional_instrument(:started) do
|
|
69
|
+
client.begin_transaction
|
|
70
|
+
# Set before the `started` listeners run, so an error raised by one of them still
|
|
71
|
+
# aborts the transaction that has begun
|
|
72
|
+
@transaction_open = true
|
|
73
|
+
end
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
result = nil
|
|
@@ -100,6 +105,15 @@ module WaterDrop
|
|
|
100
105
|
# We need to handle any interrupt including critical in order not to have the transaction
|
|
101
106
|
# running. This will also handle things like `IRB::Abort`
|
|
102
107
|
rescue Exception => e
|
|
108
|
+
# Neither a transaction that never began nor one the error handling already aborted
|
|
109
|
+
# (abortable error) has anything left to abort: aborting again would fail with an
|
|
110
|
+
# invalid state error that replaces the original one
|
|
111
|
+
unless @transaction_open
|
|
112
|
+
transactional_reload_client_if_needed(e)
|
|
113
|
+
|
|
114
|
+
raise
|
|
115
|
+
end
|
|
116
|
+
|
|
103
117
|
# This code is a bit tricky. We have an error and when it happens we try to rollback
|
|
104
118
|
# the transaction. However we may end up in a state where transaction aborting itself
|
|
105
119
|
# produces error. In such case we also want to handle it as fatal and reload client.
|
|
@@ -124,6 +138,8 @@ module WaterDrop
|
|
|
124
138
|
|
|
125
139
|
raise unless e.is_a?(WaterDrop::Errors::AbortTransaction)
|
|
126
140
|
ensure
|
|
141
|
+
@transaction_open = false
|
|
142
|
+
|
|
127
143
|
# The first delivery handle is scoped to the transaction that produced it and is only
|
|
128
144
|
# ever read by the abort path (which runs in the rescue above, before this). Clearing it
|
|
129
145
|
# here - on commit, on abort and on a re-raise alike - keeps it from outliving its
|
|
@@ -273,6 +289,8 @@ module WaterDrop
|
|
|
273
289
|
with_transactional_error_handling(:abort, allow_abortable: false) do
|
|
274
290
|
transactional_instrument(:aborted) { client.abort_transaction }
|
|
275
291
|
end
|
|
292
|
+
|
|
293
|
+
@transaction_open = false
|
|
276
294
|
end
|
|
277
295
|
|
|
278
296
|
raise
|
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
|
|
@@ -66,6 +71,7 @@ module WaterDrop
|
|
|
66
71
|
@idempotent_fatal_error_attempts = 0
|
|
67
72
|
@transaction_fatal_error_attempts = 0
|
|
68
73
|
@transaction_first_handle = nil
|
|
74
|
+
@transaction_open = false
|
|
69
75
|
|
|
70
76
|
@status = Status.new
|
|
71
77
|
@messages = []
|
|
@@ -181,6 +187,12 @@ module WaterDrop
|
|
|
181
187
|
# @return [Array] internal messages buffer awaiting a flush. Includes both freshly buffered
|
|
182
188
|
# messages and any messages re-buffered after a failed flush that are waiting to be retried.
|
|
183
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
|
+
|
|
184
196
|
@requeued + @messages
|
|
185
197
|
end
|
|
186
198
|
|
|
@@ -221,13 +233,13 @@ module WaterDrop
|
|
|
221
233
|
|
|
222
234
|
# Fetches and caches the partition count of a topic
|
|
223
235
|
#
|
|
224
|
-
# @param
|
|
236
|
+
# @param topic_name [String] topic for which we want to get the number of partitions
|
|
225
237
|
# @return [Integer] number of partitions of the requested topic or -1 if number could not be
|
|
226
238
|
# retrieved.
|
|
227
239
|
#
|
|
228
240
|
# @note It uses the underlying `rdkafka-ruby` partition count fetch and cache.
|
|
229
|
-
def partition_count(
|
|
230
|
-
client.partition_count(
|
|
241
|
+
def partition_count(topic_name)
|
|
242
|
+
client.partition_count(topic_name.to_s)
|
|
231
243
|
end
|
|
232
244
|
|
|
233
245
|
# Purges data from both the buffer queue as well as the librdkafka queue.
|
|
@@ -238,8 +250,10 @@ module WaterDrop
|
|
|
238
250
|
def purge
|
|
239
251
|
@monitor.instrument("buffer.purged", producer_id: id) do
|
|
240
252
|
@buffer_mutex.synchronize do
|
|
241
|
-
@
|
|
242
|
-
|
|
253
|
+
@messages_mutex.synchronize do
|
|
254
|
+
@messages = []
|
|
255
|
+
@requeued = []
|
|
256
|
+
end
|
|
243
257
|
end
|
|
244
258
|
|
|
245
259
|
# We should not purge if there is no client initialized
|
data/lib/waterdrop/version.rb
CHANGED