waterdrop 2.10.4 → 2.10.6
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/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +28 -22
- data/Gemfile +0 -1
- data/Gemfile.lock +14 -14
- data/README.md +3 -3
- data/bin/verify_kafka_warnings +5 -0
- data/docker-compose.oauth.yml +3 -0
- data/docker-compose.sasl.yml +3 -0
- data/docker-compose.yml +3 -0
- data/lib/waterdrop/middleware.rb +8 -0
- data/lib/waterdrop/producer/async.rb +5 -2
- data/lib/waterdrop/producer/buffer.rb +77 -13
- data/lib/waterdrop/producer/sync.rb +5 -2
- data/lib/waterdrop/producer.rb +13 -3
- data/lib/waterdrop/version.rb +1 -1
- data/package-lock.json +9 -9
- data/renovate.json +3 -0
- data/waterdrop.gemspec +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df6f8bc25170845840699e442886688586d193d69ddb89849bfbd0a6da2c7dbd
|
|
4
|
+
data.tar.gz: 15cd50dfc2d196580d5afb44aef4ecab2683b7ce11f966d06041c4f5f120cbfc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a0dd37309d89f9ffa8d3932f338edf56f059bbd620befc1c008c6375c258f515e4aec054370815fc56411ba787f887ca9bf7bc5173ec7cf6117f6db44cec7ce
|
|
7
|
+
data.tar.gz: 87e75e5ec48f647bc070603d49848a9d5032ab89fb411ac4e33683311b70736e287de5d6cdfebb4c1a77c96d7f09438e9c85c597faea7673031e11ff64f25bfa
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# WaterDrop changelog
|
|
2
2
|
|
|
3
|
+
## 2.10.6 (2026-10-12)
|
|
4
|
+
- **[EOL]** Drop Ruby 3.2 support; require Ruby 3.3+.
|
|
5
|
+
- [Maintenance] Document that a middleware chain run is not atomic, so an in-place step followed by a raising step is applied again on the next `#flush`.
|
|
6
|
+
- [Maintenance] Stop the sync delivery error spec from flaking when librdkafka rejects the produce to the invalid topic inline instead of through the delivery report.
|
|
7
|
+
|
|
8
|
+
## 2.10.5 (2026-09-23)
|
|
9
|
+
- [Fix] Stop `#flush` from running middleware twice on messages re-buffered after a failed flush, which double-processed payloads and duplicated headers on retry (regression of #474).
|
|
10
|
+
- [Fix] Raise `ProducerClosedError` instead of silently losing a message buffered while the producer is closing.
|
|
11
|
+
- [Fix] Restore both buffers when `#flush` fails before anything is dispatched, instead of discarding the whole batch.
|
|
12
|
+
|
|
3
13
|
## 2.10.4 (2026-08-26)
|
|
4
14
|
- [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"`).
|
|
5
15
|
- [Fix] Emit `connection_pool.shutdown` only once when shutting down the global connection pool.
|
|
@@ -7,10 +17,10 @@
|
|
|
7
17
|
- [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.
|
|
8
18
|
|
|
9
19
|
## 2.10.3 (2026-07-15)
|
|
10
|
-
- [Feature] Add `wait_timeout_on_transaction_abort` (
|
|
20
|
+
- [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.
|
|
11
21
|
|
|
12
22
|
## 2.10.2 (2026-06-15)
|
|
13
|
-
- [Feature] Expose `Producer#current_variant
|
|
23
|
+
- [Feature] Expose `Producer#current_variant`, returning the variant active for the current dispatch, for use in middleware and instrumentation listeners.
|
|
14
24
|
- [Enhancement] Stop allocating one interpolated string per message in `LoggerListener` batch produce handlers.
|
|
15
25
|
- [Enhancement] Use `Array#concat` in `Producer#buffer_many` instead of appending messages one by one.
|
|
16
26
|
- [Enhancement] Skip building the `message.acknowledged` instrumentation payload in the delivery callback when nothing is subscribed to that event.
|
|
@@ -19,22 +29,22 @@
|
|
|
19
29
|
- [Enhancement] Cache the variant validation contract in a constant instead of instantiating a new `Contracts::Variant` on every `Producer#with` / `Producer#variant` call.
|
|
20
30
|
- [Enhancement] Cache the tombstone validation contract in a constant instead of instantiating a new `Contracts::Tombstone` per tombstone message.
|
|
21
31
|
- [Enhancement] Replace explicit `Warning[:performance]` opt-in with a dynamic approach using `Warning.categories` to enable all stable opt-in warning categories in the test suite.
|
|
22
|
-
- [Fix] Prevent a deadlock between a transactional single-message dispatch and `#close
|
|
23
|
-
- [Fix] Prevent a deadlock
|
|
32
|
+
- [Fix] Prevent a deadlock between a transactional single-message dispatch and `#close`.
|
|
33
|
+
- [Fix] Prevent a deadlock when closing an idempotent producer with `reload_on_idempotent_fatal_error` enabled whose final flush hits a fatal error.
|
|
24
34
|
- [Fix] Make concurrent idempotent fatal-error reload thread-safe so a second thread's reload can no longer raise `NoMethodError` after the first reset `@client`.
|
|
25
35
|
- [Fix] Stop `#flush_async` / `#flush_sync` from silently dropping valid buffered messages when the dispatch fails; unsent messages are now re-buffered so they can be retried instead of being lost.
|
|
26
36
|
- [Fix] Make `Producer#close` fork-safe so the GC finalizer inherited by a forked child can no longer flush and close the parent's client.
|
|
27
|
-
- [Fix]
|
|
28
|
-
- [Fix] Stop a nested
|
|
29
|
-
- [Fix] Stop `ConnectionPool#shutdown` and `#reload` from
|
|
30
|
-
- [Fix]
|
|
37
|
+
- [Fix] Prevent a concurrent flush, purge or close from dropping a message buffered with `#buffer` or `#buffer_many`.
|
|
38
|
+
- [Fix] Stop a nested variant call on the same producer from overriding the outer variant inside a variant `transaction` block.
|
|
39
|
+
- [Fix] Stop `ConnectionPool#shutdown` and `#reload` from dropping in-flight messages by closing producers gracefully. Use `pool.shutdown(force: true)` for the previous force-and-purge behavior.
|
|
40
|
+
- [Fix] Fix a race that could leave a newly registered producer permanently unpolled by the FD poller.
|
|
31
41
|
|
|
32
42
|
## 2.10.1 (2026-05-25)
|
|
33
|
-
- [Fix] Prevent `Producer#close` from raising `ThreadError: can't be called from trap context` when
|
|
43
|
+
- [Fix] Prevent `Producer#close` from raising `ThreadError: can't be called from trap context` when called from a signal trap (e.g. Puma's `after_stopped` hook in single mode). It now closes from a background thread and waits for it (#866).
|
|
34
44
|
|
|
35
45
|
## 2.10.0 (2026-05-07)
|
|
36
|
-
- [Fix] Clean up native
|
|
37
|
-
- **[Breaking]** Skip
|
|
46
|
+
- [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.
|
|
47
|
+
- **[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.
|
|
38
48
|
- **[Breaking]** Raise `WaterDrop::Errors::StatisticsNotEnabledError` when attempting to subscribe to `statistics.emitted` on a monitor where librdkafka statistics have been disabled at client build time, replacing the previous "silent nothing" failure mode.
|
|
39
49
|
- [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.
|
|
40
50
|
- [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`.
|
|
@@ -43,14 +53,14 @@
|
|
|
43
53
|
- [Fix] Use `delete` in the variant ensure block to avoid leaving stale nil entries in `Fiber.current.waterdrop_clients` and prevent memory leaks in long-running processes (#836).
|
|
44
54
|
- [Fix] Exclude test files, `.github/`, and `log/` directories from gem releases to reduce package size.
|
|
45
55
|
- **[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.
|
|
46
|
-
- **[Breaking]**
|
|
56
|
+
- **[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`.
|
|
47
57
|
- [Feature] Add `config.statistics_decorator` setting to allow full control over the `StatisticsDecorator` instance used for statistics decoration. Users can provide a custom decorator with different `only_keys` and `excluded_keys` to match their instrumentation needs.
|
|
48
58
|
- [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).
|
|
49
59
|
|
|
50
60
|
**Upgrade Notes**: https://karafka.io/docs/Upgrades-WaterDrop-2.9/
|
|
51
61
|
|
|
52
62
|
## 2.8.16 (2026-02-25)
|
|
53
|
-
- [Feature] Add FD-based polling
|
|
63
|
+
- [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.
|
|
54
64
|
- [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.
|
|
55
65
|
- [Feature] Add `Producer#queue_size` (alias: `#queue_length`) to return the count of messages pending in the librdkafka queue. This counts messages that have been dispatched to librdkafka but not yet transmitted to the Kafka broker.
|
|
56
66
|
- [Enhancement] Add `poll_nb` and `poll_drain` methods to rdkafka Producer for efficient non-blocking polling without GVL release overhead.
|
|
@@ -77,11 +87,7 @@
|
|
|
77
87
|
|
|
78
88
|
## 2.8.12 (2025-10-10)
|
|
79
89
|
- [Enhancement] Introduce `reload_on_idempotent_fatal_error` to automatically reload librdkafka producer after fatal errors on idempotent (non-transactional) producers.
|
|
80
|
-
- [Enhancement] Add
|
|
81
|
-
- `wait_backoff_on_idempotent_fatal_error` (default: 5000ms) - backoff before retrying after idempotent fatal error reload
|
|
82
|
-
- `max_attempts_on_idempotent_fatal_error` (default: 5) - max reload attempts for idempotent fatal errors
|
|
83
|
-
- `wait_backoff_on_transaction_fatal_error` (default: 1000ms) - backoff after transactional fatal error reload
|
|
84
|
-
- `max_attempts_on_transaction_fatal_error` (default: 10) - max reload attempts for transactional fatal errors
|
|
90
|
+
- [Enhancement] Add backoff and retry limits for fatal error reloads to prevent infinite reload loops (defaults in brackets): `wait_backoff_on_idempotent_fatal_error` (5000ms), `max_attempts_on_idempotent_fatal_error` (5), `wait_backoff_on_transaction_fatal_error` (1000ms) and `max_attempts_on_transaction_fatal_error` (10).
|
|
85
91
|
- [Enhancement] Ensure `error.occurred` is instrumented before idempotent fatal error reload for visibility.
|
|
86
92
|
- [Enhancement] Automatically reset fatal error reload attempts counter on successful produce/transaction to allow recovery.
|
|
87
93
|
- [Refactor] Extract idempotence-related logic into separate `WaterDrop::Producer::Idempotence` module.
|
|
@@ -107,7 +113,7 @@
|
|
|
107
113
|
|
|
108
114
|
## 2.8.8 (2025-09-23)
|
|
109
115
|
- [Feature] Add `WaterDrop::ConnectionPool` for efficient connection pooling using the proven `connection_pool` gem.
|
|
110
|
-
- [Feature] Add `WaterDrop.instrumentation`
|
|
116
|
+
- [Feature] Add class-level `WaterDrop.instrumentation` with `producer.created` and `producer.configured` events, so libraries (e.g. Datadog tracing) can configure producers without instance references.
|
|
111
117
|
- **[EOL]** Remove Ruby `3.1` specs according to the EOL schedule.
|
|
112
118
|
|
|
113
119
|
## 2.8.7 (2025-09-02)
|
|
@@ -196,7 +202,7 @@ This release contains **BREAKING** changes. Make sure to read and apply upgrade
|
|
|
196
202
|
- **[Breaking]** Remove no longer needed `wait_timeout` configuration option.
|
|
197
203
|
- **[Breaking]** Do **not** validate or morph (via middleware) messages added to the buffer prior to `flush_sync` or `flush_async`.
|
|
198
204
|
- [Enhancement] Provide `WaterDrop::Producer#transaction?` that returns only when producer has an active transaction running.
|
|
199
|
-
- [Enhancement] Introduce `instrument_on_wait_queue_full`
|
|
205
|
+
- [Enhancement] Introduce `instrument_on_wait_queue_full` (defaults to `true`) to control whether retryable queue full errors are instrumented in the error pipeline.
|
|
200
206
|
- [Enhancement] Protect critical `rdkafka` thread executable code sections.
|
|
201
207
|
- [Enhancement] Treat the queue size as a gauge rather than a cumulative stat (isturdy).
|
|
202
208
|
- [Fix] Fix a case where purge on non-initialized client would crash.
|
|
@@ -220,7 +226,7 @@ This release contains **BREAKING** changes. Make sure to read and apply upgrade
|
|
|
220
226
|
- [Fix] Fix a case where buffered test client would not accumulate messages on failed transactions
|
|
221
227
|
|
|
222
228
|
## 2.6.10 (2023-10-24)
|
|
223
|
-
- [Improvement] Introduce `message.purged` event
|
|
229
|
+
- [Improvement] Introduce the `message.purged` event for a transactional message purged before delivery to Kafka. Non-transactional producers report purges via `error.occurred` instead.
|
|
224
230
|
- [Fix] Fix a case where `message.acknowledged` would not have `caller` key.
|
|
225
231
|
- [Fix] Fix a bug where critical errors (like `IRB::Abort`) would not abort the ongoing transaction.
|
|
226
232
|
|
|
@@ -274,7 +280,7 @@ This release contains **BREAKING** changes. Make sure to read and apply upgrade
|
|
|
274
280
|
- [Improvement] Make `#produce` method private to avoid confusion and make sure it is not used directly (it is not part of the official API).
|
|
275
281
|
- [Change] Change `wait_on_queue_full` from `false` to `true` as a default.
|
|
276
282
|
- [Change] Rename `wait_on_queue_full_timeout` to `wait_backoff_on_queue_full` to match what it actually does.
|
|
277
|
-
- [Enhancement] Introduce `wait_timeout_on_queue_full
|
|
283
|
+
- [Enhancement] Introduce `wait_timeout_on_queue_full`: the time after which the error is raised despite backoff, so never-deliverable messages no longer cause an infinite loop.
|
|
278
284
|
- [Fix] Provide `type` for queue full errors that references the appropriate public API method correctly.
|
|
279
285
|
|
|
280
286
|
## 2.5.3 (2023-05-26)
|
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.6)
|
|
5
5
|
karafka-core (>= 2.5.12, < 3.0.0)
|
|
6
6
|
karafka-rdkafka (>= 0.24.0)
|
|
7
7
|
zeitwerk (~> 2.3)
|
|
@@ -14,12 +14,12 @@ GEM
|
|
|
14
14
|
connection_pool (3.0.2)
|
|
15
15
|
drb (2.2.3)
|
|
16
16
|
ffi (1.17.4)
|
|
17
|
-
io-console (0.
|
|
18
|
-
json (
|
|
19
|
-
karafka-core (2.6.
|
|
17
|
+
io-console (0.9.4)
|
|
18
|
+
json (3.0.2)
|
|
19
|
+
karafka-core (2.6.4)
|
|
20
20
|
karafka-rdkafka (>= 0.20.0)
|
|
21
21
|
logger (>= 1.6.0)
|
|
22
|
-
karafka-rdkafka (0.
|
|
22
|
+
karafka-rdkafka (0.30.2)
|
|
23
23
|
ffi (~> 1.17.1)
|
|
24
24
|
json (> 2.0)
|
|
25
25
|
logger
|
|
@@ -35,10 +35,10 @@ GEM
|
|
|
35
35
|
ostruct (0.6.3)
|
|
36
36
|
prism (1.9.0)
|
|
37
37
|
rake (13.4.2)
|
|
38
|
-
reline (0.
|
|
38
|
+
reline (0.7.0)
|
|
39
39
|
io-console (~> 0.5)
|
|
40
40
|
ruby2_keywords (0.0.5)
|
|
41
|
-
simplecov (1.
|
|
41
|
+
simplecov (1.3.2)
|
|
42
42
|
warning (1.6.0)
|
|
43
43
|
zeitwerk (2.8.3)
|
|
44
44
|
|
|
@@ -61,10 +61,10 @@ CHECKSUMS
|
|
|
61
61
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
62
62
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
63
63
|
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
|
64
|
-
io-console (0.
|
|
65
|
-
json (
|
|
66
|
-
karafka-core (2.6.
|
|
67
|
-
karafka-rdkafka (0.
|
|
64
|
+
io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1
|
|
65
|
+
json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
|
|
66
|
+
karafka-core (2.6.4) sha256=e08166ca7ef12fadf0500f2a3aba63b5ba918a98533c3feaac9f43c90f159363
|
|
67
|
+
karafka-rdkafka (0.30.2) sha256=11d565109ab9ebc6216020d6097bd9adbc39a9e599fb2a8cc5af93965b9008b7
|
|
68
68
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
69
69
|
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
|
|
70
70
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
@@ -72,11 +72,11 @@ CHECKSUMS
|
|
|
72
72
|
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
|
73
73
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
74
74
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
75
|
-
reline (0.
|
|
75
|
+
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
|
76
76
|
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
|
77
|
-
simplecov (1.
|
|
77
|
+
simplecov (1.3.2) sha256=2a22ec1759f1b8db60881a2e038758956fc69ef93ed669be0bde0678de6d0e85
|
|
78
78
|
warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e
|
|
79
|
-
waterdrop (2.10.
|
|
79
|
+
waterdrop (2.10.6)
|
|
80
80
|
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
|
|
81
81
|
|
|
82
82
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# WaterDrop
|
|
2
2
|
|
|
3
|
-
[](https://github.com/karafka/waterdrop/actions
|
|
4
|
-
[](
|
|
3
|
+
[](https://github.com/karafka/waterdrop/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/waterdrop)
|
|
5
5
|
[](https://slack.karafka.io)
|
|
6
6
|
|
|
7
7
|
WaterDrop is a standalone gem that sends messages to Kafka easily with an extra validation layer. It is a part of the [Karafka](https://github.com/karafka/karafka) ecosystem.
|
|
@@ -16,7 +16,7 @@ It:
|
|
|
16
16
|
- Supports multiple delivery policies
|
|
17
17
|
- Supports per-topic configuration alterations (variants)
|
|
18
18
|
- Works with [async](https://github.com/socketry/async) gems ecosystem
|
|
19
|
-
- Works with Kafka `1.0+` and Ruby `3.
|
|
19
|
+
- Works with Kafka `1.0+` and Ruby `3.3+`
|
|
20
20
|
- Works with and without Karafka
|
|
21
21
|
|
|
22
22
|
## Documentation
|
data/bin/verify_kafka_warnings
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
# Only specified warnings are allowed, all others should trigger failure
|
|
5
5
|
|
|
6
6
|
allowed_patterns=(
|
|
7
|
+
# Enabling share groups (KIP-932) requires "share" in group.coordinator.rebalance.protocols
|
|
8
|
+
# on cp-kafka 8.3.0 (Kafka 4.x). That mechanism is deprecated in favour of the share.version
|
|
9
|
+
# feature flag (Kafka 5.0), but is still functionally required to enable share groups in this
|
|
10
|
+
# version, so the broker logs this deprecation warning on every startup. Expected here.
|
|
11
|
+
"group.coordinator.rebalance.protocols. is deprecated"
|
|
7
12
|
"Performing controller activation"
|
|
8
13
|
"registered with feature metadata.version"
|
|
9
14
|
"TOPIC_ALREADY_EXISTS"
|
data/docker-compose.oauth.yml
CHANGED
|
@@ -49,6 +49,9 @@ services:
|
|
|
49
49
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
50
50
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
|
51
51
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
|
52
|
+
KAFKA_GROUP_COORDINATOR_REBALANCE_PROTOCOLS: 'classic,consumer,share'
|
|
53
|
+
KAFKA_SHARE_COORDINATOR_STATE_TOPIC_REPLICATION_FACTOR: 1
|
|
54
|
+
KAFKA_SHARE_COORDINATOR_STATE_TOPIC_MIN_ISR: 1
|
|
52
55
|
# Required by Confluent image when SASL is enabled
|
|
53
56
|
# Also allow HTTP URLs for JWKS endpoint (Kafka 3.0+ blocks non-HTTPS by default)
|
|
54
57
|
KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/kafka_server_jaas.conf -Dorg.apache.kafka.sasl.oauthbearer.allowed.urls=http://keycloak:8080/realms/kafka/protocol/openid-connect/certs"
|
data/docker-compose.sasl.yml
CHANGED
|
@@ -21,6 +21,9 @@ services:
|
|
|
21
21
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
22
22
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
|
23
23
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
|
24
|
+
KAFKA_GROUP_COORDINATOR_REBALANCE_PROTOCOLS: 'classic,consumer,share'
|
|
25
|
+
KAFKA_SHARE_COORDINATOR_STATE_TOPIC_REPLICATION_FACTOR: 1
|
|
26
|
+
KAFKA_SHARE_COORDINATOR_STATE_TOPIC_MIN_ISR: 1
|
|
24
27
|
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
|
|
25
28
|
KAFKA_AUTHORIZER_CLASS_NAME: org.apache.kafka.metadata.authorizer.StandardAuthorizer
|
|
26
29
|
KAFKA_OPTS: ""
|
data/docker-compose.yml
CHANGED
|
@@ -21,5 +21,8 @@ services:
|
|
|
21
21
|
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
|
|
22
22
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
|
23
23
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
|
24
|
+
KAFKA_GROUP_COORDINATOR_REBALANCE_PROTOCOLS: 'classic,consumer,share'
|
|
25
|
+
KAFKA_SHARE_COORDINATOR_STATE_TOPIC_REPLICATION_FACTOR: 1
|
|
26
|
+
KAFKA_SHARE_COORDINATOR_STATE_TOPIC_MIN_ISR: 1
|
|
24
27
|
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
|
|
25
28
|
KAFKA_AUTHORIZER_CLASS_NAME: org.apache.kafka.metadata.authorizer.StandardAuthorizer
|
data/lib/waterdrop/middleware.rb
CHANGED
|
@@ -17,6 +17,14 @@ module WaterDrop
|
|
|
17
17
|
# into a new object.
|
|
18
18
|
# @note You need to decide yourself whether you don't use the message hash data anywhere else
|
|
19
19
|
# and you want to save on memory by modifying it in place or do you want to do a deep copy
|
|
20
|
+
# @note Steps run sequentially and a chain run is not atomic. If an in-place step mutates the
|
|
21
|
+
# message and a later step raises, the message is left partially transformed. During a
|
|
22
|
+
# buffer flush that message is re-buffered as not yet processed, so the next flush runs the
|
|
23
|
+
# whole chain over it again and the earlier in-place steps are applied twice (for example
|
|
24
|
+
# double encryption or duplicated headers). Making the run atomic would mean copying every
|
|
25
|
+
# message before its chain runs, which defeats the memory saving in-place mode is for, so
|
|
26
|
+
# this is a deliberate trade-off. If an in-place step can be followed by a step that may
|
|
27
|
+
# raise, make the in-place step idempotent or have it return a copy instead.
|
|
20
28
|
def run(message)
|
|
21
29
|
return message if @count.zero?
|
|
22
30
|
|
|
@@ -44,15 +44,18 @@ module WaterDrop
|
|
|
44
44
|
#
|
|
45
45
|
# @param messages [Array<Hash>] array with messages that comply with the
|
|
46
46
|
# {Contracts::Message} contract
|
|
47
|
+
# @param run_middleware [Boolean] should the middleware chain be run on the messages. Set to
|
|
48
|
+
# false only when the messages have already been through middleware (for example when
|
|
49
|
+
# re-dispatching a re-buffered flush batch) so it is not applied twice.
|
|
47
50
|
#
|
|
48
51
|
# @return [Array<Rdkafka::Producer::DeliveryHandle>] deliveries handles
|
|
49
52
|
#
|
|
50
53
|
# @raise [Rdkafka::RdkafkaError] When adding the messages to rdkafka's queue failed
|
|
51
54
|
# @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
|
|
52
55
|
# and the message could not be sent to Kafka
|
|
53
|
-
def produce_many_async(messages)
|
|
56
|
+
def produce_many_async(messages, run_middleware: true)
|
|
54
57
|
dispatched = []
|
|
55
|
-
messages = middleware.run_many(messages)
|
|
58
|
+
messages = middleware.run_many(messages) if run_middleware
|
|
56
59
|
messages.each { |message| validate_message!(message) }
|
|
57
60
|
|
|
58
61
|
@monitor.instrument(
|
|
@@ -15,12 +15,22 @@ module WaterDrop
|
|
|
15
15
|
# The append runs under @buffer_mutex because flush/purge/close swap @messages for a fresh
|
|
16
16
|
# array under the same lock. Without it, a concurrent swap between reading @messages and
|
|
17
17
|
# appending would land the message in the orphaned old array and silently lose it.
|
|
18
|
+
#
|
|
19
|
+
# The liveness check is repeated under the same lock because `close` marks the producer
|
|
20
|
+
# `:closing` before its final flush acquires @buffer_mutex. Checking only above the lock
|
|
21
|
+
# would let a close complete in between, stranding the message in a closed producer's
|
|
22
|
+
# buffer while this call returned success.
|
|
18
23
|
@monitor.instrument(
|
|
19
24
|
"message.buffered",
|
|
20
25
|
producer_id: id,
|
|
21
26
|
message: message,
|
|
22
27
|
buffer: @messages
|
|
23
|
-
)
|
|
28
|
+
) do
|
|
29
|
+
@buffer_mutex.synchronize do
|
|
30
|
+
ensure_active!
|
|
31
|
+
@messages << message
|
|
32
|
+
end
|
|
33
|
+
end
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
# Adds given messages into the internal producer buffer without flushing them to Kafka
|
|
@@ -34,14 +44,20 @@ module WaterDrop
|
|
|
34
44
|
|
|
35
45
|
# The concat runs under @buffer_mutex for the same reason as #buffer: flush/purge/close swap
|
|
36
46
|
# @messages under the lock, so an unguarded concat could append into an array that has just
|
|
37
|
-
# been captured for dispatch (or discarded), silently losing the messages.
|
|
47
|
+
# been captured for dispatch (or discarded), silently losing the messages. The liveness
|
|
48
|
+
# check is repeated under the lock for the same reason as in #buffer: a close that
|
|
49
|
+
# completes between the check above and the lock would otherwise strand the batch.
|
|
38
50
|
@monitor.instrument(
|
|
39
51
|
"messages.buffered",
|
|
40
52
|
producer_id: id,
|
|
41
53
|
messages: messages,
|
|
42
54
|
buffer: @messages
|
|
43
55
|
) do
|
|
44
|
-
@buffer_mutex.synchronize
|
|
56
|
+
@buffer_mutex.synchronize do
|
|
57
|
+
ensure_active!
|
|
58
|
+
@messages.concat(messages)
|
|
59
|
+
end
|
|
60
|
+
|
|
45
61
|
messages
|
|
46
62
|
end
|
|
47
63
|
end
|
|
@@ -53,7 +69,7 @@ module WaterDrop
|
|
|
53
69
|
@monitor.instrument(
|
|
54
70
|
"buffer.flushed_async",
|
|
55
71
|
producer_id: id,
|
|
56
|
-
messages:
|
|
72
|
+
messages: messages
|
|
57
73
|
) { flush(false) }
|
|
58
74
|
end
|
|
59
75
|
|
|
@@ -64,7 +80,7 @@ module WaterDrop
|
|
|
64
80
|
@monitor.instrument(
|
|
65
81
|
"buffer.flushed_sync",
|
|
66
82
|
producer_id: id,
|
|
67
|
-
messages:
|
|
83
|
+
messages: messages
|
|
68
84
|
) { flush(true) }
|
|
69
85
|
end
|
|
70
86
|
|
|
@@ -78,17 +94,53 @@ module WaterDrop
|
|
|
78
94
|
# @note We use this method underneath to provide a different instrumentation for sync and
|
|
79
95
|
# async flushing within the public API
|
|
80
96
|
def flush(sync)
|
|
81
|
-
|
|
97
|
+
fresh = nil
|
|
98
|
+
requeued = nil
|
|
99
|
+
data_for_dispatch = []
|
|
82
100
|
|
|
83
101
|
@buffer_mutex.synchronize do
|
|
84
|
-
|
|
102
|
+
requeued = @requeued
|
|
103
|
+
@requeued = []
|
|
104
|
+
fresh = @messages
|
|
85
105
|
@messages = []
|
|
86
106
|
end
|
|
87
107
|
|
|
108
|
+
# Middleware runs exactly once per message: on the fresh messages here, never again on the
|
|
109
|
+
# already-transformed ones coming back from a failed flush, which a second pass would
|
|
110
|
+
# corrupt.
|
|
111
|
+
#
|
|
112
|
+
# A middleware step raising here (a serializer, encryptor or schema lookup failing) would
|
|
113
|
+
# otherwise destroy both buffers, since they were emptied above and nothing has been
|
|
114
|
+
# dispatched yet to recover them from. Messages are run one by one so that on failure the
|
|
115
|
+
# ones whose chain completed are kept as returned and are not transformed again, while the
|
|
116
|
+
# failing one and those after it stay on the middleware path. The failing one is not
|
|
117
|
+
# requeued because that would dispatch it without the rest of its chain.
|
|
118
|
+
#
|
|
119
|
+
# Known edge case (#46840): the failing message is re-buffered in whatever state its chain
|
|
120
|
+
# left it. Copy-style steps leave it untouched, but in-place steps that ran before the
|
|
121
|
+
# raising one leave it partially transformed, and the next flush runs its full chain again.
|
|
122
|
+
# We accept this rather than snapshotting every message before its run, which would cost a
|
|
123
|
+
# copy per message on the success path as well. See Middleware#run.
|
|
124
|
+
processed = []
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
fresh.each { |message| processed << middleware.run(message) }
|
|
128
|
+
rescue
|
|
129
|
+
restore_undispatched(fresh.drop(processed.size), requeued.concat(processed))
|
|
130
|
+
|
|
131
|
+
raise
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
data_for_dispatch = requeued.concat(processed)
|
|
135
|
+
|
|
88
136
|
# Do nothing if nothing to flush
|
|
89
137
|
return data_for_dispatch if data_for_dispatch.empty?
|
|
90
138
|
|
|
91
|
-
sync
|
|
139
|
+
if sync
|
|
140
|
+
produce_many_sync(data_for_dispatch, run_middleware: false)
|
|
141
|
+
else
|
|
142
|
+
produce_many_async(data_for_dispatch, run_middleware: false)
|
|
143
|
+
end
|
|
92
144
|
rescue Errors::ProduceManyError => e
|
|
93
145
|
# A dispatch failed partway through the batch. Re-buffer the messages that never reached
|
|
94
146
|
# librdkafka so a partial failure does not silently drop valid buffered messages. For a
|
|
@@ -106,15 +158,27 @@ module WaterDrop
|
|
|
106
158
|
raise
|
|
107
159
|
end
|
|
108
160
|
|
|
109
|
-
# Puts
|
|
110
|
-
#
|
|
111
|
-
#
|
|
161
|
+
# Puts both sets back after a failure that happened before anything reached librdkafka. They
|
|
162
|
+
# go to different buffers because middleware has run on one of them and not the other, which
|
|
163
|
+
# is what keeps it applied exactly once per message.
|
|
164
|
+
#
|
|
165
|
+
# @param fresh [Array<Hash>] messages whose middleware chain has not completed
|
|
166
|
+
# @param requeued [Array<Hash>] already middleware-processed messages
|
|
167
|
+
def restore_undispatched(fresh, requeued)
|
|
168
|
+
@buffer_mutex.synchronize do
|
|
169
|
+
@messages.unshift(*fresh)
|
|
170
|
+
@requeued.unshift(*requeued)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Puts not-yet-dispatched messages back at the front of the retry buffer (preserving their
|
|
175
|
+
# original order relative to each other), so a failed flush does not lose them.
|
|
112
176
|
#
|
|
113
|
-
# @param messages [Array<Hash>] messages to restore
|
|
177
|
+
# @param messages [Array<Hash>] already middleware-processed messages to restore
|
|
114
178
|
def requeue_unflushed(messages)
|
|
115
179
|
return if messages.empty?
|
|
116
180
|
|
|
117
|
-
@buffer_mutex.synchronize { @
|
|
181
|
+
@buffer_mutex.synchronize { @requeued.unshift(*messages) }
|
|
118
182
|
end
|
|
119
183
|
end
|
|
120
184
|
end
|
|
@@ -48,6 +48,9 @@ module WaterDrop
|
|
|
48
48
|
#
|
|
49
49
|
# @param messages [Array<Hash>] array with messages that comply with the
|
|
50
50
|
# {Contracts::Message} contract
|
|
51
|
+
# @param run_middleware [Boolean] should the middleware chain be run on the messages. Set to
|
|
52
|
+
# false only when the messages have already been through middleware (for example when
|
|
53
|
+
# re-dispatching a re-buffered flush batch) so it is not applied twice.
|
|
51
54
|
#
|
|
52
55
|
# @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles for messages that were
|
|
53
56
|
# sent (can be used to verify offset, partition, etc via `#create_result`)
|
|
@@ -63,8 +66,8 @@ module WaterDrop
|
|
|
63
66
|
# errors), returning handles guarantees the same type in the `dispatched` collection,
|
|
64
67
|
# allowing uniform error handling. Each handle is in its final state after this method
|
|
65
68
|
# returns, so you can call `handle.create_result` to obtain the delivery report if needed.
|
|
66
|
-
def produce_many_sync(messages)
|
|
67
|
-
messages = middleware.run_many(messages)
|
|
69
|
+
def produce_many_sync(messages, run_middleware: true)
|
|
70
|
+
messages = middleware.run_many(messages) if run_middleware
|
|
68
71
|
messages.each { |message| validate_message!(message) }
|
|
69
72
|
|
|
70
73
|
dispatched = []
|
data/lib/waterdrop/producer.rb
CHANGED
|
@@ -39,8 +39,6 @@ module WaterDrop
|
|
|
39
39
|
attr_reader :id
|
|
40
40
|
# @return [Status] producer status object
|
|
41
41
|
attr_reader :status
|
|
42
|
-
# @return [Array] internal messages buffer
|
|
43
|
-
attr_reader :messages
|
|
44
42
|
# @return [Object] monitor we want to use
|
|
45
43
|
attr_reader :monitor
|
|
46
44
|
# @return [Object] dry-configurable config object
|
|
@@ -71,6 +69,9 @@ module WaterDrop
|
|
|
71
69
|
|
|
72
70
|
@status = Status.new
|
|
73
71
|
@messages = []
|
|
72
|
+
# Messages re-buffered after a failed flush. Kept apart from @messages because they have
|
|
73
|
+
# already passed through middleware and must not be transformed again on the next flush.
|
|
74
|
+
@requeued = []
|
|
74
75
|
|
|
75
76
|
# Instrument producer creation for global listeners
|
|
76
77
|
class_monitor.instrument(
|
|
@@ -177,6 +178,12 @@ module WaterDrop
|
|
|
177
178
|
@client
|
|
178
179
|
end
|
|
179
180
|
|
|
181
|
+
# @return [Array] internal messages buffer awaiting a flush. Includes both freshly buffered
|
|
182
|
+
# messages and any messages re-buffered after a failed flush that are waiting to be retried.
|
|
183
|
+
def messages
|
|
184
|
+
@requeued + @messages
|
|
185
|
+
end
|
|
186
|
+
|
|
180
187
|
# Returns the number of messages in the librdkafka producer queue.
|
|
181
188
|
#
|
|
182
189
|
# This count includes:
|
|
@@ -232,6 +239,7 @@ module WaterDrop
|
|
|
232
239
|
@monitor.instrument("buffer.purged", producer_id: id) do
|
|
233
240
|
@buffer_mutex.synchronize do
|
|
234
241
|
@messages = []
|
|
242
|
+
@requeued = []
|
|
235
243
|
end
|
|
236
244
|
|
|
237
245
|
# We should not purge if there is no client initialized
|
|
@@ -321,6 +329,7 @@ module WaterDrop
|
|
|
321
329
|
return false unless @client
|
|
322
330
|
return false unless @status.connected?
|
|
323
331
|
return false unless @messages.empty?
|
|
332
|
+
return false unless @requeued.empty?
|
|
324
333
|
return false unless @operations_in_progress.value.zero?
|
|
325
334
|
|
|
326
335
|
@status.disconnecting!
|
|
@@ -354,6 +363,7 @@ module WaterDrop
|
|
|
354
363
|
return false unless @client
|
|
355
364
|
return false unless @status.connected?
|
|
356
365
|
return false unless @messages.empty?
|
|
366
|
+
return false unless @requeued.empty?
|
|
357
367
|
return false if @transaction_mutex.locked?
|
|
358
368
|
return false if @operating_mutex.locked?
|
|
359
369
|
|
|
@@ -503,7 +513,7 @@ module WaterDrop
|
|
|
503
513
|
# Try to get buffer info safely
|
|
504
514
|
if @buffer_mutex.try_lock
|
|
505
515
|
begin
|
|
506
|
-
parts << "buffer_size=#{@messages.size}"
|
|
516
|
+
parts << "buffer_size=#{@messages.size + @requeued.size}"
|
|
507
517
|
ensure
|
|
508
518
|
@buffer_mutex.unlock
|
|
509
519
|
end
|
data/lib/waterdrop/version.rb
CHANGED
data/package-lock.json
CHANGED
|
@@ -103,9 +103,9 @@
|
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
105
|
"node_modules/fastq": {
|
|
106
|
-
"version": "1.20.
|
|
107
|
-
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.
|
|
108
|
-
"integrity": "sha512-
|
|
106
|
+
"version": "1.20.3",
|
|
107
|
+
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
|
|
108
|
+
"integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==",
|
|
109
109
|
"dev": true,
|
|
110
110
|
"license": "ISC",
|
|
111
111
|
"dependencies": {
|
|
@@ -286,9 +286,9 @@
|
|
|
286
286
|
}
|
|
287
287
|
},
|
|
288
288
|
"node_modules/smol-toml": {
|
|
289
|
-
"version": "1.
|
|
290
|
-
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.
|
|
291
|
-
"integrity": "sha512-
|
|
289
|
+
"version": "1.9.0",
|
|
290
|
+
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz",
|
|
291
|
+
"integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==",
|
|
292
292
|
"dev": true,
|
|
293
293
|
"license": "BSD-3-Clause",
|
|
294
294
|
"engines": {
|
|
@@ -312,9 +312,9 @@
|
|
|
312
312
|
}
|
|
313
313
|
},
|
|
314
314
|
"node_modules/yaml": {
|
|
315
|
-
"version": "2.9.
|
|
316
|
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.
|
|
317
|
-
"integrity": "sha512-
|
|
315
|
+
"version": "2.9.1",
|
|
316
|
+
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
|
317
|
+
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
|
318
318
|
"dev": true,
|
|
319
319
|
"license": "ISC",
|
|
320
320
|
"bin": {
|
data/renovate.json
CHANGED
data/waterdrop.gemspec
CHANGED
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.add_dependency "karafka-rdkafka", ">= 0.24.0"
|
|
21
21
|
spec.add_dependency "zeitwerk", "~> 2.3"
|
|
22
22
|
|
|
23
|
-
spec.required_ruby_version = ">= 3.
|
|
23
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
24
24
|
|
|
25
25
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match?(%r{^(spec|test|\.github|log)/}) }
|
|
26
26
|
spec.executables = []
|
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.
|
|
4
|
+
version: 2.10.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -153,14 +153,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
153
153
|
requirements:
|
|
154
154
|
- - ">="
|
|
155
155
|
- !ruby/object:Gem::Version
|
|
156
|
-
version: 3.
|
|
156
|
+
version: 3.3.0
|
|
157
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
158
|
requirements:
|
|
159
159
|
- - ">="
|
|
160
160
|
- !ruby/object:Gem::Version
|
|
161
161
|
version: '0'
|
|
162
162
|
requirements: []
|
|
163
|
-
rubygems_version: 4.0.
|
|
163
|
+
rubygems_version: 4.0.20
|
|
164
164
|
specification_version: 4
|
|
165
165
|
summary: Kafka messaging made easy!
|
|
166
166
|
test_files: []
|