waterdrop 2.8.14 → 2.8.16
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/.github/workflows/ci.yml +215 -36
- data/.github/workflows/push.yml +3 -3
- data/.github/workflows/trigger-wiki-refresh.yml +1 -1
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.gitignore +0 -1
- data/.rubocop.yml +87 -0
- data/.ruby-version +1 -1
- data/.yard-lint.yml +172 -72
- data/CHANGELOG.md +13 -0
- data/Gemfile +8 -9
- data/Gemfile.lint +14 -0
- data/Gemfile.lint.lock +123 -0
- data/Gemfile.lock +27 -28
- data/README.md +1 -1
- data/Rakefile +2 -2
- data/bin/integrations +28 -29
- data/bin/verify_topics_naming +8 -8
- data/config/locales/errors.yml +12 -0
- data/docker-compose.oauth.yml +56 -0
- data/docker-compose.yml +1 -1
- data/lib/waterdrop/clients/dummy.rb +9 -0
- data/lib/waterdrop/clients/rdkafka.rb +13 -2
- data/lib/waterdrop/config.rb +32 -5
- data/lib/waterdrop/connection_pool.rb +13 -11
- data/lib/waterdrop/contracts/config.rb +30 -6
- data/lib/waterdrop/contracts/message.rb +2 -2
- data/lib/waterdrop/contracts/poller_config.rb +26 -0
- data/lib/waterdrop/contracts/transactional_offset.rb +2 -2
- data/lib/waterdrop/contracts/variant.rb +18 -18
- data/lib/waterdrop/errors.rb +3 -0
- data/lib/waterdrop/instrumentation/callbacks/delivery.rb +8 -8
- data/lib/waterdrop/instrumentation/callbacks/error.rb +5 -5
- data/lib/waterdrop/instrumentation/callbacks/oauthbearer_token_refresh.rb +4 -4
- data/lib/waterdrop/instrumentation/callbacks/statistics.rb +18 -5
- data/lib/waterdrop/instrumentation/idle_disconnector_listener.rb +4 -4
- data/lib/waterdrop/instrumentation/logger_listener.rb +10 -10
- data/lib/waterdrop/instrumentation/notifications.rb +3 -0
- data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +19 -19
- data/lib/waterdrop/polling/config.rb +52 -0
- data/lib/waterdrop/polling/latch.rb +49 -0
- data/lib/waterdrop/polling/poller.rb +415 -0
- data/lib/waterdrop/polling/queue_pipe.rb +63 -0
- data/lib/waterdrop/polling/state.rb +151 -0
- data/lib/waterdrop/polling.rb +22 -0
- data/lib/waterdrop/producer/async.rb +6 -6
- data/lib/waterdrop/producer/buffer.rb +8 -8
- data/lib/waterdrop/producer/idempotence.rb +3 -3
- data/lib/waterdrop/producer/sync.rb +15 -8
- data/lib/waterdrop/producer/testing.rb +1 -1
- data/lib/waterdrop/producer/transactions.rb +6 -6
- data/lib/waterdrop/producer.rb +113 -30
- data/lib/waterdrop/version.rb +1 -1
- data/lib/waterdrop.rb +15 -10
- data/package-lock.json +331 -0
- data/package.json +9 -0
- data/renovate.json +25 -6
- data/waterdrop.gemspec +23 -23
- metadata +17 -5
- data/.coditsu/ci.yml +0 -3
|
@@ -13,7 +13,7 @@ module WaterDrop
|
|
|
13
13
|
ensure_active!
|
|
14
14
|
|
|
15
15
|
@monitor.instrument(
|
|
16
|
-
|
|
16
|
+
"message.buffered",
|
|
17
17
|
producer_id: id,
|
|
18
18
|
message: message,
|
|
19
19
|
buffer: @messages
|
|
@@ -30,7 +30,7 @@ module WaterDrop
|
|
|
30
30
|
ensure_active!
|
|
31
31
|
|
|
32
32
|
@monitor.instrument(
|
|
33
|
-
|
|
33
|
+
"messages.buffered",
|
|
34
34
|
producer_id: id,
|
|
35
35
|
messages: messages,
|
|
36
36
|
buffer: @messages
|
|
@@ -45,18 +45,18 @@ module WaterDrop
|
|
|
45
45
|
# flushed
|
|
46
46
|
def flush_async
|
|
47
47
|
@monitor.instrument(
|
|
48
|
-
|
|
48
|
+
"buffer.flushed_async",
|
|
49
49
|
producer_id: id,
|
|
50
50
|
messages: @messages
|
|
51
51
|
) { flush(false) }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Flushes the internal buffer to Kafka in a sync way
|
|
55
|
-
# @return [Array<Rdkafka::Producer::
|
|
56
|
-
# flushed
|
|
55
|
+
# @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles for messages that were
|
|
56
|
+
# flushed (handles are in final state, call `#create_result` to get delivery report)
|
|
57
57
|
def flush_sync
|
|
58
58
|
@monitor.instrument(
|
|
59
|
-
|
|
59
|
+
"buffer.flushed_sync",
|
|
60
60
|
producer_id: id,
|
|
61
61
|
messages: @messages
|
|
62
62
|
) { flush(true) }
|
|
@@ -66,8 +66,8 @@ module WaterDrop
|
|
|
66
66
|
|
|
67
67
|
# Method for triggering the buffer
|
|
68
68
|
# @param sync [Boolean] should it flush in a sync way
|
|
69
|
-
# @return [Array<Rdkafka::Producer::DeliveryHandle
|
|
70
|
-
#
|
|
69
|
+
# @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles (in final state for
|
|
70
|
+
# sync, pending for async)
|
|
71
71
|
# @raise [Errors::ProduceManyError] when there was a failure in flushing
|
|
72
72
|
# @note We use this method underneath to provide a different instrumentation for sync and
|
|
73
73
|
# async flushing within the public API
|
|
@@ -10,7 +10,7 @@ module WaterDrop
|
|
|
10
10
|
return true if transactional?
|
|
11
11
|
return @idempotent unless @idempotent.nil?
|
|
12
12
|
|
|
13
|
-
@idempotent = config.kafka.to_h.fetch(:
|
|
13
|
+
@idempotent = config.kafka.to_h.fetch(:"enable.idempotence", false)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Checks if the given error should trigger an idempotent producer reload
|
|
@@ -61,7 +61,7 @@ module WaterDrop
|
|
|
61
61
|
# Users can subscribe to this event and modify event[:caller].config.kafka to change
|
|
62
62
|
# producer config
|
|
63
63
|
@monitor.instrument(
|
|
64
|
-
|
|
64
|
+
"producer.reload",
|
|
65
65
|
producer_id: id,
|
|
66
66
|
error: error,
|
|
67
67
|
attempt: attempt,
|
|
@@ -73,7 +73,7 @@ module WaterDrop
|
|
|
73
73
|
@idempotent = nil
|
|
74
74
|
|
|
75
75
|
@monitor.instrument(
|
|
76
|
-
|
|
76
|
+
"producer.reloaded",
|
|
77
77
|
producer_id: id,
|
|
78
78
|
attempt: attempt
|
|
79
79
|
) do
|
|
@@ -8,7 +8,7 @@ module WaterDrop
|
|
|
8
8
|
#
|
|
9
9
|
# @param message [Hash] hash that complies with the {Contracts::Message} contract
|
|
10
10
|
#
|
|
11
|
-
# @return [Rdkafka::Producer::
|
|
11
|
+
# @return [Rdkafka::Producer::DeliveryReport] delivery report
|
|
12
12
|
#
|
|
13
13
|
# @raise [Rdkafka::RdkafkaError] When adding the message to rdkafka's queue failed
|
|
14
14
|
# @raise [Rdkafka::Producer::WaitTimeoutError] When the timeout has been reached and the
|
|
@@ -20,7 +20,7 @@ module WaterDrop
|
|
|
20
20
|
validate_message!(message)
|
|
21
21
|
|
|
22
22
|
@monitor.instrument(
|
|
23
|
-
|
|
23
|
+
"message.produced_sync",
|
|
24
24
|
producer_id: id,
|
|
25
25
|
message: message
|
|
26
26
|
) do
|
|
@@ -33,11 +33,11 @@ module WaterDrop
|
|
|
33
33
|
raise Errors::ProduceError, e.inspect
|
|
34
34
|
rescue Errors::ProduceError => ex
|
|
35
35
|
@monitor.instrument(
|
|
36
|
-
|
|
36
|
+
"error.occurred",
|
|
37
37
|
producer_id: id,
|
|
38
38
|
message: message,
|
|
39
39
|
error: ex,
|
|
40
|
-
type:
|
|
40
|
+
type: "message.produce_sync"
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
raise ex
|
|
@@ -49,13 +49,20 @@ module WaterDrop
|
|
|
49
49
|
# @param messages [Array<Hash>] array with messages that comply with the
|
|
50
50
|
# {Contracts::Message} contract
|
|
51
51
|
#
|
|
52
|
-
# @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery
|
|
52
|
+
# @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles for messages that were
|
|
53
|
+
# sent (can be used to verify offset, partition, etc via `#create_result`)
|
|
53
54
|
#
|
|
54
55
|
# @raise [Rdkafka::RdkafkaError] When adding the messages to rdkafka's queue failed
|
|
55
56
|
# @raise [Rdkafka::Producer::WaitTimeoutError] When the timeout has been reached and some
|
|
56
57
|
# handles are still pending
|
|
57
58
|
# @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
|
|
58
59
|
# and the message could not be sent to Kafka
|
|
60
|
+
#
|
|
61
|
+
# @note We return delivery handles instead of delivery reports to ensure consistent return
|
|
62
|
+
# types for both success and error flows. In case of partial failures (e.g., mid-batch
|
|
63
|
+
# errors), returning handles guarantees the same type in the `dispatched` collection,
|
|
64
|
+
# allowing uniform error handling. Each handle is in its final state after this method
|
|
65
|
+
# returns, so you can call `handle.create_result` to obtain the delivery report if needed.
|
|
59
66
|
def produce_many_sync(messages)
|
|
60
67
|
messages = middleware.run_many(messages)
|
|
61
68
|
messages.each { |message| validate_message!(message) }
|
|
@@ -63,7 +70,7 @@ module WaterDrop
|
|
|
63
70
|
dispatched = []
|
|
64
71
|
inline_error = nil
|
|
65
72
|
|
|
66
|
-
@monitor.instrument(
|
|
73
|
+
@monitor.instrument("messages.produced_sync", producer_id: id, messages: messages) do
|
|
67
74
|
# While most of the librdkafka errors are async and not inline, there are some like
|
|
68
75
|
# buffer overflow that can leak in during the `#produce` itself. When this happens, we
|
|
69
76
|
# still (since it's a sync mode) need to wait on deliveries of things that were
|
|
@@ -99,7 +106,7 @@ module WaterDrop
|
|
|
99
106
|
re_raised = Errors::ProduceManyError.new(dispatched, e.inspect)
|
|
100
107
|
|
|
101
108
|
@monitor.instrument(
|
|
102
|
-
|
|
109
|
+
"error.occurred",
|
|
103
110
|
producer_id: id,
|
|
104
111
|
messages: messages,
|
|
105
112
|
# If it is a transactional producer nothing was successfully dispatched on error, thus
|
|
@@ -108,7 +115,7 @@ module WaterDrop
|
|
|
108
115
|
# isolation level.
|
|
109
116
|
dispatched: transactional? ? EMPTY_ARRAY : dispatched,
|
|
110
117
|
error: re_raised,
|
|
111
|
-
type:
|
|
118
|
+
type: "messages.produce_many_sync"
|
|
112
119
|
)
|
|
113
120
|
|
|
114
121
|
raise re_raised
|
|
@@ -106,7 +106,7 @@ module WaterDrop
|
|
|
106
106
|
return if client.respond_to?(:trigger_test_fatal_error)
|
|
107
107
|
|
|
108
108
|
# Require the rdkafka testing module if not already loaded
|
|
109
|
-
require
|
|
109
|
+
require "rdkafka/producer/testing" unless defined?(::Rdkafka::Testing)
|
|
110
110
|
|
|
111
111
|
client.singleton_class.include(::Rdkafka::Testing)
|
|
112
112
|
end
|
|
@@ -80,7 +80,7 @@ module WaterDrop
|
|
|
80
80
|
if !e && !finished
|
|
81
81
|
raise(
|
|
82
82
|
Errors::EarlyTransactionExitNotAllowedError,
|
|
83
|
-
<<~ERROR_MSG.tr("\n",
|
|
83
|
+
<<~ERROR_MSG.tr("\n", " ")
|
|
84
84
|
Using `return`, `break` or `throw` to exit a transaction block is not allowed.
|
|
85
85
|
If the `throw` came from `Timeout.timeout(duration)`, pass an exception class as
|
|
86
86
|
a second argument so it doesn't use `throw` to abort its block.
|
|
@@ -110,7 +110,7 @@ module WaterDrop
|
|
|
110
110
|
client.abort_transaction
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
|
-
rescue
|
|
113
|
+
rescue => e
|
|
114
114
|
# If something from rdkafka leaks here, it means there was a non-retryable error that
|
|
115
115
|
# bubbled up. In such cases if we should, we do reload the underling client
|
|
116
116
|
transactional_reload_client_if_needed(e)
|
|
@@ -134,7 +134,7 @@ module WaterDrop
|
|
|
134
134
|
def transactional?
|
|
135
135
|
return @transactional unless @transactional.nil?
|
|
136
136
|
|
|
137
|
-
@transactional = config.kafka.to_h.key?(:
|
|
137
|
+
@transactional = config.kafka.to_h.key?(:"transactional.id")
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
# Checks if we can still retry reloading after a transactional fatal error
|
|
@@ -229,7 +229,7 @@ module WaterDrop
|
|
|
229
229
|
do_retry = e.retryable? && attempt < config.max_attempts_on_transaction_command
|
|
230
230
|
|
|
231
231
|
@monitor.instrument(
|
|
232
|
-
|
|
232
|
+
"error.occurred",
|
|
233
233
|
producer_id: id,
|
|
234
234
|
caller: self,
|
|
235
235
|
error: e,
|
|
@@ -299,7 +299,7 @@ module WaterDrop
|
|
|
299
299
|
# Users can subscribe to this event and modify event[:caller].config.kafka to change
|
|
300
300
|
# producer config. This is useful for escaping fencing loops by changing transactional.id
|
|
301
301
|
@monitor.instrument(
|
|
302
|
-
|
|
302
|
+
"producer.reload",
|
|
303
303
|
producer_id: id,
|
|
304
304
|
error: rd_error,
|
|
305
305
|
attempt: @transaction_fatal_error_attempts,
|
|
@@ -311,7 +311,7 @@ module WaterDrop
|
|
|
311
311
|
@transactional = nil
|
|
312
312
|
|
|
313
313
|
@monitor.instrument(
|
|
314
|
-
|
|
314
|
+
"producer.reloaded",
|
|
315
315
|
producer_id: id,
|
|
316
316
|
attempt: @transaction_fatal_error_attempts
|
|
317
317
|
) do
|
data/lib/waterdrop/producer.rb
CHANGED
|
@@ -62,6 +62,8 @@ module WaterDrop
|
|
|
62
62
|
@closing_thread_id = nil
|
|
63
63
|
@idempotent = nil
|
|
64
64
|
@transactional = nil
|
|
65
|
+
@fd_polling = nil
|
|
66
|
+
@poller = nil
|
|
65
67
|
@idempotent_fatal_error_attempts = 0
|
|
66
68
|
@transaction_fatal_error_attempts = 0
|
|
67
69
|
|
|
@@ -70,7 +72,7 @@ module WaterDrop
|
|
|
70
72
|
|
|
71
73
|
# Instrument producer creation for global listeners
|
|
72
74
|
class_monitor.instrument(
|
|
73
|
-
|
|
75
|
+
"producer.created",
|
|
74
76
|
producer: self,
|
|
75
77
|
producer_id: @id
|
|
76
78
|
)
|
|
@@ -85,9 +87,9 @@ module WaterDrop
|
|
|
85
87
|
raise Errors::ProducerAlreadyConfiguredError, id unless @status.initial?
|
|
86
88
|
|
|
87
89
|
@config = Config
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
.new
|
|
91
|
+
.setup(...)
|
|
92
|
+
.config
|
|
91
93
|
|
|
92
94
|
@id = @config.id
|
|
93
95
|
@monitor = @config.monitor
|
|
@@ -99,7 +101,7 @@ module WaterDrop
|
|
|
99
101
|
|
|
100
102
|
# Instrument producer configuration for global listeners
|
|
101
103
|
class_monitor.instrument(
|
|
102
|
-
|
|
104
|
+
"producer.configured",
|
|
103
105
|
producer: self,
|
|
104
106
|
producer_id: @id,
|
|
105
107
|
config: @config
|
|
@@ -121,7 +123,7 @@ module WaterDrop
|
|
|
121
123
|
|
|
122
124
|
# Instrument producer configuration for global listeners
|
|
123
125
|
class_monitor.instrument(
|
|
124
|
-
|
|
126
|
+
"producer.configured",
|
|
125
127
|
producer: self,
|
|
126
128
|
producer_id: @id,
|
|
127
129
|
config: @config
|
|
@@ -166,12 +168,47 @@ module WaterDrop
|
|
|
166
168
|
@client = Builder.new.call(self, @config)
|
|
167
169
|
|
|
168
170
|
@status.connected!
|
|
169
|
-
@monitor.instrument(
|
|
171
|
+
@monitor.instrument("producer.connected", producer_id: id)
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
@client
|
|
173
175
|
end
|
|
174
176
|
|
|
177
|
+
# Returns the number of messages in the librdkafka producer queue.
|
|
178
|
+
#
|
|
179
|
+
# This count includes:
|
|
180
|
+
# - Messages waiting to be sent to the broker
|
|
181
|
+
# - Messages currently in-flight (being transmitted)
|
|
182
|
+
# - Delivery reports waiting to be processed
|
|
183
|
+
#
|
|
184
|
+
# @return [Integer] the number of pending messages in the rdkafka queue, or 0 if the
|
|
185
|
+
# producer is not connected
|
|
186
|
+
#
|
|
187
|
+
# @note This only counts messages in the rdkafka queue, not the internal WaterDrop buffer.
|
|
188
|
+
# To get the internal buffer count, use `messages.size`.
|
|
189
|
+
#
|
|
190
|
+
# @note Returns 0 when the producer is not connected as there cannot be any pending
|
|
191
|
+
# messages if we haven't connected.
|
|
192
|
+
#
|
|
193
|
+
# @example Check pending messages
|
|
194
|
+
# producer.queue_size #=> 42
|
|
195
|
+
#
|
|
196
|
+
# @example Check total pending work (buffer + rdkafka queue)
|
|
197
|
+
# internal_buffer = producer.messages.size
|
|
198
|
+
# rdkafka_queue = producer.queue_size
|
|
199
|
+
# total_pending = internal_buffer + rdkafka_queue
|
|
200
|
+
def queue_size
|
|
201
|
+
return 0 unless @status.connected?
|
|
202
|
+
|
|
203
|
+
@connecting_mutex.synchronize do
|
|
204
|
+
return 0 unless @client
|
|
205
|
+
|
|
206
|
+
@client.queue_size
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
alias_method :queue_length, :queue_size
|
|
211
|
+
|
|
175
212
|
# Fetches and caches the partition count of a topic
|
|
176
213
|
#
|
|
177
214
|
# @param topic [String] topic for which we want to get the number of partitions
|
|
@@ -189,7 +226,7 @@ module WaterDrop
|
|
|
189
226
|
# purge the internal WaterDrop buffer but will also purge the librdkafka queue as well as
|
|
190
227
|
# will cancel any outgoing messages dispatches.
|
|
191
228
|
def purge
|
|
192
|
-
@monitor.instrument(
|
|
229
|
+
@monitor.instrument("buffer.purged", producer_id: id) do
|
|
193
230
|
@buffer_mutex.synchronize do
|
|
194
231
|
@messages = []
|
|
195
232
|
end
|
|
@@ -218,7 +255,7 @@ module WaterDrop
|
|
|
218
255
|
Variant.new(self, **args)
|
|
219
256
|
end
|
|
220
257
|
|
|
221
|
-
|
|
258
|
+
alias_method :variant, :with
|
|
222
259
|
|
|
223
260
|
# Returns and caches the middleware object that may be used
|
|
224
261
|
# @return [WaterDrop::Producer::Middleware]
|
|
@@ -261,9 +298,12 @@ module WaterDrop
|
|
|
261
298
|
return false unless @operations_in_progress.value.zero?
|
|
262
299
|
|
|
263
300
|
@status.disconnecting!
|
|
264
|
-
@monitor.instrument(
|
|
301
|
+
@monitor.instrument("producer.disconnecting", producer_id: id)
|
|
302
|
+
|
|
303
|
+
@monitor.instrument("producer.disconnected", producer_id: id) do
|
|
304
|
+
# Unregister from poller before closing if fiber polling is enabled
|
|
305
|
+
unregister_from_poller
|
|
265
306
|
|
|
266
|
-
@monitor.instrument('producer.disconnected', producer_id: id) do
|
|
267
307
|
# Close the client
|
|
268
308
|
@client.close
|
|
269
309
|
@client = nil
|
|
@@ -298,6 +338,16 @@ module WaterDrop
|
|
|
298
338
|
# @param force [Boolean] should we force closing even with outstanding messages after the
|
|
299
339
|
# max wait timeout
|
|
300
340
|
def close(force: false)
|
|
341
|
+
# When closing from within the FD poller thread (e.g., from a callback like
|
|
342
|
+
# message.acknowledged or error.occurred), we must delegate to a background thread.
|
|
343
|
+
# Close performs flush which waits for delivery reports, but delivery reports require
|
|
344
|
+
# the poller to poll. Since we're ON the poller thread inside a callback, this would
|
|
345
|
+
# deadlock. Spawning a thread allows the callback to return, letting the poller continue.
|
|
346
|
+
if fd_polling? && poller.in_poller_thread?
|
|
347
|
+
Thread.new { close(force: force) }
|
|
348
|
+
return
|
|
349
|
+
end
|
|
350
|
+
|
|
301
351
|
# If we already own the transactional mutex, it means we are inside of a transaction and
|
|
302
352
|
# it should not we allowed to close the producer in such a case.
|
|
303
353
|
if @transaction_mutex.locked? && @transaction_mutex.owned?
|
|
@@ -311,11 +361,11 @@ module WaterDrop
|
|
|
311
361
|
return unless @status.active?
|
|
312
362
|
|
|
313
363
|
@monitor.instrument(
|
|
314
|
-
|
|
364
|
+
"producer.closed",
|
|
315
365
|
producer_id: id
|
|
316
366
|
) do
|
|
317
367
|
@status.closing!
|
|
318
|
-
@monitor.instrument(
|
|
368
|
+
@monitor.instrument("producer.closing", producer_id: id)
|
|
319
369
|
|
|
320
370
|
# No need for auto-gc if everything got closed by us
|
|
321
371
|
# This should be used only in case a producer was not closed properly and forgotten
|
|
@@ -355,6 +405,9 @@ module WaterDrop
|
|
|
355
405
|
purge if force
|
|
356
406
|
end
|
|
357
407
|
|
|
408
|
+
# Unregister from poller before closing if fiber polling is enabled
|
|
409
|
+
unregister_from_poller
|
|
410
|
+
|
|
358
411
|
@client.close
|
|
359
412
|
|
|
360
413
|
@client = nil
|
|
@@ -391,20 +444,38 @@ module WaterDrop
|
|
|
391
444
|
@buffer_mutex.unlock
|
|
392
445
|
end
|
|
393
446
|
else
|
|
394
|
-
parts <<
|
|
447
|
+
parts << "buffer_size=busy"
|
|
395
448
|
end
|
|
396
449
|
|
|
397
450
|
# Check if client is connected without triggering connection
|
|
398
451
|
parts << if @status.connected?
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
452
|
+
"connected=true"
|
|
453
|
+
else
|
|
454
|
+
"connected=false"
|
|
455
|
+
end
|
|
403
456
|
|
|
404
457
|
parts << "operations=#{@operations_in_progress.value}"
|
|
405
|
-
parts <<
|
|
458
|
+
parts << "in_transaction=true" if @transaction_mutex.locked?
|
|
459
|
+
|
|
460
|
+
"#<#{self.class.name}:#{format("%#x", object_id)} #{parts.join(" ")}>"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# @return [Boolean] true if FD-based polling mode is enabled
|
|
464
|
+
def fd_polling?
|
|
465
|
+
return @fd_polling unless @fd_polling.nil?
|
|
466
|
+
return false unless config
|
|
467
|
+
|
|
468
|
+
@fd_polling = config.polling.mode == :fd
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Returns the poller instance for this producer
|
|
472
|
+
# @return [WaterDrop::Polling::Poller] custom poller if configured, otherwise the global
|
|
473
|
+
# singleton poller
|
|
474
|
+
def poller
|
|
475
|
+
return @poller unless @poller.nil?
|
|
476
|
+
return nil unless config
|
|
406
477
|
|
|
407
|
-
|
|
478
|
+
@poller = config.polling.poller || Polling::Poller.instance
|
|
408
479
|
end
|
|
409
480
|
|
|
410
481
|
private
|
|
@@ -438,8 +509,7 @@ module WaterDrop
|
|
|
438
509
|
# final result and it is an error.
|
|
439
510
|
def wait(handler, raise_response_error: true)
|
|
440
511
|
handler.wait(
|
|
441
|
-
|
|
442
|
-
max_wait_timeout: current_variant.max_wait_timeout / 1_000.0,
|
|
512
|
+
max_wait_timeout_ms: current_variant.max_wait_timeout,
|
|
443
513
|
raise_response_error: raise_response_error
|
|
444
514
|
)
|
|
445
515
|
end
|
|
@@ -479,10 +549,10 @@ module WaterDrop
|
|
|
479
549
|
end
|
|
480
550
|
|
|
481
551
|
result = if transactional?
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
552
|
+
transaction { client.produce(**message) }
|
|
553
|
+
else
|
|
554
|
+
client.produce(**message)
|
|
555
|
+
end
|
|
486
556
|
|
|
487
557
|
# Reset attempts counter on successful produce
|
|
488
558
|
@idempotent_fatal_error_attempts = 0
|
|
@@ -499,10 +569,10 @@ module WaterDrop
|
|
|
499
569
|
|
|
500
570
|
# Instrument error.occurred before attempting reload for visibility
|
|
501
571
|
@monitor.instrument(
|
|
502
|
-
|
|
572
|
+
"error.occurred",
|
|
503
573
|
producer_id: id,
|
|
504
574
|
error: e,
|
|
505
|
-
type:
|
|
575
|
+
type: "librdkafka.idempotent_fatal_error",
|
|
506
576
|
attempt: @idempotent_fatal_error_attempts
|
|
507
577
|
)
|
|
508
578
|
|
|
@@ -526,7 +596,7 @@ module WaterDrop
|
|
|
526
596
|
# in an infinite loop, effectively hanging the processing
|
|
527
597
|
raise unless monotonic_now - produce_time < @config.wait_timeout_on_queue_full
|
|
528
598
|
|
|
529
|
-
label = caller_locations(2, 1)[0].label.split.last.split(
|
|
599
|
+
label = caller_locations(2, 1)[0].label.split.last.split("#").last
|
|
530
600
|
|
|
531
601
|
# We use this syntax here because we want to preserve the original `#cause` when we
|
|
532
602
|
# instrument the error and there is no way to manually assign `#cause` value. We want to keep
|
|
@@ -546,7 +616,7 @@ module WaterDrop
|
|
|
546
616
|
# If this type of event happens too often, it may indicate that the buffer settings are
|
|
547
617
|
# not well configured.
|
|
548
618
|
@monitor.instrument(
|
|
549
|
-
|
|
619
|
+
"error.occurred",
|
|
550
620
|
producer_id: id,
|
|
551
621
|
message: message,
|
|
552
622
|
error: e,
|
|
@@ -570,9 +640,22 @@ module WaterDrop
|
|
|
570
640
|
def reload!
|
|
571
641
|
@client.flush(current_variant.max_wait_timeout)
|
|
572
642
|
purge
|
|
643
|
+
# Unregister from poller before closing if fiber polling is enabled
|
|
644
|
+
unregister_from_poller
|
|
573
645
|
@client.close
|
|
574
646
|
@client = nil
|
|
575
647
|
@status.configured!
|
|
576
648
|
end
|
|
649
|
+
|
|
650
|
+
# Unregisters this producer from its poller
|
|
651
|
+
#
|
|
652
|
+
# @note We only unregister when fd_polling? is true because thread-mode producers never
|
|
653
|
+
# register with the Poller. The Poller.unregister method handles unregistered producers
|
|
654
|
+
# gracefully, but this guard avoids making unnecessary unregister calls in thread mode.
|
|
655
|
+
def unregister_from_poller
|
|
656
|
+
return unless fd_polling?
|
|
657
|
+
|
|
658
|
+
poller.unregister(self)
|
|
659
|
+
end
|
|
577
660
|
end
|
|
578
661
|
end
|
data/lib/waterdrop/version.rb
CHANGED
data/lib/waterdrop.rb
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# External components
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
10
|
-
require
|
|
4
|
+
require "delegate"
|
|
5
|
+
require "forwardable"
|
|
6
|
+
require "json"
|
|
7
|
+
require "zeitwerk"
|
|
8
|
+
require "securerandom"
|
|
9
|
+
require "singleton"
|
|
10
|
+
require "karafka-core"
|
|
11
|
+
require "pathname"
|
|
11
12
|
|
|
12
13
|
# WaterDrop library
|
|
13
14
|
module WaterDrop
|
|
14
15
|
class << self
|
|
15
16
|
# @return [String] root path of this gem
|
|
16
17
|
def gem_root
|
|
17
|
-
Pathname.new(File.expand_path(
|
|
18
|
+
Pathname.new(File.expand_path("..", __dir__))
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
# @return [WaterDrop::Instrumentation::ClassMonitor] global monitor for
|
|
@@ -34,15 +35,19 @@ module WaterDrop
|
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# @deprecated Use #monitor instead. This method is provided for backward compatibility only.
|
|
37
|
-
|
|
38
|
+
alias_method :instrumentation, :monitor
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
loader = Zeitwerk::Loader.for_gem
|
|
42
|
-
loader.inflector.inflect(
|
|
43
|
+
loader.inflector.inflect("waterdrop" => "WaterDrop")
|
|
43
44
|
# Do not load vendors instrumentation components. Those need to be required manually if needed
|
|
44
45
|
loader.ignore("#{__dir__}/waterdrop/instrumentation/vendors/**/*.rb")
|
|
45
46
|
# Do not load testing components. Those need to be required manually in test environments
|
|
46
47
|
loader.ignore("#{__dir__}/waterdrop/producer/testing.rb")
|
|
47
48
|
loader.setup
|
|
48
49
|
loader.eager_load
|
|
50
|
+
|
|
51
|
+
# Setup the poller. Even if users do not use it, it is few objects and we prevent any potential
|
|
52
|
+
# race conditions later
|
|
53
|
+
WaterDrop::Polling::Poller.instance
|