waterdrop 2.8.12 → 2.8.13
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/.github/workflows/ci.yml +0 -25
- data/CHANGELOG.md +7 -2
- data/Gemfile +1 -3
- data/Gemfile.lock +15 -15
- data/config/locales/errors.yml +1 -0
- data/lib/waterdrop/config.rb +14 -0
- data/lib/waterdrop/contracts/config.rb +3 -0
- data/lib/waterdrop/instrumentation/notifications.rb +1 -0
- data/lib/waterdrop/producer/idempotence.rb +20 -7
- data/lib/waterdrop/producer/transactions.rb +22 -5
- data/lib/waterdrop/producer.rb +12 -8
- data/lib/waterdrop/version.rb +1 -1
- metadata +1 -2
- data/.diffend.yml +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da869aec5fa217a2adda674c787dd142a4f4e50e99a9b86d349d42f700821c88
|
|
4
|
+
data.tar.gz: 1934d9b60db038b6ea2f99faf7961a5f3ea2b3dfe845f84632db7f145f094cd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c25b0221d070e1ba2b9badcff468caad02d4f4da158083a3514aa8a4d2b5b867579d6bebdaa35f45416c97de6bdddcb9c080a9d5399bca7f509e26e9f1775140
|
|
7
|
+
data.tar.gz: d2c32960c72034b00a48d3cd5b7ab30e12908638945f08aa69ddd27947b98cf38b9b3b0d3ccf78b02ce25b569a5ad551d1bedcbaf40b06df0b1caa7565567b56
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -17,7 +17,6 @@ jobs:
|
|
|
17
17
|
specs:
|
|
18
18
|
timeout-minutes: 15
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
|
-
needs: diffend
|
|
21
20
|
env:
|
|
22
21
|
BUNDLE_FORCE_RUBY_PLATFORM: ${{ matrix.force_ruby_platform }}
|
|
23
22
|
strategy:
|
|
@@ -87,29 +86,6 @@ jobs:
|
|
|
87
86
|
- name: Check test topics naming convention
|
|
88
87
|
run: bin/verify_topics_naming
|
|
89
88
|
|
|
90
|
-
diffend:
|
|
91
|
-
timeout-minutes: 5
|
|
92
|
-
|
|
93
|
-
runs-on: ubuntu-latest
|
|
94
|
-
strategy:
|
|
95
|
-
fail-fast: false
|
|
96
|
-
steps:
|
|
97
|
-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
98
|
-
with:
|
|
99
|
-
fetch-depth: 0
|
|
100
|
-
- name: Set up Ruby
|
|
101
|
-
uses: ruby/setup-ruby@2a7b30092b0caf9c046252510f9273b4875f3db9 # v1.254.0
|
|
102
|
-
with:
|
|
103
|
-
ruby-version: 3.4
|
|
104
|
-
self-hosted: false
|
|
105
|
-
|
|
106
|
-
- name: Install latest bundler
|
|
107
|
-
run: gem install bundler --no-document
|
|
108
|
-
- name: Install Diffend plugin
|
|
109
|
-
run: bundle plugin install diffend
|
|
110
|
-
- name: Bundle Secure
|
|
111
|
-
run: bundle secure
|
|
112
|
-
|
|
113
89
|
coditsu:
|
|
114
90
|
timeout-minutes: 5
|
|
115
91
|
runs-on: ubuntu-latest
|
|
@@ -139,7 +115,6 @@ jobs:
|
|
|
139
115
|
runs-on: ubuntu-latest
|
|
140
116
|
if: always()
|
|
141
117
|
needs:
|
|
142
|
-
- diffend
|
|
143
118
|
- coditsu
|
|
144
119
|
- specs
|
|
145
120
|
steps:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# WaterDrop changelog
|
|
2
2
|
|
|
3
|
+
## 2.8.13 (2025-10-31)
|
|
4
|
+
- [Enhancement] Make `fenced` error skip-reload behavior configurable via new `non_reloadable_errors` setting (defaults to `[:fenced]` for backward compatibility).
|
|
5
|
+
- [Enhancement] Add `producer.reload` event allowing config modification before reload to escape fencing loops (#706).
|
|
6
|
+
- [Enhancement] Do not early initialize the new instance on reload.
|
|
7
|
+
|
|
3
8
|
## 2.8.12 (2025-10-10)
|
|
4
9
|
- [Enhancement] Introduce `reload_on_idempotent_fatal_error` to automatically reload librdkafka producer after fatal errors on idempotent (non-transactional) producers.
|
|
5
10
|
- [Enhancement] Add configurable backoff and retry limits for fatal error recovery to prevent infinite reload loops:
|
|
@@ -19,7 +24,7 @@
|
|
|
19
24
|
## 2.8.11 (2025-09-27)
|
|
20
25
|
- [Enhancement] Provide fast-track for middleware-less flows (20% faster) for single message, 5000x faster for batches.
|
|
21
26
|
- [Enhancement] Optimize middlewares application by around 20%.
|
|
22
|
-
- [
|
|
27
|
+
- **[EOL]** Remove Ruby `3.1` according to the EOL schedule.
|
|
23
28
|
- [Fix] Connection pool timeout parameter now accepts milliseconds instead of seconds for consistency with other WaterDrop timeouts. The default timeout has been changed from `5` seconds to `5000` milliseconds (equivalent value).
|
|
24
29
|
|
|
25
30
|
## 2.8.10 (2025-09-25)
|
|
@@ -33,7 +38,7 @@
|
|
|
33
38
|
## 2.8.8 (2025-09-23)
|
|
34
39
|
- [Feature] Add `WaterDrop::ConnectionPool` for efficient connection pooling using the proven `connection_pool` gem.
|
|
35
40
|
- [Feature] Add `WaterDrop.instrumentation` class-level instrumentation for producer lifecycle events. This allows external libraries to subscribe to `producer.created` and `producer.configured` events without needing producer instance references, enabling middleware injection and configuration by libraries like Datadog tracing.
|
|
36
|
-
- [
|
|
41
|
+
- **[EOL]** Remove Ruby `3.1` specs according to the EOL schedule.
|
|
37
42
|
|
|
38
43
|
## 2.8.7 (2025-09-02)
|
|
39
44
|
- [Enhancement] Disable Nagle algorithm by default (improves latency / aligned with librdkafka)
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
waterdrop (2.8.
|
|
4
|
+
waterdrop (2.8.13)
|
|
5
5
|
karafka-core (>= 2.4.9, < 3.0.0)
|
|
6
6
|
karafka-rdkafka (>= 0.20.0)
|
|
7
7
|
zeitwerk (~> 2.3)
|
|
@@ -24,35 +24,35 @@ GEM
|
|
|
24
24
|
ffi (1.17.2-x86_64-darwin)
|
|
25
25
|
ffi (1.17.2-x86_64-linux-gnu)
|
|
26
26
|
ffi (1.17.2-x86_64-linux-musl)
|
|
27
|
-
json (2.
|
|
28
|
-
karafka-core (2.5.
|
|
27
|
+
json (2.15.1)
|
|
28
|
+
karafka-core (2.5.7)
|
|
29
29
|
karafka-rdkafka (>= 0.20.0)
|
|
30
30
|
logger (>= 1.6.0)
|
|
31
|
-
karafka-rdkafka (0.
|
|
31
|
+
karafka-rdkafka (0.22.2)
|
|
32
32
|
ffi (~> 1.15)
|
|
33
33
|
json (> 2.0)
|
|
34
34
|
logger
|
|
35
35
|
mini_portile2 (~> 2.6)
|
|
36
36
|
rake (> 12)
|
|
37
|
-
karafka-rdkafka (0.
|
|
37
|
+
karafka-rdkafka (0.22.2-aarch64-linux-gnu)
|
|
38
38
|
ffi (~> 1.15)
|
|
39
39
|
json (> 2.0)
|
|
40
40
|
logger
|
|
41
41
|
mini_portile2 (~> 2.6)
|
|
42
42
|
rake (> 12)
|
|
43
|
-
karafka-rdkafka (0.
|
|
43
|
+
karafka-rdkafka (0.22.2-arm64-darwin)
|
|
44
44
|
ffi (~> 1.15)
|
|
45
45
|
json (> 2.0)
|
|
46
46
|
logger
|
|
47
47
|
mini_portile2 (~> 2.6)
|
|
48
48
|
rake (> 12)
|
|
49
|
-
karafka-rdkafka (0.
|
|
49
|
+
karafka-rdkafka (0.22.2-x86_64-linux-gnu)
|
|
50
50
|
ffi (~> 1.15)
|
|
51
51
|
json (> 2.0)
|
|
52
52
|
logger
|
|
53
53
|
mini_portile2 (~> 2.6)
|
|
54
54
|
rake (> 12)
|
|
55
|
-
karafka-rdkafka (0.
|
|
55
|
+
karafka-rdkafka (0.22.2-x86_64-linux-musl)
|
|
56
56
|
ffi (~> 1.15)
|
|
57
57
|
json (> 2.0)
|
|
58
58
|
logger
|
|
@@ -62,27 +62,27 @@ GEM
|
|
|
62
62
|
mini_portile2 (2.8.9)
|
|
63
63
|
ostruct (0.6.3)
|
|
64
64
|
rake (13.3.0)
|
|
65
|
-
rspec (3.13.
|
|
65
|
+
rspec (3.13.2)
|
|
66
66
|
rspec-core (~> 3.13.0)
|
|
67
67
|
rspec-expectations (~> 3.13.0)
|
|
68
68
|
rspec-mocks (~> 3.13.0)
|
|
69
|
-
rspec-core (3.13.
|
|
69
|
+
rspec-core (3.13.6)
|
|
70
70
|
rspec-support (~> 3.13.0)
|
|
71
71
|
rspec-expectations (3.13.5)
|
|
72
72
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
73
73
|
rspec-support (~> 3.13.0)
|
|
74
|
-
rspec-mocks (3.13.
|
|
74
|
+
rspec-mocks (3.13.6)
|
|
75
75
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
76
76
|
rspec-support (~> 3.13.0)
|
|
77
|
-
rspec-support (3.13.
|
|
77
|
+
rspec-support (3.13.6)
|
|
78
78
|
simplecov (0.22.0)
|
|
79
79
|
docile (~> 1.1)
|
|
80
80
|
simplecov-html (~> 0.11)
|
|
81
81
|
simplecov_json_formatter (~> 0.1)
|
|
82
|
-
simplecov-html (0.13.
|
|
82
|
+
simplecov-html (0.13.2)
|
|
83
83
|
simplecov_json_formatter (0.1.4)
|
|
84
84
|
warning (1.5.0)
|
|
85
|
-
zeitwerk (2.
|
|
85
|
+
zeitwerk (2.7.3)
|
|
86
86
|
|
|
87
87
|
PLATFORMS
|
|
88
88
|
aarch64-linux-gnu
|
|
@@ -105,7 +105,7 @@ DEPENDENCIES
|
|
|
105
105
|
simplecov
|
|
106
106
|
warning
|
|
107
107
|
waterdrop!
|
|
108
|
-
zeitwerk (~> 2.
|
|
108
|
+
zeitwerk (~> 2.7.0)
|
|
109
109
|
|
|
110
110
|
BUNDLED WITH
|
|
111
111
|
2.7.0
|
data/config/locales/errors.yml
CHANGED
|
@@ -23,6 +23,7 @@ en:
|
|
|
23
23
|
reload_on_transaction_fatal_error_format: must be boolean
|
|
24
24
|
wait_backoff_on_transaction_fatal_error_format: must be a numeric that is equal or bigger to 0
|
|
25
25
|
max_attempts_on_transaction_fatal_error_format: must be an integer that is equal or bigger than 1
|
|
26
|
+
non_reloadable_errors_format: must be an array of symbols
|
|
26
27
|
oauth.token_provider_listener_format: 'must be false or respond to #on_oauthbearer_token_refresh'
|
|
27
28
|
idle_disconnect_timeout_format: 'must be an integer that is equal to 0 or bigger than 30 000 (30 seconds)'
|
|
28
29
|
|
data/lib/waterdrop/config.rb
CHANGED
|
@@ -95,6 +95,20 @@ module WaterDrop
|
|
|
95
95
|
# option [Integer] How many times to attempt reloading on transactional fatal error before
|
|
96
96
|
# giving up. This prevents infinite reload loops if the producer never recovers.
|
|
97
97
|
setting :max_attempts_on_transaction_fatal_error, default: 10
|
|
98
|
+
# option [Array<Symbol>] List of fatal error codes that should NOT trigger producer reload.
|
|
99
|
+
# These errors represent states that cannot be recovered by simply recreating the client.
|
|
100
|
+
#
|
|
101
|
+
# WARNING: Modifying this setting can cause infinite reload loops if not properly understood.
|
|
102
|
+
# The default includes :fenced errors because:
|
|
103
|
+
# - Fencing occurs when another producer with the same transactional.id takes over
|
|
104
|
+
# - This is an unrecoverable state - reloading won't help as the other producer is active
|
|
105
|
+
# - Attempting to reload on fenced errors creates: produce -> fenced -> reload -> produce ->
|
|
106
|
+
# fenced -> reload (infinite loop)
|
|
107
|
+
#
|
|
108
|
+
# Only remove :fenced from this list if you have explicit logic to handle producer fencing
|
|
109
|
+
# in your application (e.g., coordinated transactional.id assignment, manual intervention).
|
|
110
|
+
# In most cases, you should keep the default value.
|
|
111
|
+
setting :non_reloadable_errors, default: %i[fenced]
|
|
98
112
|
# option [Integer] Idle disconnect timeout in milliseconds. When set to 0, idle disconnection
|
|
99
113
|
# is disabled. When set to a positive value, WaterDrop will automatically disconnect
|
|
100
114
|
# producers that haven't sent any messages for the specified time period. This helps preserve
|
|
@@ -30,6 +30,9 @@ module WaterDrop
|
|
|
30
30
|
required(:max_attempts_on_idempotent_fatal_error) { |val| val.is_a?(Integer) && val >= 1 }
|
|
31
31
|
required(:wait_backoff_on_transaction_fatal_error) { |val| val.is_a?(Numeric) && val >= 0 }
|
|
32
32
|
required(:max_attempts_on_transaction_fatal_error) { |val| val.is_a?(Integer) && val >= 1 }
|
|
33
|
+
required(:non_reloadable_errors) do |val|
|
|
34
|
+
val.is_a?(Array) && val.all?(Symbol)
|
|
35
|
+
end
|
|
33
36
|
required(:idle_disconnect_timeout) do |val|
|
|
34
37
|
val.is_a?(Integer) && (val.zero? || val >= 30_000)
|
|
35
38
|
end
|
|
@@ -23,13 +23,13 @@ module WaterDrop
|
|
|
23
23
|
# - Producer is idempotent
|
|
24
24
|
# - Producer is not transactional
|
|
25
25
|
# - reload_on_idempotent_fatal_error config is enabled
|
|
26
|
-
# - Error is not in the
|
|
26
|
+
# - Error is not in the non_reloadable_errors config list
|
|
27
27
|
def idempotent_reloadable?(error)
|
|
28
28
|
return false unless error.fatal?
|
|
29
29
|
return false unless idempotent?
|
|
30
30
|
return false if transactional?
|
|
31
31
|
return false unless config.reload_on_idempotent_fatal_error
|
|
32
|
-
return false if
|
|
32
|
+
return false if config.non_reloadable_errors.include?(error.code)
|
|
33
33
|
|
|
34
34
|
true
|
|
35
35
|
end
|
|
@@ -50,21 +50,34 @@ module WaterDrop
|
|
|
50
50
|
# old client, and create a new client instance to continue operations.
|
|
51
51
|
#
|
|
52
52
|
# @param attempt [Integer] the current reload attempt number
|
|
53
|
+
# @param error [Rdkafka::RdkafkaError] the error that triggered the reload
|
|
53
54
|
#
|
|
54
55
|
# @note This is only called for idempotent, non-transactional producers when
|
|
55
56
|
# `reload_on_idempotent_fatal_error` is enabled
|
|
56
57
|
# @note After reload, the producer will automatically retry the failed operation
|
|
57
|
-
def idempotent_reload_client_on_fatal_error(attempt)
|
|
58
|
+
def idempotent_reload_client_on_fatal_error(attempt, error)
|
|
58
59
|
@operating_mutex.synchronize do
|
|
60
|
+
# Emit producer.reload event before reload
|
|
61
|
+
# Users can subscribe to this event and modify event[:caller].config.kafka to change
|
|
62
|
+
# producer config
|
|
63
|
+
@monitor.instrument(
|
|
64
|
+
'producer.reload',
|
|
65
|
+
producer_id: id,
|
|
66
|
+
error: error,
|
|
67
|
+
attempt: attempt,
|
|
68
|
+
caller: self
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Clear cached state that depends on config
|
|
72
|
+
# We always clear @idempotent as it might have been modified via the event
|
|
73
|
+
@idempotent = nil
|
|
74
|
+
|
|
59
75
|
@monitor.instrument(
|
|
60
76
|
'producer.reloaded',
|
|
61
77
|
producer_id: id,
|
|
62
78
|
attempt: attempt
|
|
63
79
|
) do
|
|
64
|
-
|
|
65
|
-
purge
|
|
66
|
-
@client.close
|
|
67
|
-
@client = Builder.new.call(self, @config)
|
|
80
|
+
reload!
|
|
68
81
|
end
|
|
69
82
|
end
|
|
70
83
|
end
|
|
@@ -281,24 +281,41 @@ module WaterDrop
|
|
|
281
281
|
|
|
282
282
|
return unless rd_error.is_a?(Rdkafka::RdkafkaError)
|
|
283
283
|
return unless config.reload_on_transaction_fatal_error
|
|
284
|
-
return if
|
|
284
|
+
return if config.non_reloadable_errors.include?(rd_error.code)
|
|
285
285
|
|
|
286
286
|
# Check if we've exceeded max reload attempts
|
|
287
287
|
return unless transactional_retryable?
|
|
288
|
+
# We bubble up transactional errors, so there are cases where when fencing is not
|
|
289
|
+
# considered a non-reloadable, two layers of error handling would attempt to reload the
|
|
290
|
+
# client causing double reload. This halts reload if we're in a configured state as it
|
|
291
|
+
# means, we've already reloaded and we are not even yet connected
|
|
292
|
+
return if @status.configured?
|
|
288
293
|
|
|
289
294
|
# Increment attempts before reload
|
|
290
295
|
@transaction_fatal_error_attempts += 1
|
|
291
296
|
|
|
292
297
|
@operating_mutex.synchronize do
|
|
298
|
+
# Emit producer.reload event before reload
|
|
299
|
+
# Users can subscribe to this event and modify event[:caller].config.kafka to change
|
|
300
|
+
# producer config. This is useful for escaping fencing loops by changing transactional.id
|
|
301
|
+
@monitor.instrument(
|
|
302
|
+
'producer.reload',
|
|
303
|
+
producer_id: id,
|
|
304
|
+
error: rd_error,
|
|
305
|
+
attempt: @transaction_fatal_error_attempts,
|
|
306
|
+
caller: self
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Clear cached state that depends on config
|
|
310
|
+
# We always clear @transactional as it might have been modified via the event
|
|
311
|
+
@transactional = nil
|
|
312
|
+
|
|
293
313
|
@monitor.instrument(
|
|
294
314
|
'producer.reloaded',
|
|
295
315
|
producer_id: id,
|
|
296
316
|
attempt: @transaction_fatal_error_attempts
|
|
297
317
|
) do
|
|
298
|
-
|
|
299
|
-
purge
|
|
300
|
-
@client.close
|
|
301
|
-
@client = Builder.new.call(self, @config)
|
|
318
|
+
reload!
|
|
302
319
|
end
|
|
303
320
|
end
|
|
304
321
|
|
data/lib/waterdrop/producer.rb
CHANGED
|
@@ -22,12 +22,6 @@ module WaterDrop
|
|
|
22
22
|
Rdkafka::Producer::DeliveryHandle::WaitTimeoutError
|
|
23
23
|
].freeze
|
|
24
24
|
|
|
25
|
-
# We should never reload producer on certain fatal errors as they may indicate state that
|
|
26
|
-
# cannot be recovered by simply recreating the client
|
|
27
|
-
NON_RELOADABLE_FATAL_ERRORS = %i[
|
|
28
|
-
fenced
|
|
29
|
-
].freeze
|
|
30
|
-
|
|
31
25
|
# Empty hash to save on memory allocations
|
|
32
26
|
EMPTY_HASH = {}.freeze
|
|
33
27
|
|
|
@@ -35,7 +29,7 @@ module WaterDrop
|
|
|
35
29
|
EMPTY_ARRAY = [].freeze
|
|
36
30
|
|
|
37
31
|
private_constant(
|
|
38
|
-
:SUPPORTED_FLOW_ERRORS, :
|
|
32
|
+
:SUPPORTED_FLOW_ERRORS, :EMPTY_HASH, :EMPTY_ARRAY
|
|
39
33
|
)
|
|
40
34
|
|
|
41
35
|
def_delegators :config
|
|
@@ -508,7 +502,7 @@ module WaterDrop
|
|
|
508
502
|
)
|
|
509
503
|
|
|
510
504
|
# Attempt to reload the producer
|
|
511
|
-
idempotent_reload_client_on_fatal_error(@idempotent_fatal_error_attempts)
|
|
505
|
+
idempotent_reload_client_on_fatal_error(@idempotent_fatal_error_attempts, e)
|
|
512
506
|
|
|
513
507
|
# Wait before retrying to avoid rapid reload loops
|
|
514
508
|
sleep(@config.wait_backoff_on_idempotent_fatal_error / 1_000.0)
|
|
@@ -565,5 +559,15 @@ module WaterDrop
|
|
|
565
559
|
ensure
|
|
566
560
|
@operations_in_progress.decrement
|
|
567
561
|
end
|
|
562
|
+
|
|
563
|
+
# Reloads the client
|
|
564
|
+
# @note This should be used only within proper mutexes internally
|
|
565
|
+
def reload!
|
|
566
|
+
@client.flush(current_variant.max_wait_timeout)
|
|
567
|
+
purge
|
|
568
|
+
@client.close
|
|
569
|
+
@client = nil
|
|
570
|
+
@status.configured!
|
|
571
|
+
end
|
|
568
572
|
end
|
|
569
573
|
end
|
data/lib/waterdrop/version.rb
CHANGED
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.8.
|
|
4
|
+
version: 2.8.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -65,7 +65,6 @@ extensions: []
|
|
|
65
65
|
extra_rdoc_files: []
|
|
66
66
|
files:
|
|
67
67
|
- ".coditsu/ci.yml"
|
|
68
|
-
- ".diffend.yml"
|
|
69
68
|
- ".github/CODEOWNERS"
|
|
70
69
|
- ".github/FUNDING.yml"
|
|
71
70
|
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
data/.diffend.yml
DELETED