waterdrop 2.6.14 → 2.8.2
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
- checksums.yaml.gz.sig +0 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/ci.yml +11 -19
- data/.ruby-version +1 -1
- data/CHANGELOG.md +256 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +35 -31
- data/LICENSE +17 -0
- data/README.md +4 -4
- data/bin/verify_kafka_warnings +31 -0
- data/certs/cert.pem +26 -0
- data/config/locales/errors.yml +17 -3
- data/docker-compose.yml +1 -3
- data/lib/waterdrop/clients/rdkafka.rb +34 -4
- data/lib/waterdrop/config.rb +27 -12
- data/lib/waterdrop/contracts/config.rb +12 -1
- data/lib/waterdrop/contracts/variant.rb +107 -0
- data/lib/waterdrop/errors.rb +16 -0
- data/lib/waterdrop/instrumentation/callbacks/delivery.rb +11 -0
- data/lib/waterdrop/instrumentation/callbacks/error.rb +11 -0
- data/lib/waterdrop/instrumentation/callbacks/oauthbearer_token_refresh.rb +49 -0
- data/lib/waterdrop/instrumentation/callbacks/statistics.rb +11 -0
- data/lib/waterdrop/instrumentation/logger_listener.rb +11 -3
- data/lib/waterdrop/instrumentation/notifications.rb +3 -0
- data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +10 -1
- data/lib/waterdrop/producer/buffer.rb +0 -6
- data/lib/waterdrop/producer/sync.rb +33 -9
- data/lib/waterdrop/producer/transactions.rb +98 -19
- data/lib/waterdrop/producer/variant.rb +109 -0
- data/lib/waterdrop/producer.rb +155 -92
- data/lib/waterdrop/version.rb +1 -1
- data/lib/waterdrop.rb +1 -0
- data/waterdrop.gemspec +7 -4
- data.tar.gz.sig +0 -0
- metadata +51 -33
- metadata.gz.sig +0 -0
- data/MIT-LICENSE +0 -18
- data/certs/cert_chain.pem +0 -26
|
@@ -4,10 +4,16 @@ module WaterDrop
|
|
|
4
4
|
class Producer
|
|
5
5
|
# Transactions related producer functionalities
|
|
6
6
|
module Transactions
|
|
7
|
+
# We should never reload producer if it was fenced, otherwise we could end up with some sort
|
|
8
|
+
# of weird race-conditions
|
|
9
|
+
NON_RELOADABLE_ERRORS = %i[
|
|
10
|
+
fenced
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
7
13
|
# Contract to validate that input for transactional offset storage is correct
|
|
8
14
|
CONTRACT = Contracts::TransactionalOffset.new
|
|
9
15
|
|
|
10
|
-
private_constant :CONTRACT
|
|
16
|
+
private_constant :CONTRACT, :NON_RELOADABLE_ERRORS
|
|
11
17
|
|
|
12
18
|
# Creates a transaction.
|
|
13
19
|
#
|
|
@@ -29,7 +35,7 @@ module WaterDrop
|
|
|
29
35
|
# with a transaction. One transaction per single dispatch and for `produce_many` it will be
|
|
30
36
|
# a single transaction wrapping all messages dispatches (not one per message).
|
|
31
37
|
#
|
|
32
|
-
# @return Block result
|
|
38
|
+
# @return Block result or `nil` in case of early break/return
|
|
33
39
|
#
|
|
34
40
|
# @example Simple transaction
|
|
35
41
|
# producer.transaction do
|
|
@@ -39,7 +45,7 @@ module WaterDrop
|
|
|
39
45
|
# @example Aborted transaction - messages producer won't be visible by consumers
|
|
40
46
|
# producer.transaction do
|
|
41
47
|
# producer.produce_sync(topic: 'topic', payload: 'data')
|
|
42
|
-
#
|
|
48
|
+
# raise WaterDrop::AbortTransaction
|
|
43
49
|
# end
|
|
44
50
|
#
|
|
45
51
|
# @example Use block result last handler to wait on all messages ack
|
|
@@ -49,26 +55,46 @@ module WaterDrop
|
|
|
49
55
|
#
|
|
50
56
|
# handler.wait
|
|
51
57
|
def transaction
|
|
58
|
+
unless transactional?
|
|
59
|
+
raise(
|
|
60
|
+
Errors::ProducerNotTransactionalError,
|
|
61
|
+
"#{id} is not transactional"
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
52
65
|
# This will safely allow us to support one operation transactions so a transactional
|
|
53
66
|
# producer can work without the transactional block if needed
|
|
54
67
|
return yield if @transaction_mutex.owned?
|
|
55
68
|
|
|
56
69
|
@transaction_mutex.synchronize do
|
|
70
|
+
ensure_active!
|
|
71
|
+
|
|
57
72
|
transactional_instrument(:finished) do
|
|
58
73
|
with_transactional_error_handling(:begin) do
|
|
59
74
|
transactional_instrument(:started) { client.begin_transaction }
|
|
60
75
|
end
|
|
61
76
|
|
|
62
77
|
result = nil
|
|
63
|
-
|
|
78
|
+
finished = false
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
begin
|
|
66
81
|
result = yield
|
|
67
|
-
|
|
82
|
+
finished = true
|
|
83
|
+
rescue Exception => e
|
|
84
|
+
raise(e)
|
|
85
|
+
ensure
|
|
86
|
+
if !e && !finished
|
|
87
|
+
raise(
|
|
88
|
+
Errors::EarlyTransactionExitNotAllowedError,
|
|
89
|
+
<<~ERROR_MSG.tr("\n", ' ')
|
|
90
|
+
Using `return`, `break` or `throw` to exit a transaction block is not allowed.
|
|
91
|
+
If the `throw` came from `Timeout.timeout(duration)`, pass an exception class as
|
|
92
|
+
a second argument so it doesn't use `throw` to abort its block.
|
|
93
|
+
ERROR_MSG
|
|
94
|
+
)
|
|
95
|
+
end
|
|
68
96
|
end
|
|
69
97
|
|
|
70
|
-
commit || raise(WaterDrop::Errors::AbortTransaction)
|
|
71
|
-
|
|
72
98
|
with_transactional_error_handling(:commit) do
|
|
73
99
|
transactional_instrument(:committed) { client.commit_transaction }
|
|
74
100
|
end
|
|
@@ -76,19 +102,37 @@ module WaterDrop
|
|
|
76
102
|
result
|
|
77
103
|
# We need to handle any interrupt including critical in order not to have the transaction
|
|
78
104
|
# running. This will also handle things like `IRB::Abort`
|
|
79
|
-
#
|
|
80
|
-
# rubocop:disable Lint/RescueException
|
|
81
105
|
rescue Exception => e
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
# This code is a bit tricky. We have an error and when it happens we try to rollback
|
|
107
|
+
# the transaction. However we may end up in a state where transaction aborting itself
|
|
108
|
+
# produces error. In such case we also want to handle it as fatal and reload client.
|
|
109
|
+
# This is why we catch this here
|
|
110
|
+
begin
|
|
111
|
+
with_transactional_error_handling(:abort) do
|
|
112
|
+
transactional_instrument(:aborted) do
|
|
113
|
+
client.abort_transaction
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
# If something from rdkafka leaks here, it means there was a non-retryable error that
|
|
118
|
+
# bubbled up. In such cases if we should, we do reload the underling client
|
|
119
|
+
transactional_reload_client_if_needed(e)
|
|
120
|
+
|
|
121
|
+
raise
|
|
85
122
|
end
|
|
86
123
|
|
|
124
|
+
transactional_reload_client_if_needed(e)
|
|
125
|
+
|
|
87
126
|
raise unless e.is_a?(WaterDrop::Errors::AbortTransaction)
|
|
88
127
|
end
|
|
89
128
|
end
|
|
90
129
|
end
|
|
91
130
|
|
|
131
|
+
# @return [Boolean] true if we are in an active transaction
|
|
132
|
+
def transaction?
|
|
133
|
+
@transaction_mutex.owned?
|
|
134
|
+
end
|
|
135
|
+
|
|
92
136
|
# @return [Boolean] Is this producer a transactional one
|
|
93
137
|
def transactional?
|
|
94
138
|
return @transactional if instance_variable_defined?(:'@transactional')
|
|
@@ -132,8 +176,7 @@ module WaterDrop
|
|
|
132
176
|
client.send_offsets_to_transaction(
|
|
133
177
|
consumer,
|
|
134
178
|
tpl,
|
|
135
|
-
|
|
136
|
-
@config.max_wait_timeout * 1_000
|
|
179
|
+
current_variant.max_wait_timeout
|
|
137
180
|
)
|
|
138
181
|
end
|
|
139
182
|
end
|
|
@@ -193,11 +236,16 @@ module WaterDrop
|
|
|
193
236
|
attempt: attempt
|
|
194
237
|
)
|
|
195
238
|
|
|
196
|
-
|
|
239
|
+
if e.fatal?
|
|
240
|
+
# Reload the client on fatal errors if requested
|
|
241
|
+
transactional_reload_client_if_needed(e)
|
|
242
|
+
|
|
243
|
+
raise
|
|
244
|
+
end
|
|
197
245
|
|
|
198
246
|
if do_retry
|
|
199
247
|
# Backoff more and more before retries
|
|
200
|
-
sleep(config.wait_backoff_on_transaction_command * attempt)
|
|
248
|
+
sleep((config.wait_backoff_on_transaction_command / 1_000.0) * attempt)
|
|
201
249
|
|
|
202
250
|
retry
|
|
203
251
|
end
|
|
@@ -208,12 +256,43 @@ module WaterDrop
|
|
|
208
256
|
with_transactional_error_handling(:abort, allow_abortable: false) do
|
|
209
257
|
transactional_instrument(:aborted) { client.abort_transaction }
|
|
210
258
|
end
|
|
211
|
-
|
|
212
|
-
raise
|
|
213
259
|
end
|
|
214
260
|
|
|
215
261
|
raise
|
|
216
262
|
end
|
|
263
|
+
|
|
264
|
+
# Reloads the underlying client instance if needed and allowed
|
|
265
|
+
#
|
|
266
|
+
# This should be used only in transactions as only then we can get fatal transactional
|
|
267
|
+
# errors and we can safely reload the client.
|
|
268
|
+
#
|
|
269
|
+
# @param error [Exception] any error that was raised
|
|
270
|
+
#
|
|
271
|
+
# @note We only reload on rdkafka errors that are a cause on messages dispatches.
|
|
272
|
+
# Because we reload on any errors where cause is `Rdkafka::RdkafkaError` (minus exclusions)
|
|
273
|
+
# this in theory can cause reload if it was something else that raised those in transactions,
|
|
274
|
+
# for example Karafka. This is a trade-off. Since any error anyhow will cause a rollback,
|
|
275
|
+
# putting aside performance implication of closing and reconnecting, this should not be an
|
|
276
|
+
# issue.
|
|
277
|
+
def transactional_reload_client_if_needed(error)
|
|
278
|
+
rd_error = error.is_a?(Rdkafka::RdkafkaError) ? error : error.cause
|
|
279
|
+
|
|
280
|
+
return unless rd_error.is_a?(Rdkafka::RdkafkaError)
|
|
281
|
+
return unless config.reload_on_transaction_fatal_error
|
|
282
|
+
return if NON_RELOADABLE_ERRORS.include?(rd_error.code)
|
|
283
|
+
|
|
284
|
+
@operating_mutex.synchronize do
|
|
285
|
+
@monitor.instrument(
|
|
286
|
+
'producer.reloaded',
|
|
287
|
+
producer_id: id
|
|
288
|
+
) do
|
|
289
|
+
@client.flush(current_variant.max_wait_timeout)
|
|
290
|
+
purge
|
|
291
|
+
@client.close
|
|
292
|
+
@client = Builder.new.call(self, @config)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
217
296
|
end
|
|
218
297
|
end
|
|
219
298
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WaterDrop
|
|
4
|
+
class Producer
|
|
5
|
+
# Object that acts as a proxy allowing for alteration of certain low-level per-topic
|
|
6
|
+
# configuration and some other settings that users may find useful to alter, without having
|
|
7
|
+
# to create new producers with their underlying librdkafka instances.
|
|
8
|
+
#
|
|
9
|
+
# Since each librdkafka instance creates at least one TCP connection per broker, creating
|
|
10
|
+
# separate objects just to alter thing like `acks` may not be efficient and may lead to
|
|
11
|
+
# extensive usage of TCP connections, especially in bigger clusters.
|
|
12
|
+
#
|
|
13
|
+
# This variant object allows for "wrapping" of the producer with alteration of those settings
|
|
14
|
+
# in such a way, that two or more alterations can co-exist and share the same producer,
|
|
15
|
+
# effectively sharing the librdkafka client.
|
|
16
|
+
#
|
|
17
|
+
# Since this is an enhanced `SimpleDelegator` all `WaterDrop::Producer` APIs are preserved and
|
|
18
|
+
# a variant alteration can be used as a regular producer. The only important thing is to
|
|
19
|
+
# remember to only close it once.
|
|
20
|
+
#
|
|
21
|
+
# @note Not all settings are alterable. We only allow to alter things that are safe to be
|
|
22
|
+
# altered as they have no impact on the producer. If there is a setting you consider
|
|
23
|
+
# important and want to make it alterable, please open a GH issue for evaluation.
|
|
24
|
+
#
|
|
25
|
+
# @note Please be aware, that variant changes also affect buffers. If you overwrite the
|
|
26
|
+
# `max_wait_timeout`, since buffers are shared (as they exist on producer level), flushing
|
|
27
|
+
# may be impacted.
|
|
28
|
+
#
|
|
29
|
+
# @note `topic_config` is validated when created for the first time during message production.
|
|
30
|
+
# This means, that configuration error may be raised only during dispatch. There is no
|
|
31
|
+
# way out of this, since we need `librdkafka` instance to create the references.
|
|
32
|
+
class Variant < SimpleDelegator
|
|
33
|
+
# Empty hash we use as defaults for topic config.
|
|
34
|
+
# When rdkafka-ruby detects empty hash, it will use the librdkafka defaults
|
|
35
|
+
EMPTY_HASH = {}.freeze
|
|
36
|
+
|
|
37
|
+
private_constant :EMPTY_HASH
|
|
38
|
+
|
|
39
|
+
attr_reader :max_wait_timeout, :topic_config, :producer
|
|
40
|
+
|
|
41
|
+
# @param producer [WaterDrop::Producer] producer for which we want to have a variant
|
|
42
|
+
# @param max_wait_timeout [Integer, nil] alteration to max wait timeout or nil to use
|
|
43
|
+
# default
|
|
44
|
+
# @param topic_config [Hash] extra topic configuration that can be altered.
|
|
45
|
+
# @param default [Boolean] is this a default variant or an altered one
|
|
46
|
+
# @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties
|
|
47
|
+
def initialize(
|
|
48
|
+
producer,
|
|
49
|
+
max_wait_timeout: producer.config.max_wait_timeout,
|
|
50
|
+
topic_config: EMPTY_HASH,
|
|
51
|
+
default: false
|
|
52
|
+
)
|
|
53
|
+
@producer = producer
|
|
54
|
+
@max_wait_timeout = max_wait_timeout
|
|
55
|
+
@topic_config = topic_config
|
|
56
|
+
@default = default
|
|
57
|
+
super(producer)
|
|
58
|
+
|
|
59
|
+
Contracts::Variant.new.validate!(to_h, Errors::VariantInvalidError)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean] is this a default variant for this producer
|
|
63
|
+
def default?
|
|
64
|
+
@default
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# We need to wrap any methods from our API that could use a variant alteration with the
|
|
68
|
+
# per thread variant injection. Since method_missing can be slow and problematic, it is just
|
|
69
|
+
# easier to use our public API components methods to ensure the variant is being injected.
|
|
70
|
+
[
|
|
71
|
+
Async,
|
|
72
|
+
Buffer,
|
|
73
|
+
Sync,
|
|
74
|
+
Transactions
|
|
75
|
+
].each do |scope|
|
|
76
|
+
scope.instance_methods(false).each do |method_name|
|
|
77
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
78
|
+
def #{method_name}(*args, &block)
|
|
79
|
+
ref = Fiber.current.waterdrop_clients ||= {}
|
|
80
|
+
ref[@producer.id] = self
|
|
81
|
+
|
|
82
|
+
@producer.#{method_name}(*args, &block)
|
|
83
|
+
ensure
|
|
84
|
+
ref[@producer.id] = nil
|
|
85
|
+
end
|
|
86
|
+
RUBY
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @return [Hash] hash representation for contract validation to ensure basic sanity of the
|
|
93
|
+
# settings.
|
|
94
|
+
def to_h
|
|
95
|
+
{
|
|
96
|
+
default: default?,
|
|
97
|
+
max_wait_timeout: max_wait_timeout,
|
|
98
|
+
topic_config: topic_config,
|
|
99
|
+
# We pass this to validation, to make sure no-one alters the `acks` value when operating
|
|
100
|
+
# in the transactional mode as it causes librdkafka to crash ruby
|
|
101
|
+
# @see https://github.com/confluentinc/librdkafka/issues/4710
|
|
102
|
+
transactional: @producer.transactional?,
|
|
103
|
+
# We pass this for a similar reason as above
|
|
104
|
+
idempotent: @producer.idempotent?
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/waterdrop/producer.rb
CHANGED
|
@@ -9,6 +9,10 @@ module WaterDrop
|
|
|
9
9
|
include Buffer
|
|
10
10
|
include Transactions
|
|
11
11
|
include ::Karafka::Core::Helpers::Time
|
|
12
|
+
include ::Karafka::Core::Taggable
|
|
13
|
+
|
|
14
|
+
# Local storage for given thread waterdrop client references for variants
|
|
15
|
+
::Fiber.send(:attr_accessor, :waterdrop_clients)
|
|
12
16
|
|
|
13
17
|
# Which of the inline flow errors do we want to intercept and re-bind
|
|
14
18
|
SUPPORTED_FLOW_ERRORS = [
|
|
@@ -16,12 +20,15 @@ module WaterDrop
|
|
|
16
20
|
Rdkafka::Producer::DeliveryHandle::WaitTimeoutError
|
|
17
21
|
].freeze
|
|
18
22
|
|
|
19
|
-
# Empty
|
|
23
|
+
# Empty hash to save on memory allocations
|
|
20
24
|
EMPTY_HASH = {}.freeze
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
# Empty array to save on memory allocations
|
|
27
|
+
EMPTY_ARRAY = [].freeze
|
|
28
|
+
|
|
29
|
+
private_constant :SUPPORTED_FLOW_ERRORS, :EMPTY_HASH, :EMPTY_ARRAY
|
|
23
30
|
|
|
24
|
-
def_delegators :config
|
|
31
|
+
def_delegators :config
|
|
25
32
|
|
|
26
33
|
# @return [String] uuid of the current producer
|
|
27
34
|
attr_reader :id
|
|
@@ -65,6 +72,7 @@ module WaterDrop
|
|
|
65
72
|
@id = @config.id
|
|
66
73
|
@monitor = @config.monitor
|
|
67
74
|
@contract = Contracts::Message.new(max_payload_size: @config.max_payload_size)
|
|
75
|
+
@default_variant = Variant.new(self, default: true)
|
|
68
76
|
@status.configured!
|
|
69
77
|
end
|
|
70
78
|
|
|
@@ -78,6 +86,7 @@ module WaterDrop
|
|
|
78
86
|
|
|
79
87
|
# Don't allow to obtain a client reference for a producer that was not configured
|
|
80
88
|
raise Errors::ProducerNotConfiguredError, id if @status.initial?
|
|
89
|
+
raise Errors::ProducerClosedError, id if @status.closed?
|
|
81
90
|
|
|
82
91
|
@connecting_mutex.synchronize do
|
|
83
92
|
return @client if @client && @pid == Process.pid
|
|
@@ -104,18 +113,6 @@ module WaterDrop
|
|
|
104
113
|
@pid = Process.pid
|
|
105
114
|
@client = Builder.new.call(self, @config)
|
|
106
115
|
|
|
107
|
-
# Register statistics runner for this particular type of callbacks
|
|
108
|
-
::Karafka::Core::Instrumentation.statistics_callbacks.add(
|
|
109
|
-
@id,
|
|
110
|
-
Instrumentation::Callbacks::Statistics.new(@id, @client.name, @config.monitor)
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Register error tracking callback
|
|
114
|
-
::Karafka::Core::Instrumentation.error_callbacks.add(
|
|
115
|
-
@id,
|
|
116
|
-
Instrumentation::Callbacks::Error.new(@id, @client.name, @config.monitor)
|
|
117
|
-
)
|
|
118
|
-
|
|
119
116
|
@status.connected!
|
|
120
117
|
@monitor.instrument('producer.connected', producer_id: id)
|
|
121
118
|
end
|
|
@@ -145,73 +142,115 @@ module WaterDrop
|
|
|
145
142
|
@messages = []
|
|
146
143
|
end
|
|
147
144
|
|
|
148
|
-
|
|
145
|
+
# We should not purge if there is no client initialized
|
|
146
|
+
# It may not be initialized if we created a new producer that never connected to kafka,
|
|
147
|
+
# we used buffer and purged. In cases like this client won't exist
|
|
148
|
+
@connecting_mutex.synchronize do
|
|
149
|
+
@client&.purge
|
|
150
|
+
end
|
|
149
151
|
end
|
|
150
152
|
end
|
|
151
153
|
|
|
154
|
+
# Builds the variant alteration and returns it.
|
|
155
|
+
#
|
|
156
|
+
# @param args [Object] anything `Producer::Variant` initializer accepts
|
|
157
|
+
# @return [WaterDrop::Producer::Variant] variant proxy to use with alterations
|
|
158
|
+
def with(**args)
|
|
159
|
+
ensure_active!
|
|
160
|
+
|
|
161
|
+
Variant.new(self, **args)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
alias variant with
|
|
165
|
+
|
|
166
|
+
# @return [Boolean] true if current producer is idempotent
|
|
167
|
+
def idempotent?
|
|
168
|
+
# Every transactional producer is idempotent by default always
|
|
169
|
+
return true if transactional?
|
|
170
|
+
return @idempotent if instance_variable_defined?(:'@idempotent')
|
|
171
|
+
|
|
172
|
+
@idempotent = config.kafka.to_h.key?(:'enable.idempotence')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns and caches the middleware object that may be used
|
|
176
|
+
# @return [WaterDrop::Producer::Middleware]
|
|
177
|
+
def middleware
|
|
178
|
+
@middleware ||= config.middleware
|
|
179
|
+
end
|
|
180
|
+
|
|
152
181
|
# Flushes the buffers in a sync way and closes the producer
|
|
153
182
|
# @param force [Boolean] should we force closing even with outstanding messages after the
|
|
154
183
|
# max wait timeout
|
|
155
184
|
def close(force: false)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# We
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
#
|
|
202
|
-
|
|
185
|
+
# If we already own the transactional mutex, it means we are inside of a transaction and
|
|
186
|
+
# it should not we allowed to close the producer in such a case.
|
|
187
|
+
if @transaction_mutex.locked? && @transaction_mutex.owned?
|
|
188
|
+
raise Errors::ProducerTransactionalCloseAttemptError, id
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The transactional mutex here can be used even when no transactions are in use
|
|
192
|
+
# It prevents us from closing a mutex during transactions and is irrelevant in other cases
|
|
193
|
+
@transaction_mutex.synchronize do
|
|
194
|
+
@operating_mutex.synchronize do
|
|
195
|
+
return unless @status.active?
|
|
196
|
+
|
|
197
|
+
@monitor.instrument(
|
|
198
|
+
'producer.closed',
|
|
199
|
+
producer_id: id
|
|
200
|
+
) do
|
|
201
|
+
@status.closing!
|
|
202
|
+
@monitor.instrument('producer.closing', producer_id: id)
|
|
203
|
+
|
|
204
|
+
# No need for auto-gc if everything got closed by us
|
|
205
|
+
# This should be used only in case a producer was not closed properly and forgotten
|
|
206
|
+
ObjectSpace.undefine_finalizer(id)
|
|
207
|
+
|
|
208
|
+
# We save this thread id because we need to bypass the activity verification on the
|
|
209
|
+
# producer for final flush of buffers.
|
|
210
|
+
@closing_thread_id = Thread.current.object_id
|
|
211
|
+
|
|
212
|
+
# Wait until all the outgoing operations are done. Only when no one is using the
|
|
213
|
+
# underlying client running operations we can close
|
|
214
|
+
sleep(0.001) until @operations_in_progress.value.zero?
|
|
215
|
+
|
|
216
|
+
# Flush has its own buffer mutex but even if it is blocked, flushing can still happen
|
|
217
|
+
# as we close the client after the flushing (even if blocked by the mutex)
|
|
218
|
+
flush(true)
|
|
219
|
+
|
|
220
|
+
# We should not close the client in several threads the same time
|
|
221
|
+
# It is safe to run it several times but not exactly the same moment
|
|
222
|
+
# We also mark it as closed only if it was connected, if not, it would trigger a new
|
|
223
|
+
# connection that anyhow would be immediately closed
|
|
224
|
+
if @client
|
|
225
|
+
# Why do we trigger it early instead of just having `#close` do it?
|
|
226
|
+
# The linger.ms time will be ignored for the duration of the call,
|
|
227
|
+
# queued messages will be sent to the broker as soon as possible.
|
|
228
|
+
begin
|
|
229
|
+
@client.flush(current_variant.max_wait_timeout) unless @client.closed?
|
|
230
|
+
# We can safely ignore timeouts here because any left outstanding requests
|
|
231
|
+
# will anyhow force wait on close if not forced.
|
|
232
|
+
# If forced, we will purge the queue and just close
|
|
233
|
+
rescue ::Rdkafka::RdkafkaError, Rdkafka::AbstractHandle::WaitTimeoutError
|
|
234
|
+
nil
|
|
235
|
+
ensure
|
|
236
|
+
# Purge fully the local queue in case of a forceful shutdown just to be sure, that
|
|
237
|
+
# there are no dangling messages. In case flush was successful, there should be
|
|
238
|
+
# none but we do it just in case it timed out
|
|
239
|
+
purge if force
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
@client.close
|
|
243
|
+
|
|
244
|
+
@client = nil
|
|
203
245
|
end
|
|
204
246
|
|
|
205
|
-
|
|
247
|
+
# Remove callbacks runners that were registered
|
|
248
|
+
::Karafka::Core::Instrumentation.statistics_callbacks.delete(@id)
|
|
249
|
+
::Karafka::Core::Instrumentation.error_callbacks.delete(@id)
|
|
250
|
+
::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.delete(@id)
|
|
206
251
|
|
|
207
|
-
@
|
|
252
|
+
@status.closed!
|
|
208
253
|
end
|
|
209
|
-
|
|
210
|
-
# Remove callbacks runners that were registered
|
|
211
|
-
::Karafka::Core::Instrumentation.statistics_callbacks.delete(@id)
|
|
212
|
-
::Karafka::Core::Instrumentation.error_callbacks.delete(@id)
|
|
213
|
-
|
|
214
|
-
@status.closed!
|
|
215
254
|
end
|
|
216
255
|
end
|
|
217
256
|
end
|
|
@@ -248,13 +287,23 @@ module WaterDrop
|
|
|
248
287
|
# Waits on a given handler
|
|
249
288
|
#
|
|
250
289
|
# @param handler [Rdkafka::Producer::DeliveryHandle]
|
|
251
|
-
|
|
290
|
+
# @param raise_response_error [Boolean] should we raise the response error after we receive the
|
|
291
|
+
# final result and it is an error.
|
|
292
|
+
def wait(handler, raise_response_error: true)
|
|
252
293
|
handler.wait(
|
|
253
|
-
max_wait_timeout
|
|
254
|
-
|
|
294
|
+
# rdkafka max_wait_timeout is in seconds and we use ms
|
|
295
|
+
max_wait_timeout: current_variant.max_wait_timeout / 1_000.0,
|
|
296
|
+
raise_response_error: raise_response_error
|
|
255
297
|
)
|
|
256
298
|
end
|
|
257
299
|
|
|
300
|
+
# @return [Producer::Variant] the variant config. Either custom if built using `#with` or
|
|
301
|
+
# a default one.
|
|
302
|
+
def current_variant
|
|
303
|
+
Fiber.current.waterdrop_clients ||= {}
|
|
304
|
+
Fiber.current.waterdrop_clients[id] || @default_variant
|
|
305
|
+
end
|
|
306
|
+
|
|
258
307
|
# Runs the client produce method with a given message
|
|
259
308
|
#
|
|
260
309
|
# @param message [Hash] message we want to send
|
|
@@ -270,9 +319,17 @@ module WaterDrop
|
|
|
270
319
|
ensure_active!
|
|
271
320
|
end
|
|
272
321
|
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
|
|
322
|
+
# We basically only duplicate the message hash only if it is needed.
|
|
323
|
+
# It is needed when user is using a custom settings variant or when symbol is provided as
|
|
324
|
+
# the topic name. We should never mutate user input message as it may be a hash that the
|
|
325
|
+
# user is using for some other operations
|
|
326
|
+
if message[:topic].is_a?(Symbol) || !current_variant.default?
|
|
327
|
+
message = message.dup
|
|
328
|
+
# In case someone defines topic as a symbol, we need to convert it into a string as
|
|
329
|
+
# librdkafka does not accept symbols
|
|
330
|
+
message[:topic] = message[:topic].to_s
|
|
331
|
+
message[:topic_config] = current_variant.topic_config
|
|
332
|
+
end
|
|
276
333
|
|
|
277
334
|
if transactional?
|
|
278
335
|
transaction { client.produce(**message) }
|
|
@@ -286,9 +343,9 @@ module WaterDrop
|
|
|
286
343
|
# If we're running for longer than the timeout, we need to re-raise the queue full.
|
|
287
344
|
# This will prevent from situation where cluster is down forever and we just retry and retry
|
|
288
345
|
# in an infinite loop, effectively hanging the processing
|
|
289
|
-
raise unless monotonic_now - produce_time < @config.wait_timeout_on_queue_full
|
|
346
|
+
raise unless monotonic_now - produce_time < @config.wait_timeout_on_queue_full
|
|
290
347
|
|
|
291
|
-
label = caller_locations(2, 1)[0].label.split(' ').last
|
|
348
|
+
label = caller_locations(2, 1)[0].label.split(' ').last.split('#').last
|
|
292
349
|
|
|
293
350
|
# We use this syntax here because we want to preserve the original `#cause` when we
|
|
294
351
|
# instrument the error and there is no way to manually assign `#cause` value. We want to keep
|
|
@@ -297,22 +354,28 @@ module WaterDrop
|
|
|
297
354
|
begin
|
|
298
355
|
raise Errors::ProduceError, e.inspect
|
|
299
356
|
rescue Errors::ProduceError => e
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
357
|
+
# Users can configure this because in pipe-like flows with high throughput, queue full with
|
|
358
|
+
# retry may be used as a throttling system that will backoff and wait.
|
|
359
|
+
# In such scenarios this error notification can be removed and until queue full is
|
|
360
|
+
# retryable, it will not be raised as an error.
|
|
361
|
+
if @config.instrument_on_wait_queue_full
|
|
362
|
+
# We want to instrument on this event even when we restart it.
|
|
363
|
+
# The reason is simple: instrumentation and visibility.
|
|
364
|
+
# We can recover from this, but despite that we should be able to instrument this.
|
|
365
|
+
# If this type of event happens too often, it may indicate that the buffer settings are
|
|
366
|
+
# not well configured.
|
|
367
|
+
@monitor.instrument(
|
|
368
|
+
'error.occurred',
|
|
369
|
+
producer_id: id,
|
|
370
|
+
message: message,
|
|
371
|
+
error: e,
|
|
372
|
+
type: "message.#{label}"
|
|
373
|
+
)
|
|
374
|
+
end
|
|
312
375
|
|
|
313
376
|
# We do not poll the producer because polling happens in a background thread
|
|
314
377
|
# It also should not be a frequent case (queue full), hence it's ok to just throttle.
|
|
315
|
-
sleep @config.wait_backoff_on_queue_full
|
|
378
|
+
sleep @config.wait_backoff_on_queue_full / 1_000.0
|
|
316
379
|
end
|
|
317
380
|
|
|
318
381
|
@operations_in_progress.decrement
|
data/lib/waterdrop/version.rb
CHANGED