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
data/docker-compose.yml
CHANGED
|
@@ -11,20 +11,50 @@ module WaterDrop
|
|
|
11
11
|
# @param producer [WaterDrop::Producer] producer instance with its config, etc
|
|
12
12
|
# @note We overwrite this that way, because we do not care
|
|
13
13
|
def new(producer)
|
|
14
|
-
|
|
14
|
+
kafka_config = producer.config.kafka.to_h
|
|
15
|
+
monitor = producer.config.monitor
|
|
15
16
|
|
|
16
|
-
client = ::Rdkafka::Config.new(
|
|
17
|
+
client = ::Rdkafka::Config.new(kafka_config).producer(native_kafka_auto_start: false)
|
|
18
|
+
|
|
19
|
+
# Register statistics runner for this particular type of callbacks
|
|
20
|
+
::Karafka::Core::Instrumentation.statistics_callbacks.add(
|
|
21
|
+
producer.id,
|
|
22
|
+
Instrumentation::Callbacks::Statistics.new(producer.id, client.name, monitor)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Register error tracking callback
|
|
26
|
+
::Karafka::Core::Instrumentation.error_callbacks.add(
|
|
27
|
+
producer.id,
|
|
28
|
+
Instrumentation::Callbacks::Error.new(producer.id, client.name, monitor)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Register oauth bearer refresh for this particular type of callbacks
|
|
32
|
+
::Karafka::Core::Instrumentation.oauthbearer_token_refresh_callbacks.add(
|
|
33
|
+
producer.id,
|
|
34
|
+
Instrumentation::Callbacks::OauthbearerTokenRefresh.new(client, monitor)
|
|
35
|
+
)
|
|
17
36
|
|
|
18
37
|
# This callback is not global and is per client, thus we do not have to wrap it with a
|
|
19
38
|
# callbacks manager to make it work
|
|
20
39
|
client.delivery_callback = Instrumentation::Callbacks::Delivery.new(
|
|
21
40
|
producer.id,
|
|
22
41
|
producer.transactional?,
|
|
23
|
-
|
|
42
|
+
monitor
|
|
24
43
|
)
|
|
25
44
|
|
|
45
|
+
oauth_listener = producer.config.oauth.token_provider_listener
|
|
46
|
+
# We need to subscribe the oauth listener here because we want it to be ready before
|
|
47
|
+
# any producer callbacks run. In theory because WaterDrop rdkafka producer is lazy loaded
|
|
48
|
+
# we would have enough time to make user subscribe it himself, but then it would not
|
|
49
|
+
# coop with auto-configuration coming from Karafka. The way it is done below, if it is
|
|
50
|
+
# configured it will be subscribed and if not, user always can subscribe it himself as
|
|
51
|
+
# long as it is done prior to first usage
|
|
52
|
+
monitor.subscribe(oauth_listener) if oauth_listener
|
|
53
|
+
|
|
54
|
+
client.start
|
|
55
|
+
|
|
26
56
|
# Switch to the transactional mode if user provided the transactional id
|
|
27
|
-
client.init_transactions if
|
|
57
|
+
client.init_transactions if kafka_config.key?(:'transactional.id')
|
|
28
58
|
|
|
29
59
|
client
|
|
30
60
|
end
|
data/lib/waterdrop/config.rb
CHANGED
|
@@ -12,7 +12,12 @@ module WaterDrop
|
|
|
12
12
|
'client.id': 'waterdrop',
|
|
13
13
|
# emit librdkafka statistics every five seconds. This is used in instrumentation.
|
|
14
14
|
# When disabled, part of metrics will not be published and available.
|
|
15
|
-
'statistics.interval.ms': 5_000
|
|
15
|
+
'statistics.interval.ms': 5_000,
|
|
16
|
+
# We set it to a value that is lower than `max_wait_timeout` to have a final verdict upon
|
|
17
|
+
# sync delivery
|
|
18
|
+
'message.timeout.ms': 50_000,
|
|
19
|
+
# Must be more or equal to `message.timeout.ms` defaults
|
|
20
|
+
'transaction.timeout.ms': 55_000
|
|
16
21
|
}.freeze
|
|
17
22
|
|
|
18
23
|
private_constant :KAFKA_DEFAULTS
|
|
@@ -25,7 +30,7 @@ module WaterDrop
|
|
|
25
30
|
setting(
|
|
26
31
|
:id,
|
|
27
32
|
default: false,
|
|
28
|
-
constructor: ->(id) { id || SecureRandom.hex(6) }
|
|
33
|
+
constructor: ->(id) { id || "waterdrop-#{SecureRandom.hex(6)}" }
|
|
29
34
|
)
|
|
30
35
|
# option [Instance] logger that we want to use
|
|
31
36
|
# @note Due to how rdkafka works, this setting is global for all the producers
|
|
@@ -44,12 +49,8 @@ module WaterDrop
|
|
|
44
49
|
# option [Integer] max payload size allowed for delivery to Kafka
|
|
45
50
|
setting :max_payload_size, default: 1_000_012
|
|
46
51
|
# option [Integer] Wait that long for the delivery report or raise an error if this takes
|
|
47
|
-
# longer than the timeout.
|
|
48
|
-
setting :max_wait_timeout, default:
|
|
49
|
-
# option [Numeric] how long should we wait between re-checks on the availability of the
|
|
50
|
-
# delivery report. In a really robust systems, this describes the min-delivery time
|
|
51
|
-
# for a single sync message when produced in isolation
|
|
52
|
-
setting :wait_timeout, default: 0.005 # 5 milliseconds
|
|
52
|
+
# longer than the timeout ms.
|
|
53
|
+
setting :max_wait_timeout, default: 60_000
|
|
53
54
|
# option [Boolean] should we upon detecting full librdkafka queue backoff and retry or should
|
|
54
55
|
# we raise an exception.
|
|
55
56
|
# When this is set to `true`, upon full queue, we won't raise an error. There will be error
|
|
@@ -60,15 +61,21 @@ module WaterDrop
|
|
|
60
61
|
# option [Integer] how long (in seconds) should we backoff before a retry when queue is full
|
|
61
62
|
# The retry will happen with the same message and backoff should give us some time to
|
|
62
63
|
# dispatch previously buffered messages.
|
|
63
|
-
setting :wait_backoff_on_queue_full, default:
|
|
64
|
-
# option [Numeric] how many
|
|
64
|
+
setting :wait_backoff_on_queue_full, default: 100
|
|
65
|
+
# option [Numeric] how many ms should we wait with the backoff on queue having space for
|
|
65
66
|
# more messages before re-raising the error.
|
|
66
|
-
setting :wait_timeout_on_queue_full, default:
|
|
67
|
+
setting :wait_timeout_on_queue_full, default: 10_000
|
|
68
|
+
# option [Boolean] should we instrument non-critical, retryable queue full errors
|
|
69
|
+
setting :instrument_on_wait_queue_full, default: true
|
|
67
70
|
# option [Numeric] How long to wait before retrying a retryable transaction related error
|
|
68
|
-
setting :wait_backoff_on_transaction_command, default:
|
|
71
|
+
setting :wait_backoff_on_transaction_command, default: 500
|
|
69
72
|
# option [Numeric] How many times to retry a retryable transaction related error before
|
|
70
73
|
# giving up
|
|
71
74
|
setting :max_attempts_on_transaction_command, default: 5
|
|
75
|
+
# When a fatal transactional error occurs, should we close and recreate the underlying producer
|
|
76
|
+
# to keep going or should we stop. Since we will open a new instance and the failed transaction
|
|
77
|
+
# anyhow rolls back, we should be able to safely reload.
|
|
78
|
+
setting :reload_on_transaction_fatal_error, default: true
|
|
72
79
|
|
|
73
80
|
# option [Boolean] should we send messages. Setting this to false can be really useful when
|
|
74
81
|
# testing and or developing because when set to false, won't actually ping Kafka but will
|
|
@@ -86,6 +93,14 @@ module WaterDrop
|
|
|
86
93
|
constructor: ->(middleware) { middleware || WaterDrop::Middleware.new }
|
|
87
94
|
)
|
|
88
95
|
|
|
96
|
+
# Namespace for oauth related configuration
|
|
97
|
+
setting :oauth do
|
|
98
|
+
# option [false, #call] Listener for using oauth bearer. This listener will be able to
|
|
99
|
+
# get the client name to decide whether to use a single multi-client token refreshing
|
|
100
|
+
# or have separate tokens per instance.
|
|
101
|
+
setting :token_provider_listener, default: false
|
|
102
|
+
end
|
|
103
|
+
|
|
89
104
|
# Configuration method
|
|
90
105
|
# @yield Runs a block of code providing a config singleton instance to it
|
|
91
106
|
# @yieldparam [WaterDrop::Config] WaterDrop config instance
|
|
@@ -14,14 +14,25 @@ module WaterDrop
|
|
|
14
14
|
|
|
15
15
|
required(:id) { |val| val.is_a?(String) && !val.empty? }
|
|
16
16
|
required(:logger) { |val| !val.nil? }
|
|
17
|
+
required(:monitor) { |val| !val.nil? }
|
|
17
18
|
required(:deliver) { |val| [true, false].include?(val) }
|
|
18
19
|
required(:max_payload_size) { |val| val.is_a?(Integer) && val >= 1 }
|
|
19
20
|
required(:max_wait_timeout) { |val| val.is_a?(Numeric) && val >= 0 }
|
|
20
|
-
required(:
|
|
21
|
+
required(:client_class) { |val| !val.nil? }
|
|
21
22
|
required(:kafka) { |val| val.is_a?(Hash) && !val.empty? }
|
|
22
23
|
required(:wait_on_queue_full) { |val| [true, false].include?(val) }
|
|
24
|
+
required(:instrument_on_wait_queue_full) { |val| [true, false].include?(val) }
|
|
23
25
|
required(:wait_backoff_on_queue_full) { |val| val.is_a?(Numeric) && val >= 0 }
|
|
24
26
|
required(:wait_timeout_on_queue_full) { |val| val.is_a?(Numeric) && val >= 0 }
|
|
27
|
+
required(:wait_backoff_on_transaction_command) { |val| val.is_a?(Numeric) && val >= 0 }
|
|
28
|
+
required(:max_attempts_on_transaction_command) { |val| val.is_a?(Integer) && val >= 1 }
|
|
29
|
+
required(:reload_on_transaction_fatal_error) { |val| [true, false].include?(val) }
|
|
30
|
+
|
|
31
|
+
nested(:oauth) do
|
|
32
|
+
required(:token_provider_listener) do |val|
|
|
33
|
+
val == false || val.respond_to?(:on_oauthbearer_token_refresh)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
25
36
|
|
|
26
37
|
# rdkafka allows both symbols and strings as keys for config but then casts them to strings
|
|
27
38
|
# This can be confusing, so we expect all keys to be symbolized
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WaterDrop
|
|
4
|
+
module Contracts
|
|
5
|
+
# Variant validator to ensure basic sanity of the variant alteration data
|
|
6
|
+
class Variant < ::Karafka::Core::Contractable::Contract
|
|
7
|
+
# Taken from librdkafka config
|
|
8
|
+
# Those values can be changed on a per topic basis. We do not support experimental or
|
|
9
|
+
# deprecated values. We also do not support settings that would break rdkafka-ruby
|
|
10
|
+
#
|
|
11
|
+
# @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties
|
|
12
|
+
TOPIC_CONFIG_KEYS = %i[
|
|
13
|
+
acks
|
|
14
|
+
compression.codec
|
|
15
|
+
compression.level
|
|
16
|
+
compression.type
|
|
17
|
+
delivery.timeout.ms
|
|
18
|
+
message.timeout.ms
|
|
19
|
+
partitioner
|
|
20
|
+
request.required.acks
|
|
21
|
+
request.timeout.ms
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Boolean values
|
|
25
|
+
BOOLEANS = [true, false].freeze
|
|
26
|
+
|
|
27
|
+
private_constant :TOPIC_CONFIG_KEYS, :BOOLEANS
|
|
28
|
+
|
|
29
|
+
configure do |config|
|
|
30
|
+
config.error_messages = YAML.safe_load(
|
|
31
|
+
File.read(
|
|
32
|
+
File.join(WaterDrop.gem_root, 'config', 'locales', 'errors.yml')
|
|
33
|
+
)
|
|
34
|
+
).fetch('en').fetch('validations').fetch('variant')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
required(:default) { |val| BOOLEANS.include?(val) }
|
|
38
|
+
required(:max_wait_timeout) { |val| val.is_a?(Numeric) && val >= 0 }
|
|
39
|
+
|
|
40
|
+
# Checks if all keys are symbols
|
|
41
|
+
virtual do |config, errors|
|
|
42
|
+
next true unless errors.empty?
|
|
43
|
+
|
|
44
|
+
errors = []
|
|
45
|
+
|
|
46
|
+
config
|
|
47
|
+
.fetch(:topic_config)
|
|
48
|
+
.keys
|
|
49
|
+
.reject { |key| key.is_a?(Symbol) }
|
|
50
|
+
.each { |key| errors << [[:kafka, key], :kafka_key_must_be_a_symbol] }
|
|
51
|
+
|
|
52
|
+
errors
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Checks if we have any keys that are not allowed
|
|
56
|
+
virtual do |config, errors|
|
|
57
|
+
next true unless errors.empty?
|
|
58
|
+
|
|
59
|
+
errors = []
|
|
60
|
+
|
|
61
|
+
config
|
|
62
|
+
.fetch(:topic_config)
|
|
63
|
+
.keys
|
|
64
|
+
.reject { |key| TOPIC_CONFIG_KEYS.include?(key) }
|
|
65
|
+
.each { |key| errors << [[:kafka, key], :kafka_key_not_per_topic] }
|
|
66
|
+
|
|
67
|
+
errors
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Ensure, that acks is not changed when in transactional mode
|
|
71
|
+
# acks needs to be set to 'all' and should not be changed when working with transactional
|
|
72
|
+
# producer as it causes librdkafka to crash
|
|
73
|
+
virtual do |config, errors|
|
|
74
|
+
next true unless errors.empty?
|
|
75
|
+
# Relevant only for the transactional producer
|
|
76
|
+
next true unless config.fetch(:transactional)
|
|
77
|
+
|
|
78
|
+
errors = []
|
|
79
|
+
|
|
80
|
+
config
|
|
81
|
+
.fetch(:topic_config)
|
|
82
|
+
.keys
|
|
83
|
+
.select { |key| key.to_s.include?('acks') }
|
|
84
|
+
.each { |key| errors << [[:kafka, key], :kafka_key_acks_not_changeable] }
|
|
85
|
+
|
|
86
|
+
errors
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Prevent from creating variants altering acks when idempotent
|
|
90
|
+
virtual do |config, errors|
|
|
91
|
+
next true unless errors.empty?
|
|
92
|
+
# Relevant only for the transactional producer
|
|
93
|
+
next true unless config.fetch(:idempotent)
|
|
94
|
+
|
|
95
|
+
errors = []
|
|
96
|
+
|
|
97
|
+
config
|
|
98
|
+
.fetch(:topic_config)
|
|
99
|
+
.keys
|
|
100
|
+
.select { |key| key.to_s.include?('acks') }
|
|
101
|
+
.each { |key| errors << [[:kafka, key], :kafka_key_acks_not_changeable] }
|
|
102
|
+
|
|
103
|
+
errors
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/waterdrop/errors.rb
CHANGED
|
@@ -9,6 +9,9 @@ module WaterDrop
|
|
|
9
9
|
# Raised when configuration doesn't match with validation contract
|
|
10
10
|
ConfigurationInvalidError = Class.new(BaseError)
|
|
11
11
|
|
|
12
|
+
# Raised when variant alteration is not valid
|
|
13
|
+
VariantInvalidError = Class.new(BaseError)
|
|
14
|
+
|
|
12
15
|
# Raised when we want to use a producer that was not configured
|
|
13
16
|
ProducerNotConfiguredError = Class.new(BaseError)
|
|
14
17
|
|
|
@@ -22,12 +25,18 @@ module WaterDrop
|
|
|
22
25
|
# Raised when there was an attempt to use a closed producer
|
|
23
26
|
ProducerClosedError = Class.new(BaseError)
|
|
24
27
|
|
|
28
|
+
# Raised if you attempt to close the producer from within a transaction. This is not allowed.
|
|
29
|
+
ProducerTransactionalCloseAttemptError = Class.new(BaseError)
|
|
30
|
+
|
|
25
31
|
# Raised when we want to send a message that is invalid (impossible topic, etc)
|
|
26
32
|
MessageInvalidError = Class.new(BaseError)
|
|
27
33
|
|
|
28
34
|
# Raised when we want to commit transactional offset and the input is invalid
|
|
29
35
|
TransactionalOffsetInvalidError = Class.new(BaseError)
|
|
30
36
|
|
|
37
|
+
# Raised when transaction attempt happens on a non-transactional producer
|
|
38
|
+
ProducerNotTransactionalError = Class.new(BaseError)
|
|
39
|
+
|
|
31
40
|
# Raised when we've got an unexpected status. This should never happen. If it does, please
|
|
32
41
|
# contact us as it is an error.
|
|
33
42
|
StatusInvalidError = Class.new(BaseError)
|
|
@@ -44,6 +53,9 @@ module WaterDrop
|
|
|
44
53
|
# graceful transaction aborting
|
|
45
54
|
AbortTransaction = Class.new(BaseError)
|
|
46
55
|
|
|
56
|
+
# Do not use `break`, `return` or `throw` inside of the transaction blocks
|
|
57
|
+
EarlyTransactionExitNotAllowedError = Class.new(BaseError)
|
|
58
|
+
|
|
47
59
|
# Raised when during messages producing something bad happened inline
|
|
48
60
|
class ProduceManyError < ProduceError
|
|
49
61
|
attr_reader :dispatched
|
|
@@ -57,4 +69,8 @@ module WaterDrop
|
|
|
57
69
|
end
|
|
58
70
|
end
|
|
59
71
|
end
|
|
72
|
+
|
|
73
|
+
# Alias so we can have a nicer API to abort transactions
|
|
74
|
+
# This makes referencing easier
|
|
75
|
+
AbortTransaction = Errors::AbortTransaction
|
|
60
76
|
end
|
|
@@ -44,6 +44,17 @@ module WaterDrop
|
|
|
44
44
|
else
|
|
45
45
|
instrument_error(delivery_report)
|
|
46
46
|
end
|
|
47
|
+
# This runs from the rdkafka thread, thus we want to safe-guard it and prevent absolute
|
|
48
|
+
# crashes even if the instrumentation code fails. If it would bubble-up, it could crash
|
|
49
|
+
# the rdkafka background thread
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
@monitor.instrument(
|
|
52
|
+
'error.occurred',
|
|
53
|
+
caller: self,
|
|
54
|
+
error: e,
|
|
55
|
+
producer_id: @producer_id,
|
|
56
|
+
type: 'callbacks.delivery.error'
|
|
57
|
+
)
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
private
|
|
@@ -32,6 +32,17 @@ module WaterDrop
|
|
|
32
32
|
producer_id: @producer_id,
|
|
33
33
|
type: 'librdkafka.error'
|
|
34
34
|
)
|
|
35
|
+
# This runs from the rdkafka thread, thus we want to safe-guard it and prevent absolute
|
|
36
|
+
# crashes even if the instrumentation code fails. If it would bubble-up, it could crash
|
|
37
|
+
# the rdkafka background thread
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
@monitor.instrument(
|
|
40
|
+
'error.occurred',
|
|
41
|
+
caller: self,
|
|
42
|
+
error: e,
|
|
43
|
+
producer_id: @producer_id,
|
|
44
|
+
type: 'callbacks.error.error'
|
|
45
|
+
)
|
|
35
46
|
end
|
|
36
47
|
end
|
|
37
48
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WaterDrop
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module Callbacks
|
|
6
|
+
# Callback that is triggered when oauth token needs to be refreshed.
|
|
7
|
+
class OauthbearerTokenRefresh
|
|
8
|
+
# @param bearer [Rdkafka::Producer] given rdkafka instance. It is needed as
|
|
9
|
+
# we need to have a reference to call `#oauthbearer_set_token` or
|
|
10
|
+
# `#oauthbearer_set_token_failure` upon the event.
|
|
11
|
+
# @param monitor [WaterDrop::Instrumentation::Monitor] monitor we are using
|
|
12
|
+
def initialize(bearer, monitor)
|
|
13
|
+
@bearer = bearer
|
|
14
|
+
@monitor = monitor
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Upon receiving of this event, user is required to invoke either `#oauthbearer_set_token`
|
|
18
|
+
# or `#oauthbearer_set_token_failure` on the `event[:bearer]` depending whether token
|
|
19
|
+
# obtaining was successful or not.
|
|
20
|
+
#
|
|
21
|
+
# Please refer to WaterDrop and Karafka documentation or `Rdkafka::Helpers::OAuth`
|
|
22
|
+
# documentation directly for exact parameters of those methods.
|
|
23
|
+
#
|
|
24
|
+
# @param _rd_config [Rdkafka::Config]
|
|
25
|
+
# @param bearer_name [String] name of the bearer for which we refresh
|
|
26
|
+
def call(_rd_config, bearer_name)
|
|
27
|
+
return unless @bearer.name == bearer_name
|
|
28
|
+
|
|
29
|
+
@monitor.instrument(
|
|
30
|
+
'oauthbearer.token_refresh',
|
|
31
|
+
bearer: @bearer,
|
|
32
|
+
caller: self
|
|
33
|
+
)
|
|
34
|
+
# This runs from the rdkafka thread, thus we want to safe-guard it and prevent absolute
|
|
35
|
+
# crashes even if the instrumentation code fails. If it would bubble-up, it could crash
|
|
36
|
+
# the rdkafka background thread
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
@monitor.instrument(
|
|
39
|
+
'error.occurred',
|
|
40
|
+
caller: self,
|
|
41
|
+
error: e,
|
|
42
|
+
producer_id: @producer_id,
|
|
43
|
+
type: 'callbacks.oauthbearer_token_refresh.error'
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -34,6 +34,17 @@ module WaterDrop
|
|
|
34
34
|
producer_id: @producer_id,
|
|
35
35
|
statistics: @statistics_decorator.call(statistics)
|
|
36
36
|
)
|
|
37
|
+
# This runs from the rdkafka thread, thus we want to safe-guard it and prevent absolute
|
|
38
|
+
# crashes even if the instrumentation code fails. If it would bubble-up, it could crash
|
|
39
|
+
# the rdkafka background thread
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
@monitor.instrument(
|
|
42
|
+
'error.occurred',
|
|
43
|
+
caller: self,
|
|
44
|
+
error: e,
|
|
45
|
+
producer_id: @producer_id,
|
|
46
|
+
type: 'callbacks.statistics.error'
|
|
47
|
+
)
|
|
37
48
|
end
|
|
38
49
|
end
|
|
39
50
|
end
|
|
@@ -26,7 +26,7 @@ module WaterDrop
|
|
|
26
26
|
def on_message_produced_async(event)
|
|
27
27
|
message = event[:message]
|
|
28
28
|
|
|
29
|
-
info(event, "
|
|
29
|
+
info(event, "Message to '#{message[:topic]}' topic was delegated to a dispatch queue")
|
|
30
30
|
|
|
31
31
|
return unless log_messages?
|
|
32
32
|
|
|
@@ -49,7 +49,10 @@ module WaterDrop
|
|
|
49
49
|
messages = event[:messages]
|
|
50
50
|
topics_count = messages.map { |message| "'#{message[:topic]}'" }.uniq.count
|
|
51
51
|
|
|
52
|
-
info(
|
|
52
|
+
info(
|
|
53
|
+
event,
|
|
54
|
+
"#{messages.size} messages to #{topics_count} topics were delegated to a dispatch queue"
|
|
55
|
+
)
|
|
53
56
|
|
|
54
57
|
return unless log_messages?
|
|
55
58
|
|
|
@@ -129,6 +132,11 @@ module WaterDrop
|
|
|
129
132
|
info(event, 'Closing producer')
|
|
130
133
|
end
|
|
131
134
|
|
|
135
|
+
# @param event [Dry::Events::Event] event that happened with the details
|
|
136
|
+
def on_producer_reloaded(event)
|
|
137
|
+
info(event, 'Producer successfully reloaded')
|
|
138
|
+
end
|
|
139
|
+
|
|
132
140
|
# @param event [Dry::Events::Event] event that happened with the error details
|
|
133
141
|
def on_error_occurred(event)
|
|
134
142
|
error = event[:error]
|
|
@@ -188,7 +196,7 @@ module WaterDrop
|
|
|
188
196
|
# @param log_message [String] message we want to publish
|
|
189
197
|
def info(event, log_message)
|
|
190
198
|
if event.payload.key?(:time)
|
|
191
|
-
@logger.info("[#{event[:producer_id]}] #{log_message} took #{event[:time]} ms")
|
|
199
|
+
@logger.info("[#{event[:producer_id]}] #{log_message} took #{event[:time].round(2)} ms")
|
|
192
200
|
else
|
|
193
201
|
@logger.info("[#{event[:producer_id]}] #{log_message}")
|
|
194
202
|
end
|
|
@@ -10,6 +10,7 @@ module WaterDrop
|
|
|
10
10
|
producer.connected
|
|
11
11
|
producer.closing
|
|
12
12
|
producer.closed
|
|
13
|
+
producer.reloaded
|
|
13
14
|
|
|
14
15
|
message.produced_async
|
|
15
16
|
message.produced_sync
|
|
@@ -21,6 +22,8 @@ module WaterDrop
|
|
|
21
22
|
messages.produced_sync
|
|
22
23
|
messages.buffered
|
|
23
24
|
|
|
25
|
+
oauthbearer.token_refresh
|
|
26
|
+
|
|
24
27
|
transaction.started
|
|
25
28
|
transaction.committed
|
|
26
29
|
transaction.aborted
|
|
@@ -36,7 +36,7 @@ module WaterDrop
|
|
|
36
36
|
setting :rd_kafka_metrics, default: [
|
|
37
37
|
# Client metrics
|
|
38
38
|
RdKafkaMetric.new(:count, :root, 'calls', 'tx_d'),
|
|
39
|
-
RdKafkaMetric.new(:histogram, :root, 'queue.size', '
|
|
39
|
+
RdKafkaMetric.new(:histogram, :root, 'queue.size', 'msg_cnt'),
|
|
40
40
|
|
|
41
41
|
# Broker metrics
|
|
42
42
|
RdKafkaMetric.new(:count, :brokers, 'deliver.attempts', 'txretries_d'),
|
|
@@ -199,6 +199,15 @@ module WaterDrop
|
|
|
199
199
|
tags: default_tags + ["broker:#{broker_statistics['nodename']}"]
|
|
200
200
|
)
|
|
201
201
|
end
|
|
202
|
+
when :topics
|
|
203
|
+
statistics.fetch('topics').each_value do |topic_statistics|
|
|
204
|
+
public_send(
|
|
205
|
+
metric.type,
|
|
206
|
+
metric.name,
|
|
207
|
+
topic_statistics.dig(*metric.key_location),
|
|
208
|
+
tags: default_tags + ["topic:#{topic_statistics['topic']}"]
|
|
209
|
+
)
|
|
210
|
+
end
|
|
202
211
|
else
|
|
203
212
|
raise ArgumentError, metric.scope
|
|
204
213
|
end
|
|
@@ -12,9 +12,6 @@ module WaterDrop
|
|
|
12
12
|
def buffer(message)
|
|
13
13
|
ensure_active!
|
|
14
14
|
|
|
15
|
-
message = middleware.run(message)
|
|
16
|
-
validate_message!(message)
|
|
17
|
-
|
|
18
15
|
@monitor.instrument(
|
|
19
16
|
'message.buffered',
|
|
20
17
|
producer_id: id,
|
|
@@ -32,9 +29,6 @@ module WaterDrop
|
|
|
32
29
|
def buffer_many(messages)
|
|
33
30
|
ensure_active!
|
|
34
31
|
|
|
35
|
-
messages = middleware.run_many(messages)
|
|
36
|
-
messages.each { |message| validate_message!(message) }
|
|
37
|
-
|
|
38
32
|
@monitor.instrument(
|
|
39
33
|
'messages.buffered',
|
|
40
34
|
producer_id: id,
|
|
@@ -52,8 +52,8 @@ module WaterDrop
|
|
|
52
52
|
# @return [Array<Rdkafka::Producer::DeliveryReport>] delivery reports
|
|
53
53
|
#
|
|
54
54
|
# @raise [Rdkafka::RdkafkaError] When adding the messages to rdkafka's queue failed
|
|
55
|
-
# @raise [Rdkafka::Producer::WaitTimeoutError] When the timeout has been reached and
|
|
56
|
-
#
|
|
55
|
+
# @raise [Rdkafka::Producer::WaitTimeoutError] When the timeout has been reached and some
|
|
56
|
+
# handles are still pending
|
|
57
57
|
# @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
|
|
58
58
|
# and the message could not be sent to Kafka
|
|
59
59
|
def produce_many_sync(messages)
|
|
@@ -61,17 +61,37 @@ module WaterDrop
|
|
|
61
61
|
messages.each { |message| validate_message!(message) }
|
|
62
62
|
|
|
63
63
|
dispatched = []
|
|
64
|
+
inline_error = nil
|
|
64
65
|
|
|
65
66
|
@monitor.instrument('messages.produced_sync', producer_id: id, messages: messages) do
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# While most of the librdkafka errors are async and not inline, there are some like
|
|
68
|
+
# buffer overflow that can leak in during the `#produce` itself. When this happens, we
|
|
69
|
+
# still (since it's a sync mode) need to wait on deliveries of things that were
|
|
70
|
+
# successfully delegated to librdkafka. To do this, we catch the inline error and we
|
|
71
|
+
# wait on messages that were in the buffer to reach final state. Then if no error, we
|
|
72
|
+
# check each for error and if none all good. If there was an inline error, we re-raise
|
|
73
|
+
# it with the handlers in final states.
|
|
74
|
+
#
|
|
75
|
+
# Such flow ensures, that we never end up with handlers not being in the final states
|
|
76
|
+
# for the sync flow
|
|
77
|
+
begin
|
|
78
|
+
with_transaction_if_transactional do
|
|
79
|
+
messages.each do |message|
|
|
80
|
+
dispatched << produce(message)
|
|
81
|
+
end
|
|
69
82
|
end
|
|
83
|
+
rescue *SUPPORTED_FLOW_ERRORS => e
|
|
84
|
+
inline_error = e
|
|
70
85
|
end
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
# This will ensure, that we have all verdicts before raising the failure, so we pass
|
|
88
|
+
# all delivery handles having a final verdict
|
|
89
|
+
dispatched.each { |handler| wait(handler, raise_response_error: false) }
|
|
90
|
+
|
|
91
|
+
raise(inline_error) if inline_error
|
|
92
|
+
|
|
93
|
+
# This will raise an error on the first error that have happened
|
|
94
|
+
dispatched.each { |handler| wait(handler) }
|
|
75
95
|
|
|
76
96
|
dispatched
|
|
77
97
|
end
|
|
@@ -82,7 +102,11 @@ module WaterDrop
|
|
|
82
102
|
'error.occurred',
|
|
83
103
|
producer_id: id,
|
|
84
104
|
messages: messages,
|
|
85
|
-
dispatched
|
|
105
|
+
# If it is a transactional producer nothing was successfully dispatched on error, thus
|
|
106
|
+
# we never return any dispatched handlers. While those messages might have reached
|
|
107
|
+
# Kafka, in transactional mode they will not be visible to consumers with correct
|
|
108
|
+
# isolation level.
|
|
109
|
+
dispatched: transactional? ? EMPTY_ARRAY : dispatched,
|
|
86
110
|
error: re_raised,
|
|
87
111
|
type: 'messages.produce_many_sync'
|
|
88
112
|
)
|