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 +4 -4
- data/CHANGELOG.md +10 -1
- data/Gemfile.lock +14 -14
- data/config/locales/errors.yml +2 -0
- data/lib/waterdrop/clients/buffered.rb +48 -30
- data/lib/waterdrop/clients/dummy.rb +13 -1
- data/lib/waterdrop/config.rb +9 -0
- data/lib/waterdrop/connection_pool.rb +1 -0
- data/lib/waterdrop/contracts/config.rb +4 -0
- data/lib/waterdrop/contracts/variant.rb +1 -0
- data/lib/waterdrop/errors.rb +2 -1
- data/lib/waterdrop/instrumentation/monitor.rb +1 -8
- data/lib/waterdrop/producer/buffer.rb +14 -0
- data/lib/waterdrop/producer/transactions.rb +2 -12
- data/lib/waterdrop/producer/variant/config_buffer.rb +40 -0
- data/lib/waterdrop/producer/variant.rb +10 -4
- data/lib/waterdrop/producer.rb +10 -6
- data/lib/waterdrop/version.rb +1 -1
- data/package-lock.json +3 -3
- data/renovate.json +18 -1
- data/waterdrop.gemspec +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf52057f67473d68a4ec20e57ac3d45f3ae84d5fe9dd1e42655ef7d02c1384a8
|
|
4
|
+
data.tar.gz: ad9cb21f3a98a8ddad666355e507839f38a3c08a02a14d2c9fed89260a2ba1a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
4
|
+
waterdrop (2.10.10)
|
|
5
5
|
karafka-core (>= 2.5.12, < 3.0.0)
|
|
6
|
-
karafka-rdkafka (>= 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
|
|
16
|
-
karafka-core (2.6.
|
|
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.
|
|
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
|
|
27
|
+
minitest (6.1.0)
|
|
28
28
|
drb (~> 2.0)
|
|
29
|
-
prism (
|
|
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.
|
|
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
|
|
59
|
-
karafka-core (2.6.
|
|
60
|
-
karafka-rdkafka (0.
|
|
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
|
|
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.
|
|
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.
|
|
71
|
+
waterdrop (2.10.10)
|
|
72
72
|
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
|
|
73
73
|
|
|
74
74
|
BUNDLED WITH
|
data/config/locales/errors.yml
CHANGED
|
@@ -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
|
-
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
40
|
-
|
|
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
|
-
@
|
|
52
|
+
@clients_mutex.synchronize do
|
|
53
|
+
@transaction_level -= 1
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
# Transfer transactional data on success
|
|
56
|
+
@transaction_topics.each do |topic, messages|
|
|
57
|
+
@topics[topic] += messages
|
|
58
|
+
end
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
@messages += @transaction_messages
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
@
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
data/lib/waterdrop/config.rb
CHANGED
|
@@ -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|
|
data/lib/waterdrop/errors.rb
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
112
|
+
#{target}.#{method_name}(*args, &block)
|
|
107
113
|
ensure
|
|
108
114
|
had ? (ref[@producer.id] = prev) : ref.delete(@producer.id)
|
|
109
115
|
end
|
data/lib/waterdrop/producer.rb
CHANGED
|
@@ -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
|
|
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 [
|
|
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
|
-
|
|
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]
|
|
673
|
+
message[:topic_config] ||= variant.topic_config
|
|
670
674
|
end
|
|
671
675
|
|
|
672
676
|
result = if transactional?
|
data/lib/waterdrop/version.rb
CHANGED
data/package-lock.json
CHANGED
|
@@ -286,9 +286,9 @@
|
|
|
286
286
|
}
|
|
287
287
|
},
|
|
288
288
|
"node_modules/smol-toml": {
|
|
289
|
-
"version": "1.9.
|
|
290
|
-
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.
|
|
291
|
-
"integrity": "sha512-
|
|
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": [
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|