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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a34bd3898b2807134dc7941c0b22e66933a242de7ecf1e7cc7e1c76a5f23267
4
- data.tar.gz: 8ca1ca970bc65ea3d27ca8c529f493c33b95388304dd936402d1562e2f151c79
3
+ metadata.gz: 0e6efd47232839d14e444c45df4cc5fae39661dd7e8ef646639f00d41f62d3b3
4
+ data.tar.gz: edd8560d05bb3742d8592a1b90f2dc784410e7150045e8a8d4a0ebf1c0d82e81
5
5
  SHA512:
6
- metadata.gz: 5357203176e130d237e79fe7e936ac547874d16a9bb6f3d96e30d57b0c24523508ebc6754086c01fdeb7ec33542d48bc95c2cccd74887e6ced30e8484625a5f7
7
- data.tar.gz: 6e6ec7ad65759afb4a0aa923a372e1a4e00cf447dd7c9ed00805f8ddf7e59d0dea92b7edc819d8d86c570128e0997faf23cae3c9c9ea97d8706866b5d0baccde
6
+ metadata.gz: 1532bf1ecbb02ec192bdd8a410b5e7e7579337e11515fe828fb88bd8cf2a2168e31ef669e9d3ba1c334e64720c148069d26b5259d7c3f913d80258d5ddbb711b
7
+ data.tar.gz: 87178e647ac42309e0cdbef6c48af8c0f7a902ee32238c3c5cf8cbca2ee7ae80396895bb3a70417c912f98f669834e19ce1066c596a7a5432c7245b29a537248
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.7
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 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.
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 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.
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 pending in the librdkafka queue. This counts messages that have been dispatched to librdkafka but not yet transmitted to the Kafka broker.
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` method as the preferred alias for `WaterDrop.instrumentation` to align with per-producer API naming convention. The `instrumentation` method remains available as a deprecated alias for backward compatibility.
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 for improved observability. Events include `connection_pool.created`, `connection_pool.setup`, `connection_pool.shutdown`, `connection_pool.reload`, and `connection_pool.reloaded`.
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
@@ -4,12 +4,9 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem "rake"
7
8
  gem "zeitwerk", "~> 2.8.0"
8
9
 
9
- group :development do
10
- gem "byebug"
11
- end
12
-
13
10
  group :test do
14
11
  gem "connection_pool"
15
12
  gem "ostruct"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.10.7)
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.7)
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 topic [String]
79
- def messages_for(topic)
80
- @topics[topic]
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 topic [String] topic where we want to dispatch message
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(topic, partition, offset)
17
- @topic = topic
16
+ def initialize(topic_name, partition, offset)
17
+ @topic = topic_name
18
18
  @partition = partition
19
19
  @offset = offset
20
20
  end
@@ -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
- # - This is an unrecoverable state - reloading won't help as the other producer is active
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 you have explicit logic to handle producer fencing
138
- # in your application (e.g., coordinated transactional.id assignment, manual intervention).
139
- # In most cases, you should keep the default value.
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
@@ -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 = Class.new(BaseError)
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 topic [String] Kafka topic
193
+ # @param topic_name [String] Kafka topic
194
194
  # @param method_name [Symbol] method from which this message operation comes
195
- def report_message(topic, method_name)
196
- increment(method_name, tags: default_tags + ["topic:#{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
- requeued = @requeued
103
- @requeued = []
104
- fresh = @messages
105
- @messages = []
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. Re-buffer the messages that never reached
146
- # librdkafka so a partial failure does not silently drop valid buffered messages. For a
147
- # transactional producer the whole batch is rolled back (nothing is visible to consumers),
148
- # so all of it is restored; for a regular producer `e.dispatched` holds the handles already
149
- # created, so only the remainder is restored.
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
- @messages.unshift(*fresh)
170
- @requeued.unshift(*requeued)
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 { @requeued.unshift(*messages) }
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) { client.begin_transaction }
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
@@ -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 topic [String] topic for which we want to get the number of partitions
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(topic)
230
- client.partition_count(topic.to_s)
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
- @messages = []
242
- @requeued = []
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module WaterDrop
4
4
  # Current WaterDrop version
5
- VERSION = "2.10.7"
5
+ VERSION = "2.10.9"
6
6
  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.10.7
4
+ version: 2.10.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld