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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0890fcd73147d293340b55cca58c975d81726208c609be1f338ce56d87b4d18
4
- data.tar.gz: 7a2743c068af9936186fa852d6e663c85c172aa93de76b16aaf09e2c1de24a25
3
+ metadata.gz: da869aec5fa217a2adda674c787dd142a4f4e50e99a9b86d349d42f700821c88
4
+ data.tar.gz: 1934d9b60db038b6ea2f99faf7961a5f3ea2b3dfe845f84632db7f145f094cd2
5
5
  SHA512:
6
- metadata.gz: 66136311cb010a1648a0a5440cd930fcb96bb75fe30575d5c14effb8550a7cea4295dda98828790ac567ec9f3346dde7901c11228f6ca251fdf454a5354fad9d
7
- data.tar.gz: a701a63fe3a171084b63ac46ca9f66d75572b7efbfed0b794884df0fdb2b272404a6594ba239f16956cdc9c5fbecb31dbb5574fc0e5c1f12225ce61514c776a3
6
+ metadata.gz: c25b0221d070e1ba2b9badcff468caad02d4f4da158083a3514aa8a4d2b5b867579d6bebdaa35f45416c97de6bdddcb9c080a9d5399bca7f509e26e9f1775140
7
+ data.tar.gz: d2c32960c72034b00a48d3cd5b7ab30e12908638945f08aa69ddd27947b98cf38b9b3b0d3ccf78b02ce25b569a5ad551d1bedcbaf40b06df0b1caa7565567b56
@@ -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
- - [Change] Remove Ruby `3.1` according to the EOL schedule.
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
- - [Change] Remove Ruby `3.1` specs according to the EOL schedule.
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
@@ -2,12 +2,10 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- plugin 'diffend'
6
-
7
5
  gemspec
8
6
 
9
7
  # Relaxed from 2.7 because we support Ruby 3.1
10
- gem 'zeitwerk', '~> 2.6.18'
8
+ gem 'zeitwerk', '~> 2.7.0'
11
9
 
12
10
  group :development do
13
11
  gem 'byebug'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.8.12)
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.13.2)
28
- karafka-core (2.5.6)
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.21.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.21.0-aarch64-linux-gnu)
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.21.0-arm64-darwin)
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.21.0-x86_64-linux-gnu)
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.21.0-x86_64-linux-musl)
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.1)
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.4)
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.5)
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.4)
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.1)
82
+ simplecov-html (0.13.2)
83
83
  simplecov_json_formatter (0.1.4)
84
84
  warning (1.5.0)
85
- zeitwerk (2.6.18)
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.6.18)
108
+ zeitwerk (~> 2.7.0)
109
109
 
110
110
  BUNDLED WITH
111
111
  2.7.0
@@ -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
 
@@ -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
@@ -10,6 +10,7 @@ module WaterDrop
10
10
  producer.connected
11
11
  producer.closing
12
12
  producer.closed
13
+ producer.reload
13
14
  producer.reloaded
14
15
  producer.disconnecting
15
16
  producer.disconnected
@@ -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 NON_RELOADABLE_FATAL_ERRORS list
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 NON_RELOADABLE_FATAL_ERRORS.include?(error.code)
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
- @client.flush(current_variant.max_wait_timeout)
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 NON_RELOADABLE_FATAL_ERRORS.include?(rd_error.code)
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
- @client.flush(current_variant.max_wait_timeout)
299
- purge
300
- @client.close
301
- @client = Builder.new.call(self, @config)
318
+ reload!
302
319
  end
303
320
  end
304
321
 
@@ -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, :NON_RELOADABLE_FATAL_ERRORS, :EMPTY_HASH, :EMPTY_ARRAY
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
@@ -3,5 +3,5 @@
3
3
  # WaterDrop library
4
4
  module WaterDrop
5
5
  # Current WaterDrop version
6
- VERSION = '2.8.12'
6
+ VERSION = '2.8.13'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waterdrop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.12
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
@@ -1,3 +0,0 @@
1
- project_id: 'ee590bc9-f375-4832-ac8e-beb174b49aa5'
2
- shareable_id: '1325b111-b957-4fd8-bd6f-fead41e06034'
3
- shareable_key: 'd29c6230-8bf6-479b-83ff-98f374d3466e'