waterdrop 2.10.9 → 2.10.10

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: 0e6efd47232839d14e444c45df4cc5fae39661dd7e8ef646639f00d41f62d3b3
4
- data.tar.gz: edd8560d05bb3742d8592a1b90f2dc784410e7150045e8a8d4a0ebf1c0d82e81
3
+ metadata.gz: cf52057f67473d68a4ec20e57ac3d45f3ae84d5fe9dd1e42655ef7d02c1384a8
4
+ data.tar.gz: ad9cb21f3a98a8ddad666355e507839f38a3c08a02a14d2c9fed89260a2ba1a3
5
5
  SHA512:
6
- metadata.gz: 1532bf1ecbb02ec192bdd8a410b5e7e7579337e11515fe828fb88bd8cf2a2168e31ef669e9d3ba1c334e64720c148069d26b5259d7c3f913d80258d5ddbb711b
7
- data.tar.gz: 87178e647ac42309e0cdbef6c48af8c0f7a902ee32238c3c5cf8cbca2ee7ae80396895bb3a70417c912f98f669834e19ce1066c596a7a5432c7245b29a537248
6
+ metadata.gz: 472757b2946e50858b02601f71976bcafbd496cd8674e3863068725ef4dbf750d850add9553eed3e1515563fd6275a409d05a214120b2eaef51b06c8c4376543
7
+ data.tar.gz: bc795ef820ae4c995f5f770858f674e3069dac85e1819ed72c02ab34aa1dba74099a5ab7a8485e69bad90a1adb8cebd4855b7de45931d535aa24bea6622d598b
data/CHANGELOG.md CHANGED
@@ -1,12 +1,21 @@
1
1
  # WaterDrop changelog
2
2
 
3
+ ## v2.10.10 (2026-11-10)
4
+ - [Feature] Add the `partition_count_cache_ttl_ms` setting to configure the process-wide karafka-rdkafka partition count cache TTL (default `nil` leaves it untouched).
5
+ - [Change] Require `karafka-rdkafka` `>=` `0.32.0`.
6
+ - [Change] Use the producer id (`ProducerNotTransactionalError`, `EarlyTransactionExitNotAllowedError`) or the event id (`StatisticsNotEnabledError`) as the error message instead of a sentence.
7
+ - [Maintenance] Document the errors raised by `ensure_active!` and `ensure_connection_pool_gem!`, and fix the `MessageInvalidError` namespace in `validate_message!` YARD.
8
+ - [Maintenance] Document that `max_wait_timeout` is omitted, not set to `nil`, to use the producer default in `Producer#with` and `Variant#initialize` YARD.
9
+ - [Fix] Send a message buffered through a variant with that variant's `topic_config` at flush, and a message buffered through the producer with the default one, whichever producer or variant flushes the buffer.
10
+ - [Fix] Raise `VariantInvalidError` instead of `NoMethodError` when `Producer#with` gets a `topic_config` that is not a hash (e.g. `nil`).
11
+ - [Fix] Make `Clients::Buffered` and `Clients::Dummy` safe to produce to from many threads, so on JRuby no buffered message is lost and no offset is assigned twice.
12
+
3
13
  ## 2.10.9 (2026-11-02)
4
14
  - [Maintenance] Rename the positional topic name parameters of `Producer#partition_count`, `Clients::Buffered#messages_for` and the Datadog metrics listener to `topic_name`.
5
15
  - [Maintenance] Rename the positional topic name parameter of `Clients::Dummy::Handle#initialize` to `topic_name`.
6
16
  - [Maintenance] Declare `rake` in the `Gemfile`. It came only through `karafka-rdkafka`, which will stop depending on it for precompiled platform gems.
7
17
  - [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
18
  - [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
19
 
11
20
  ## 2.10.8 (2026-10-25)
12
21
  - [Maintenance] Document that a transaction timeout also raises `:fenced`, indistinguishable from a takeover, and when removing `:fenced` from `non_reloadable_errors` is safe.
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.10.9)
4
+ waterdrop (2.10.10)
5
5
  karafka-core (>= 2.5.12, < 3.0.0)
6
- karafka-rdkafka (>= 0.31.0)
6
+ karafka-rdkafka (>= 0.32.0)
7
7
  zeitwerk (~> 2.3)
8
8
 
9
9
  GEM
@@ -12,11 +12,11 @@ GEM
12
12
  connection_pool (3.0.2)
13
13
  drb (2.2.3)
14
14
  ffi (1.17.4)
15
- json (3.0.2)
16
- karafka-core (2.6.4)
15
+ json (3.1.0)
16
+ karafka-core (2.6.6)
17
17
  karafka-rdkafka (>= 0.20.0)
18
18
  logger (>= 1.6.0)
19
- karafka-rdkafka (0.31.0)
19
+ karafka-rdkafka (0.32.0)
20
20
  ffi (~> 1.17.1)
21
21
  json (> 2.0)
22
22
  logger
@@ -24,13 +24,13 @@ GEM
24
24
  rake (> 12)
25
25
  logger (1.7.0)
26
26
  mini_portile2 (2.8.9)
27
- minitest (6.0.6)
27
+ minitest (6.1.0)
28
28
  drb (~> 2.0)
29
- prism (~> 1.5)
29
+ prism (>= 1.5, < 3)
30
30
  mocha (3.1.0)
31
31
  ruby2_keywords (>= 0.0.5)
32
32
  ostruct (0.6.3)
33
- prism (1.9.0)
33
+ prism (2.1.0)
34
34
  rake (13.4.2)
35
35
  ruby2_keywords (0.0.5)
36
36
  simplecov (1.3.2)
@@ -55,20 +55,20 @@ CHECKSUMS
55
55
  connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
56
56
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
57
57
  ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
58
- json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
59
- karafka-core (2.6.4) sha256=e08166ca7ef12fadf0500f2a3aba63b5ba918a98533c3feaac9f43c90f159363
60
- karafka-rdkafka (0.31.0) sha256=7d21274122d66ee8d08b8e56a0d06084933deb2306cf2a6e4bd0d073c14e7f9f
58
+ json (3.1.0) sha256=3ce7ccf8fde58960177a3a756c31f292a4c1e1dedee884547096a5b0604e3bd8
59
+ karafka-core (2.6.6) sha256=be14a9898c2133eecf1e2edefbdc127d105d902360d93c4ce63328ad8e74a440
60
+ karafka-rdkafka (0.32.0) sha256=07983e15df25e69c958b991fe50ea85f2b6170821816eb05248f77a9de7ffa2b
61
61
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
62
62
  mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
63
- minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
63
+ minitest (6.1.0) sha256=ff373595aa8888631e8a5463986a76b3845346ae6d7fa46e139c1633d418076e
64
64
  mocha (3.1.0) sha256=75f42d69ebfb1f10b32489dff8f8431d37a418120ecdfc07afe3bc183d4e1d56
65
65
  ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
66
- prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
66
+ prism (2.1.0) sha256=1c0a7c6dea0e93183390fb123f007564dbe663198e2f86ad87204151147601f3
67
67
  rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
68
68
  ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
69
69
  simplecov (1.3.2) sha256=2a22ec1759f1b8db60881a2e038758956fc69ef93ed669be0bde0678de6d0e85
70
70
  warning (1.6.0) sha256=a49cdfae19fb77d19afff2efbe45f8ab759e9cd25b4e4ce2c79dbaf46bdb6c9e
71
- waterdrop (2.10.9)
71
+ waterdrop (2.10.10)
72
72
  zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
73
73
 
74
74
  BUNDLED WITH
@@ -26,6 +26,7 @@ en:
26
26
  non_reloadable_errors_format: must be an array of symbols
27
27
  oauth.token_provider_listener_format: 'must be false or respond to #on_oauthbearer_token_refresh'
28
28
  idle_disconnect_timeout_format: 'must be an integer that is equal to 0 or bigger than 30 000 (30 seconds)'
29
+ partition_count_cache_ttl_ms_format: must be nil or an integer that is equal or bigger than 1
29
30
  statistics_decorator_format: 'must respond to #call'
30
31
  polling.mode_format: must be :thread or :fd
31
32
  polling.poller_format: must be nil or a WaterDrop::Polling::Poller instance
@@ -44,6 +45,7 @@ en:
44
45
  missing: must be present
45
46
  default_format: must be boolean
46
47
  max_wait_timeout_format: must be an integer that is equal or bigger than 0
48
+ topic_config_format: must be a hash
47
49
  kafka_key_must_be_a_symbol: All keys under the kafka settings scope need to be symbols
48
50
  kafka_key_not_per_topic: This config option cannot be set on a per topic basis
49
51
  kafka_key_acks_not_changeable: Acks value cannot be redefined for a transactional or idempotent producer
@@ -3,6 +3,9 @@
3
3
  module WaterDrop
4
4
  module Clients
5
5
  # Client used to buffer messages that we send out in specs and other places.
6
+ #
7
+ # It is safe to produce from many threads. Readers (`#messages`, `#messages_for`) return the
8
+ # internal buffers, not copies, so read them once producing is done or `dup` them.
6
9
  class Buffered < Clients::Dummy
7
10
  attr_accessor :messages
8
11
 
@@ -22,38 +25,44 @@ module WaterDrop
22
25
  # @param message [Hash] `WaterDrop::Producer#produce_sync` message hash
23
26
  # @return [Dummy::Handle] fake delivery handle that can be materialized into a report
24
27
  def produce(message)
25
- if @transaction_active
26
- @transaction_topics[message.fetch(:topic)] << message
27
- @transaction_messages << message
28
- else
29
- # We pre-validate the message payload, so topic is ensured to be present
30
- @topics[message.fetch(:topic)] << message
31
- @messages << message
32
- end
28
+ @clients_mutex.synchronize do
29
+ if @transaction_active
30
+ @transaction_topics[message.fetch(:topic)] << message
31
+ @transaction_messages << message
32
+ else
33
+ # We pre-validate the message payload, so topic is ensured to be present
34
+ @topics[message.fetch(:topic)] << message
35
+ @messages << message
36
+ end
33
37
 
34
- super(**message.to_h)
38
+ build_handle(message.fetch(:topic), message.fetch(:partition, 0))
39
+ end
35
40
  end
36
41
 
37
42
  # Starts the transaction on a given level
38
43
  def begin_transaction
39
- @transaction_level += 1
40
- @transaction_active = true
44
+ @clients_mutex.synchronize do
45
+ @transaction_level += 1
46
+ @transaction_active = true
47
+ end
41
48
  end
42
49
 
43
50
  # Finishes given level of transaction
44
51
  def commit_transaction
45
- @transaction_level -= 1
52
+ @clients_mutex.synchronize do
53
+ @transaction_level -= 1
46
54
 
47
- # Transfer transactional data on success
48
- @transaction_topics.each do |topic, messages|
49
- @topics[topic] += messages
50
- end
55
+ # Transfer transactional data on success
56
+ @transaction_topics.each do |topic, messages|
57
+ @topics[topic] += messages
58
+ end
51
59
 
52
- @messages += @transaction_messages
60
+ @messages += @transaction_messages
53
61
 
54
- @transaction_topics.clear
55
- @transaction_messages.clear
56
- @transaction_active = false
62
+ @transaction_topics.clear
63
+ @transaction_messages.clear
64
+ @transaction_active = false
65
+ end
57
66
  end
58
67
 
59
68
  # Fakes storing the offset in a transactional fashion
@@ -68,26 +77,35 @@ module WaterDrop
68
77
 
69
78
  # Aborts the transaction
70
79
  def abort_transaction
71
- @transaction_level -= 1
72
- @transaction_topics.clear
73
- @transaction_messages.clear
74
- @transaction_active = false
80
+ @clients_mutex.synchronize do
81
+ @transaction_level -= 1
82
+ @transaction_topics.clear
83
+ @transaction_messages.clear
84
+ @transaction_active = false
85
+ end
75
86
  end
76
87
 
77
88
  # Returns messages produced to a given topic
78
89
  # @param topic_name [String]
79
90
  def messages_for(topic_name)
91
+ @clients_mutex.synchronize { @topics[topic_name] }
92
+ rescue ThreadError => e
93
+ # Mutex#synchronize raises in a signal trap, so there the buffer is read without the lock.
94
+ raise unless e.message == "can't be called from trap context"
95
+
80
96
  @topics[topic_name]
81
97
  end
82
98
 
83
99
  # Clears internal buffer. Used in between specs so messages do not leak out
84
100
  def reset
85
- @transaction_level = 0
86
- @transaction_active = false
87
- @transaction_topics.clear
88
- @transaction_messages.clear
89
- @messages.clear
90
- @topics.each_value(&:clear)
101
+ @clients_mutex.synchronize do
102
+ @transaction_level = 0
103
+ @transaction_active = false
104
+ @transaction_topics.clear
105
+ @transaction_messages.clear
106
+ @messages.clear
107
+ @topics.each_value(&:clear)
108
+ end
91
109
  end
92
110
  end
93
111
  end
@@ -43,6 +43,7 @@ module WaterDrop
43
43
  # @return [Dummy] dummy instance
44
44
  def initialize(_producer)
45
45
  @counters = Hash.new { |h, k| h[k] = -1 }
46
+ @clients_mutex = Mutex.new
46
47
  end
47
48
 
48
49
  # "Produces" the message
@@ -54,7 +55,7 @@ module WaterDrop
54
55
  # @option _args [Hash, nil] :headers message headers
55
56
  # @return [Handle] delivery handle
56
57
  def produce(topic:, partition: 0, **_args)
57
- Handle.new(topic.to_s, partition, @counters["#{topic}#{partition}"] += 1)
58
+ @clients_mutex.synchronize { build_handle(topic, partition) }
58
59
  end
59
60
 
60
61
  # Returns 0 as dummy client doesn't queue any real messages
@@ -76,6 +77,17 @@ module WaterDrop
76
77
  def method_missing(*_args)
77
78
  self || super
78
79
  end
80
+
81
+ private
82
+
83
+ # Assigns the next offset of the given topic partition. Caller must hold `@clients_mutex`.
84
+ #
85
+ # @param topic [String, Symbol] topic where we want to dispatch message
86
+ # @param partition [Integer] target partition
87
+ # @return [Handle] delivery handle
88
+ def build_handle(topic, partition)
89
+ Handle.new(topic.to_s, partition, @counters["#{topic}#{partition}"] += 1)
90
+ end
79
91
  end
80
92
  end
81
93
  end
@@ -149,6 +149,11 @@ module WaterDrop
149
149
  # TCP connections in low-intensity scenarios. Minimum value is 30 seconds (30_000 ms) to
150
150
  # prevent overly aggressive disconnections.
151
151
  setting :idle_disconnect_timeout, default: 0
152
+ # option [Integer, nil] Time-to-live (in ms) of the partition count cache used to route
153
+ # messages with `partition_key`. When `nil` (default), the karafka-rdkafka value is left
154
+ # untouched. Due to how rdkafka works, this setting is global for all the producers and the
155
+ # last producer set up with a non-nil value wins.
156
+ setting :partition_count_cache_ttl_ms, default: nil
152
157
 
153
158
  # option [Karafka::Core::Monitoring::StatisticsDecorator] decorator instance used to compute
154
159
  # deltas (_d) and freeze durations (_fd) on raw librdkafka statistics. The default is
@@ -230,6 +235,10 @@ module WaterDrop
230
235
  Contracts::Config.new.validate!(config.to_h, Errors::ConfigurationInvalidError)
231
236
 
232
237
  ::Rdkafka::Config.logger = config.logger
238
+
239
+ if config.partition_count_cache_ttl_ms
240
+ ::Rdkafka::Config.partition_count_cache_ttl_ms = config.partition_count_cache_ttl_ms
241
+ end
233
242
  end
234
243
 
235
244
  self
@@ -172,6 +172,7 @@ module WaterDrop
172
172
 
173
173
  # Ensures the connection_pool gem is available (class method)
174
174
  # Only requires it when actually needed (lazy loading)
175
+ # @raise [LoadError] when the connection_pool gem is not available
175
176
  def ensure_connection_pool_gem!
176
177
  return if defined?(::ConnectionPool)
177
178
 
@@ -39,6 +39,10 @@ module WaterDrop
39
39
  val.is_a?(Integer) && (val.zero? || val >= 30_000)
40
40
  end
41
41
 
42
+ required(:partition_count_cache_ttl_ms) do |val|
43
+ val.nil? || (val.is_a?(Integer) && val.positive?)
44
+ end
45
+
42
46
  required(:statistics_decorator) { |val| val.respond_to?(:call) }
43
47
 
44
48
  nested(:oauth) do
@@ -34,6 +34,7 @@ module WaterDrop
34
34
 
35
35
  required(:default) { |val| BOOLEANS.include?(val) }
36
36
  required(:max_wait_timeout) { |val| val.is_a?(Numeric) && val >= 0 }
37
+ required(:topic_config) { |val| val.is_a?(Hash) }
37
38
 
38
39
  # Checks if all keys are symbols
39
40
  virtual do |config, errors|
@@ -63,7 +63,8 @@ module WaterDrop
63
63
  # graceful transaction aborting
64
64
  AbortTransaction = Class.new(BaseError)
65
65
 
66
- # Do not use `break`, `return` or `throw` inside of the transaction blocks
66
+ # Do not use `break`, `return` or `throw` inside of the transaction blocks. If the `throw`
67
+ # comes from `Timeout.timeout(duration)`, pass an exception class as its second argument
67
68
  EarlyTransactionExitNotAllowedError = Class.new(BaseError)
68
69
 
69
70
  # Raised when an error occurs in the polling loop
@@ -46,14 +46,7 @@ module WaterDrop
46
46
  # `statistics.emitted` and this monitor has been frozen for statistics
47
47
  def subscribe(event_id_or_listener, &block)
48
48
  if @statistics_listeners_frozen && targets_statistics?(event_id_or_listener, block)
49
- raise Errors::StatisticsNotEnabledError, <<~MSG.tr("\n", " ").strip
50
- Cannot subscribe to `statistics.emitted` after the producer has been connected.
51
- Statistics are disabled because no listener was subscribed before the underlying
52
- rdkafka client was built, so librdkafka is not emitting statistics at all.
53
- Subscribe your listener BEFORE the first producer use (before the underlying
54
- client is lazily initialized), or explicitly keep statistics enabled by leaving
55
- a listener in place at build time.
56
- MSG
49
+ raise Errors::StatisticsNotEnabledError, STATISTICS_EVENT
57
50
  end
58
51
 
59
52
  super
@@ -139,6 +139,20 @@ module WaterDrop
139
139
  # Do nothing if nothing to flush
140
140
  return data_for_dispatch if data_for_dispatch.empty?
141
141
 
142
+ # Messages buffered through a variant carry its topic config (see Variant#buffer). The rest
143
+ # were buffered through the producer and must not pick up the flushing variant's one.
144
+ unless current_variant.default?
145
+ default_topic_config = @default_variant.topic_config
146
+
147
+ data_for_dispatch = data_for_dispatch.map do |message|
148
+ next message if message.key?(:topic_config)
149
+
150
+ message = message.dup
151
+ message[:topic_config] = default_topic_config
152
+ message
153
+ end
154
+ end
155
+
142
156
  if sync
143
157
  produce_many_sync(data_for_dispatch, run_middleware: false)
144
158
  else
@@ -50,10 +50,7 @@ module WaterDrop
50
50
  # handler.wait
51
51
  def transaction
52
52
  unless transactional?
53
- raise(
54
- Errors::ProducerNotTransactionalError,
55
- "#{id} is not transactional"
56
- )
53
+ raise(Errors::ProducerNotTransactionalError, id)
57
54
  end
58
55
 
59
56
  # This will safely allow us to support one operation transactions so a transactional
@@ -83,14 +80,7 @@ module WaterDrop
83
80
  raise(e)
84
81
  ensure
85
82
  if !e && !finished
86
- raise(
87
- Errors::EarlyTransactionExitNotAllowedError,
88
- <<~ERROR_MSG.tr("\n", " ")
89
- Using `return`, `break` or `throw` to exit a transaction block is not allowed.
90
- If the `throw` came from `Timeout.timeout(duration)`, pass an exception class as
91
- a second argument so it doesn't use `throw` to abort its block.
92
- ERROR_MSG
93
- )
83
+ raise(Errors::EarlyTransactionExitNotAllowedError, id)
94
84
  end
95
85
  end
96
86
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ class Producer
5
+ class Variant < SimpleDelegator
6
+ # Buffers belong to the producer, so another variant or the producer itself may flush them.
7
+ # A message buffered through a variant keeps its topic config, so it is sent with it whoever
8
+ # flushes. This lives here and not in Buffer#buffer because resolving `current_variant` there
9
+ # would cost every default-producer #buffer call (~13%).
10
+ class ConfigBuffer
11
+ # @param producer [WaterDrop::Producer] producer that owns the buffer
12
+ # @param topic_config [Hash] topic config of the variant buffering the messages
13
+ def initialize(producer, topic_config)
14
+ @producer = producer
15
+ @topic_config = topic_config
16
+ end
17
+
18
+ # @param message [Hash] message to buffer
19
+ def buffer(message)
20
+ @producer.buffer(with_topic_config(message))
21
+ end
22
+
23
+ # @param messages [Array<Hash>] messages to buffer
24
+ def buffer_many(messages)
25
+ @producer.buffer_many(messages.map { |message| with_topic_config(message) })
26
+ end
27
+
28
+ private
29
+
30
+ # @param message [Hash] user message, never changed in place
31
+ # @return [Hash] copy of the message with the variant topic config
32
+ def with_topic_config(message)
33
+ message = message.dup
34
+ message[:topic_config] = @topic_config
35
+ message
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -37,13 +37,16 @@ module WaterDrop
37
37
  # Contract to validate that variant alteration data is correct
38
38
  CONTRACT = Contracts::Variant.new
39
39
 
40
- private_constant :EMPTY_HASH, :CONTRACT
40
+ # Methods a non-default variant runs through its ConfigBuffer
41
+ CONFIG_BUFFERED = %i[buffer buffer_many].freeze
42
+
43
+ private_constant :EMPTY_HASH, :CONTRACT, :CONFIG_BUFFERED, :ConfigBuffer
41
44
 
42
45
  attr_reader :max_wait_timeout, :topic_config, :producer
43
46
 
44
47
  # @param producer [WaterDrop::Producer] producer for which we want to have a variant
45
- # @param max_wait_timeout [Integer, nil] alteration to max wait timeout or nil to use
46
- # default
48
+ # @param max_wait_timeout [Integer] alteration to max wait timeout. Omit it to use the
49
+ # producer default
47
50
  # @param topic_config [Hash] extra topic configuration that can be altered.
48
51
  # @param default [Boolean] is this a default variant or an altered one
49
52
  # @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties
@@ -57,6 +60,7 @@ module WaterDrop
57
60
  @max_wait_timeout = max_wait_timeout
58
61
  @topic_config = topic_config
59
62
  @default = default
63
+ @buffer_target = default ? producer : ConfigBuffer.new(producer, topic_config)
60
64
  super(producer)
61
65
 
62
66
  CONTRACT.validate!(to_h, Errors::VariantInvalidError)
@@ -78,6 +82,8 @@ module WaterDrop
78
82
  Transactions
79
83
  ].each do |scope|
80
84
  scope.instance_methods(false).each do |method_name|
85
+ target = CONFIG_BUFFERED.include?(method_name) ? "@buffer_target" : "@producer"
86
+
81
87
  # We save and restore any variant already active for this producer in this fiber rather
82
88
  # than unconditionally deleting it. A variant-wrapped method that yields user code (e.g.
83
89
  # `transaction`) may wrap a nested same-producer variant call; without save/restore the
@@ -103,7 +109,7 @@ module WaterDrop
103
109
  prev = ref[@producer.id]
104
110
  ref[@producer.id] = self
105
111
 
106
- @producer.#{method_name}(*args, &block)
112
+ #{target}.#{method_name}(*args, &block)
107
113
  ensure
108
114
  had ? (ref[@producer.id] = prev) : ref.delete(@producer.id)
109
115
  end
@@ -268,8 +268,8 @@ module WaterDrop
268
268
  # Builds the variant alteration and returns it.
269
269
  #
270
270
  # @param args [Hash] variant configuration options
271
- # @option args [Integer, nil] :max_wait_timeout alteration to max wait timeout or nil to use
272
- # default
271
+ # @option args [Integer] :max_wait_timeout alteration to max wait timeout. Omit it to use the
272
+ # producer default
273
273
  # @option args [Hash] :topic_config extra topic configuration that can be altered
274
274
  # @option args [Boolean] :default is this a default variant or an altered one
275
275
  # @return [WaterDrop::Producer::Variant] variant proxy to use with alterations
@@ -570,6 +570,9 @@ module WaterDrop
570
570
 
571
571
  # Ensures that we don't run any operations when the producer is not configured or when it
572
572
  # was already closed
573
+ # @raise [Errors::ProducerNotConfiguredError] when the producer is not configured
574
+ # @raise [Errors::ProducerClosedError] when the producer is closing or closed
575
+ # @raise [Errors::StatusInvalidError] when the producer is in an unknown state
573
576
  def ensure_active!
574
577
  # Capture the lifecycle state once. Another thread may be transitioning the producer between
575
578
  # states (for example configured -> connected while reloading the client after a fatal error),
@@ -591,7 +594,7 @@ module WaterDrop
591
594
  # Ensures that the message we want to send out to Kafka is actually valid and that it can be
592
595
  # sent there
593
596
  # @param message [Hash] message we want to send
594
- # @raise [Karafka::Errors::MessageInvalidError]
597
+ # @raise [Errors::MessageInvalidError]
595
598
  def validate_message!(message)
596
599
  @contract.validate!(message, Errors::MessageInvalidError)
597
600
  end
@@ -660,13 +663,14 @@ module WaterDrop
660
663
  # We basically only duplicate the message hash only if it is needed.
661
664
  # It is needed when user is using a custom settings variant or when symbol is provided as
662
665
  # the topic name. We should never mutate user input message as it may be a hash that the
663
- # user is using for some other operations
664
- if message[:topic].is_a?(Symbol) || !variant.default?
666
+ # user is using for some other operations. A message buffered through a variant already
667
+ # carries that variant's topic config, so it needs no copy for it
668
+ if message[:topic].is_a?(Symbol) || (!variant.default? && !message.key?(:topic_config))
665
669
  message = message.dup
666
670
  # In case someone defines topic as a symbol, we need to convert it into a string as
667
671
  # librdkafka does not accept symbols
668
672
  message[:topic] = message[:topic].to_s
669
- message[:topic_config] = variant.topic_config
673
+ message[:topic_config] ||= variant.topic_config
670
674
  end
671
675
 
672
676
  result = if transactional?
@@ -2,5 +2,5 @@
2
2
 
3
3
  module WaterDrop
4
4
  # Current WaterDrop version
5
- VERSION = "2.10.9"
5
+ VERSION = "2.10.10"
6
6
  end
data/package-lock.json CHANGED
@@ -286,9 +286,9 @@
286
286
  }
287
287
  },
288
288
  "node_modules/smol-toml": {
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==",
289
+ "version": "1.9.1",
290
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz",
291
+ "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==",
292
292
  "dev": true,
293
293
  "license": "BSD-3-Clause",
294
294
  "engines": {
data/renovate.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "bundler": {
3
- "managerFilePatterns": ["/(^|/)Gemfile\\.lint$/"]
3
+ "managerFilePatterns": [
4
+ "/(^|/)Gemfile\\.lint$/"
5
+ ]
4
6
  },
5
7
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
6
8
  "extends": [
@@ -51,6 +53,13 @@
51
53
  "ruby/setup-ruby"
52
54
  ],
53
55
  "minimumReleaseAge": "5 days"
56
+ },
57
+ {
58
+ "description": "Major updates stay manual",
59
+ "matchUpdateTypes": [
60
+ "major"
61
+ ],
62
+ "automerge": false
54
63
  }
55
64
  ],
56
65
  "minimumReleaseAge": "7 days",
@@ -62,5 +71,13 @@
62
71
  "schedule": [
63
72
  "before 4am on the first day of the month"
64
73
  ]
74
+ },
75
+ "automerge": true,
76
+ "platformAutomerge": true,
77
+ "osvVulnerabilityAlerts": true,
78
+ "vulnerabilityAlerts": {
79
+ "labels": [
80
+ "security"
81
+ ]
65
82
  }
66
83
  }
data/waterdrop.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.licenses = %w[LGPL-3.0-only Commercial]
18
18
 
19
19
  spec.add_dependency "karafka-core", ">= 2.5.12", "< 3.0.0"
20
- spec.add_dependency "karafka-rdkafka", ">= 0.31.0"
20
+ spec.add_dependency "karafka-rdkafka", ">= 0.32.0"
21
21
  spec.add_dependency "zeitwerk", "~> 2.3"
22
22
 
23
23
  spec.required_ruby_version = ">= 3.3.0"
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.9
4
+ version: 2.10.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -35,14 +35,14 @@ dependencies:
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: 0.31.0
38
+ version: 0.32.0
39
39
  type: :runtime
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
- version: 0.31.0
45
+ version: 0.32.0
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: zeitwerk
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -130,6 +130,7 @@ files:
130
130
  - lib/waterdrop/producer/tombstone.rb
131
131
  - lib/waterdrop/producer/transactions.rb
132
132
  - lib/waterdrop/producer/variant.rb
133
+ - lib/waterdrop/producer/variant/config_buffer.rb
133
134
  - lib/waterdrop/version.rb
134
135
  - package-lock.json
135
136
  - package.json