waterdrop 2.4.11 → 2.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +4 -4
- data/README.md +38 -9
- data/lib/waterdrop/config.rb +11 -0
- data/lib/waterdrop/errors.rb +9 -6
- data/lib/waterdrop/instrumentation/callbacks/delivery.rb +24 -0
- data/lib/waterdrop/instrumentation/callbacks/error.rb +2 -0
- data/lib/waterdrop/instrumentation/vendors/datadog/{listener.rb → metrics_listener.rb} +1 -1
- data/lib/waterdrop/patches/rdkafka/client.rb +34 -0
- data/lib/waterdrop/patches/rdkafka/producer.rb +8 -0
- data/lib/waterdrop/producer/async.rb +32 -2
- data/lib/waterdrop/producer/buffer.rb +4 -30
- data/lib/waterdrop/producer/sync.rb +38 -15
- data/lib/waterdrop/producer.rb +61 -1
- data/lib/waterdrop/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b66f5a9cb1c6fe80fe594777cb60f9fd20f120c2a897ab439404c825503bb37
|
4
|
+
data.tar.gz: 985491a90694c7c729e5c2dd8a581127c96ec26a5eb5eda53d03bc32ab463ee6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ec7c01eb151ad4142f7eccfb988c56394f53b4679e480a9d8706c73323e3f6a25f8f704595fe56d5ba45f7995756add34931cf91bfcb079be6873bcc5563371
|
7
|
+
data.tar.gz: 94261472ac4786fd7911bce919b45267b0d1dc7298965c37ceee51718733d72a650c50745fabac7a0a8604909414b4efb6233640f3f24c499e51f42316d3597a
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,39 @@
|
|
1
1
|
# WaterDrop changelog
|
2
2
|
|
3
|
+
## 2.5.1 (2023-03-09)
|
4
|
+
- [Feature] Introduce a configurable backoff upon `librdkafka` queue full (false by default).
|
5
|
+
|
6
|
+
## 2.5.0 (2023-03-04)
|
7
|
+
- [Feature] Pipe **all** the errors including synchronous errors via the `error.occurred`.
|
8
|
+
- [Improvement] Pipe delivery errors that occurred not via the error callback using the `error.occurred` channel.
|
9
|
+
- [Improvement] Introduce `WaterDrop::Errors::ProduceError` and `WaterDrop::Errors::ProduceManyError` for any inline raised errors that occur. You can get the original error by using the `#cause`.
|
10
|
+
- [Improvement] Include `#dispatched` messages handler in the `WaterDrop::Errors::ProduceManyError` error, to be able to understand which of the messages were delegated to `librdkafka` prior to the failure.
|
11
|
+
- [Maintenance] Remove the `WaterDrop::Errors::FlushFailureError` in favour of correct error that occurred to unify the error handling.
|
12
|
+
- [Maintenance] Rename `Datadog::Listener` to `Datadog::MetricsListener` to align with Karafka (#329).
|
13
|
+
- [Fix] Do **not** flush when there is no data to flush in the internal buffer.
|
14
|
+
- [Fix] Wait on the final data flush for short-lived producers to make sure, that the message is actually dispatched by `librdkafka` or timeout.
|
15
|
+
|
16
|
+
### Upgrade notes
|
17
|
+
|
18
|
+
Please note, this **is** a **breaking** release, hence `2.5.0`.
|
19
|
+
|
20
|
+
1. If you used to catch `WaterDrop::Errors::FlushFailureError` now you need to catch `WaterDrop::Errors::ProduceError`. `WaterDrop::Errors::ProduceManyError` is based on the `ProduceError`, hence it should be enough.
|
21
|
+
2. Prior to `2.5.0` there was always a chance of partial dispatches via `produce_many_` methods. Now you can get the info on all the errors via `error.occurred`.
|
22
|
+
3. Inline `Rdkafka::RdkafkaError` are now re-raised via `WaterDrop::Errors::ProduceError` and available under `#cause`. Async `Rdkafka::RdkafkaError` errors are still directly available and you can differentiate between errors using the event `type`.
|
23
|
+
4. If you are using the Datadog listener, you need to:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# Replace require:
|
27
|
+
require 'waterdrop/instrumentation/vendors/datadog/listener'
|
28
|
+
# With
|
29
|
+
require 'waterdrop/instrumentation/vendors/datadog/metrics_listener'
|
30
|
+
|
31
|
+
# Replace references of
|
32
|
+
::WaterDrop::Instrumentation::Vendors::Datadog::Listener.new
|
33
|
+
# With
|
34
|
+
::WaterDrop::Instrumentation::Vendors::Datadog::MetricsListener.new
|
35
|
+
```
|
36
|
+
|
3
37
|
## 2.4.11 (2023-02-24)
|
4
38
|
- Replace the local rspec locator with generalized core one.
|
5
39
|
- Make `::WaterDrop::Instrumentation::Notifications::EVENTS` list public for anyone wanting to re-bind those into a different notification bus.
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
waterdrop (2.
|
4
|
+
waterdrop (2.5.1)
|
5
5
|
karafka-core (>= 2.0.12, < 3.0.0)
|
6
6
|
zeitwerk (~> 2.3)
|
7
7
|
|
@@ -14,7 +14,7 @@ GEM
|
|
14
14
|
minitest (>= 5.1)
|
15
15
|
tzinfo (~> 2.0)
|
16
16
|
byebug (11.1.3)
|
17
|
-
concurrent-ruby (1.2.
|
17
|
+
concurrent-ruby (1.2.2)
|
18
18
|
diff-lcs (1.5.0)
|
19
19
|
docile (1.4.0)
|
20
20
|
factory_bot (6.2.1)
|
@@ -30,7 +30,7 @@ GEM
|
|
30
30
|
mini_portile2 (~> 2.6)
|
31
31
|
rake (> 12)
|
32
32
|
mini_portile2 (2.8.1)
|
33
|
-
minitest (5.
|
33
|
+
minitest (5.18.0)
|
34
34
|
rake (13.0.6)
|
35
35
|
rspec (3.12.0)
|
36
36
|
rspec-core (~> 3.12.0)
|
@@ -67,4 +67,4 @@ DEPENDENCIES
|
|
67
67
|
waterdrop!
|
68
68
|
|
69
69
|
BUNDLED WITH
|
70
|
-
2.4.
|
70
|
+
2.4.7
|
data/README.md
CHANGED
@@ -29,6 +29,7 @@ It:
|
|
29
29
|
* [Buffering](#buffering)
|
30
30
|
+ [Using WaterDrop to buffer messages based on the application logic](#using-waterdrop-to-buffer-messages-based-on-the-application-logic)
|
31
31
|
+ [Using WaterDrop with rdkafka buffers to achieve periodic auto-flushing](#using-waterdrop-with-rdkafka-buffers-to-achieve-periodic-auto-flushing)
|
32
|
+
* [Idempotence](#idempotence)
|
32
33
|
* [Compression](#compression)
|
33
34
|
- [Instrumentation](#instrumentation)
|
34
35
|
* [Usage statistics](#usage-statistics)
|
@@ -92,13 +93,15 @@ end
|
|
92
93
|
|
93
94
|
Some of the options are:
|
94
95
|
|
95
|
-
| Option
|
96
|
-
|
97
|
-
| `id`
|
98
|
-
| `logger`
|
99
|
-
| `deliver`
|
100
|
-
| `max_wait_timeout`
|
101
|
-
| `wait_timeout`
|
96
|
+
| Option | Description |
|
97
|
+
|------------------------------|------------------------------------------------------------------|
|
98
|
+
| `id` | id of the producer for instrumentation and logging |
|
99
|
+
| `logger` | Logger that we want to use |
|
100
|
+
| `deliver` | Should we send messages to Kafka or just fake the delivery |
|
101
|
+
| `max_wait_timeout` | Waits that long for the delivery report or raises an error |
|
102
|
+
| `wait_timeout` | Waits that long before re-check of delivery report availability |
|
103
|
+
| `wait_on_queue_full` | Should be wait on queue full or raise an error when that happens |
|
104
|
+
| `wait_on_queue_full_timeout` | Waits that long before retry when queue is full |
|
102
105
|
|
103
106
|
Full list of the root configuration options is available [here](https://github.com/karafka/waterdrop/blob/master/lib/waterdrop/config.rb#L25).
|
104
107
|
|
@@ -206,6 +209,30 @@ WaterDrop producers support buffering messages in their internal buffers and on
|
|
206
209
|
|
207
210
|
This means that depending on your use case, you can achieve both granular buffering and flushing control when needed with context awareness and periodic and size-based flushing functionalities.
|
208
211
|
|
212
|
+
### Idempotence
|
213
|
+
|
214
|
+
When idempotence is enabled, the producer will ensure that messages are successfully produced exactly once and in the original production order.
|
215
|
+
|
216
|
+
To enable idempotence, you need to set the `enable.idempotence` kafka scope setting to `true`:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
WaterDrop::Producer.new do |config|
|
220
|
+
config.deliver = true
|
221
|
+
config.kafka = {
|
222
|
+
'bootstrap.servers': 'localhost:9092',
|
223
|
+
'enable.idempotence': true
|
224
|
+
}
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
The following Kafka configuration properties are adjusted automatically (if not modified by the user) when idempotence is enabled:
|
229
|
+
|
230
|
+
- `max.in.flight.requests.per.connection` set to `5`
|
231
|
+
- `retries` set to `2147483647`
|
232
|
+
- `acks` set to `all`
|
233
|
+
|
234
|
+
The idempotent producer ensures that messages are always delivered in the correct order and without duplicates. In other words, when an idempotent producer sends a message, the messaging system ensures that the message is only delivered once to the message broker and subsequently to the consumers, even if the producer tries to send the message multiple times.
|
235
|
+
|
209
236
|
### Compression
|
210
237
|
|
211
238
|
WaterDrop supports following compression types:
|
@@ -387,6 +414,8 @@ end
|
|
387
414
|
# WaterDrop error occurred: Local: Broker transport failure (transport)
|
388
415
|
```
|
389
416
|
|
417
|
+
**Note:** `error.occurred` will also include any errors originating from `librdkafka` for synchronous operations, including those that are raised back to the end user.
|
418
|
+
|
390
419
|
### Acknowledgment notifications
|
391
420
|
|
392
421
|
WaterDrop allows you to listen to Kafka messages' acknowledgment events. This will enable you to monitor deliveries of messages from WaterDrop even when using asynchronous dispatch methods.
|
@@ -423,7 +452,7 @@ WaterDrop comes with (optional) full Datadog and StatsD integration that you can
|
|
423
452
|
```ruby
|
424
453
|
# require datadog/statsd and the listener as it is not loaded by default
|
425
454
|
require 'datadog/statsd'
|
426
|
-
require 'waterdrop/instrumentation/vendors/datadog/
|
455
|
+
require 'waterdrop/instrumentation/vendors/datadog/metrics_listener'
|
427
456
|
|
428
457
|
# initialize your producer with statistics.interval.ms enabled so the metrics are published
|
429
458
|
producer = WaterDrop::Producer.new do |config|
|
@@ -435,7 +464,7 @@ producer = WaterDrop::Producer.new do |config|
|
|
435
464
|
end
|
436
465
|
|
437
466
|
# initialize the listener with statsd client
|
438
|
-
listener = ::WaterDrop::Instrumentation::Vendors::Datadog::
|
467
|
+
listener = ::WaterDrop::Instrumentation::Vendors::Datadog::MetricsListener.new do |config|
|
439
468
|
config.client = Datadog::Statsd.new('localhost', 8125)
|
440
469
|
# Publish host as a tag alongside the rest of tags
|
441
470
|
config.default_tags = ["host:#{Socket.gethostname}"]
|
data/lib/waterdrop/config.rb
CHANGED
@@ -50,6 +50,17 @@ module WaterDrop
|
|
50
50
|
# delivery report. In a really robust systems, this describes the min-delivery time
|
51
51
|
# for a single sync message when produced in isolation
|
52
52
|
setting :wait_timeout, default: 0.005 # 5 milliseconds
|
53
|
+
# option [Boolean] should we upon detecting full librdkafka queue backoff and retry or should
|
54
|
+
# we raise an exception.
|
55
|
+
# When this is set to `true`, upon full queue, we won't raise an error. There will be error
|
56
|
+
# in the `error.occurred` notification pipeline with a proper type as while this is
|
57
|
+
# recoverable, in a high number it still may mean issues.
|
58
|
+
# Waiting is one of the recommended strategies.
|
59
|
+
setting :wait_on_queue_full, default: false
|
60
|
+
# option [Integer] how long (in seconds) should we backoff before a retry when queue is full
|
61
|
+
# The retry will happen with the same message and backoff should give us some time to
|
62
|
+
# dispatch previously buffered messages.
|
63
|
+
setting :wait_on_queue_full_timeout, default: 0.1
|
53
64
|
# option [Boolean] should we send messages. Setting this to false can be really useful when
|
54
65
|
# testing and or developing because when set to false, won't actually ping Kafka but will
|
55
66
|
# run all the validations, etc
|
data/lib/waterdrop/errors.rb
CHANGED
@@ -29,15 +29,18 @@ module WaterDrop
|
|
29
29
|
# contact us as it is an error.
|
30
30
|
StatusInvalidError = Class.new(BaseError)
|
31
31
|
|
32
|
-
# Raised when during
|
33
|
-
|
34
|
-
attr_reader :dispatched_messages
|
32
|
+
# Raised when there is an inline error during single message produce operations
|
33
|
+
ProduceError = Class.new(BaseError)
|
35
34
|
|
36
|
-
|
35
|
+
# Raised when during messages producing something bad happened inline
|
36
|
+
class ProduceManyError < ProduceError
|
37
|
+
attr_reader :dispatched
|
38
|
+
|
39
|
+
# @param dispatched [Array<Rdkafka::Producer::DeliveryHandle>] handlers of the
|
37
40
|
# messages that we've dispatched
|
38
|
-
def initialize(
|
41
|
+
def initialize(dispatched)
|
39
42
|
super()
|
40
|
-
@
|
43
|
+
@dispatched = dispatched
|
41
44
|
end
|
42
45
|
end
|
43
46
|
end
|
@@ -17,6 +17,30 @@ module WaterDrop
|
|
17
17
|
# Emits delivery details to the monitor
|
18
18
|
# @param delivery_report [Rdkafka::Producer::DeliveryReport] delivery report
|
19
19
|
def call(delivery_report)
|
20
|
+
if delivery_report.error.to_i.positive?
|
21
|
+
instrument_error(delivery_report)
|
22
|
+
else
|
23
|
+
instrument_acknowledged(delivery_report)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# @param delivery_report [Rdkafka::Producer::DeliveryReport] delivery report
|
30
|
+
def instrument_error(delivery_report)
|
31
|
+
@monitor.instrument(
|
32
|
+
'error.occurred',
|
33
|
+
caller: self,
|
34
|
+
error: ::Rdkafka::RdkafkaError.new(delivery_report.error),
|
35
|
+
producer_id: @producer_id,
|
36
|
+
offset: delivery_report.offset,
|
37
|
+
partition: delivery_report.partition,
|
38
|
+
type: 'librdkafka.dispatch_error'
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param delivery_report [Rdkafka::Producer::DeliveryReport] delivery report
|
43
|
+
def instrument_acknowledged(delivery_report)
|
20
44
|
@monitor.instrument(
|
21
45
|
'message.acknowledged',
|
22
46
|
producer_id: @producer_id,
|
@@ -18,6 +18,8 @@ module WaterDrop
|
|
18
18
|
# @param client_name [String] rdkafka client name
|
19
19
|
# @param error [Rdkafka::Error] error that occurred
|
20
20
|
# @note It will only instrument on errors of the client of our producer
|
21
|
+
# @note When there is a particular message produce error (not internal error), the error
|
22
|
+
# is shipped via the delivery callback, not via error callback.
|
21
23
|
def call(client_name, error)
|
22
24
|
# Emit only errors related to our client
|
23
25
|
# Same as with statistics (mor explanation there)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WaterDrop
|
4
|
+
module Patches
|
5
|
+
module Rdkafka
|
6
|
+
# Patches for the producer client
|
7
|
+
module Client
|
8
|
+
# @param _object_id [nil] rdkafka API compatibility argument
|
9
|
+
# @param timeout_ms [Integer] final flush timeout in ms
|
10
|
+
def close(_object_id = nil, timeout_ms = 5_000)
|
11
|
+
return unless @native
|
12
|
+
|
13
|
+
# Indicate to polling thread that we're closing
|
14
|
+
@polling_thread[:closing] = true
|
15
|
+
# Wait for the polling thread to finish up
|
16
|
+
@polling_thread.join
|
17
|
+
|
18
|
+
::Rdkafka::Bindings.rd_kafka_flush(@native, timeout_ms)
|
19
|
+
::Rdkafka::Bindings.rd_kafka_destroy(@native)
|
20
|
+
|
21
|
+
@native = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
::Rdkafka::Bindings.attach_function(
|
29
|
+
:rd_kafka_flush,
|
30
|
+
%i[pointer int],
|
31
|
+
:void
|
32
|
+
)
|
33
|
+
|
34
|
+
Rdkafka::Producer::Client.prepend WaterDrop::Patches::Rdkafka::Client
|
@@ -70,6 +70,14 @@ module WaterDrop
|
|
70
70
|
|
71
71
|
@_inner_kafka
|
72
72
|
end
|
73
|
+
|
74
|
+
# Closes our librdkafka instance with the flush patch
|
75
|
+
# @param timeout_ms [Integer] flush timeout
|
76
|
+
def close(timeout_ms = 5_000)
|
77
|
+
ObjectSpace.undefine_finalizer(self)
|
78
|
+
|
79
|
+
@client.close(nil, timeout_ms)
|
80
|
+
end
|
73
81
|
end
|
74
82
|
end
|
75
83
|
end
|
@@ -23,7 +23,19 @@ module WaterDrop
|
|
23
23
|
'message.produced_async',
|
24
24
|
producer_id: id,
|
25
25
|
message: message
|
26
|
-
) {
|
26
|
+
) { produce(message) }
|
27
|
+
rescue *SUPPORTED_FLOW_ERRORS
|
28
|
+
re_raised = Errors::ProduceError.new
|
29
|
+
|
30
|
+
@monitor.instrument(
|
31
|
+
'error.occurred',
|
32
|
+
producer_id: id,
|
33
|
+
message: message,
|
34
|
+
error: re_raised,
|
35
|
+
type: 'message.produce_async'
|
36
|
+
)
|
37
|
+
|
38
|
+
raise re_raised
|
27
39
|
end
|
28
40
|
|
29
41
|
# Produces many messages to Kafka and does not wait for them to be delivered
|
@@ -39,6 +51,7 @@ module WaterDrop
|
|
39
51
|
def produce_many_async(messages)
|
40
52
|
ensure_active!
|
41
53
|
|
54
|
+
dispatched = []
|
42
55
|
messages = middleware.run_many(messages)
|
43
56
|
messages.each { |message| validate_message!(message) }
|
44
57
|
|
@@ -47,8 +60,25 @@ module WaterDrop
|
|
47
60
|
producer_id: id,
|
48
61
|
messages: messages
|
49
62
|
) do
|
50
|
-
messages.
|
63
|
+
messages.each do |message|
|
64
|
+
dispatched << produce(message)
|
65
|
+
end
|
66
|
+
|
67
|
+
dispatched
|
51
68
|
end
|
69
|
+
rescue *SUPPORTED_FLOW_ERRORS
|
70
|
+
re_raised = Errors::ProduceManyError.new(dispatched)
|
71
|
+
|
72
|
+
@monitor.instrument(
|
73
|
+
'error.occurred',
|
74
|
+
producer_id: id,
|
75
|
+
messages: messages,
|
76
|
+
dispatched: dispatched,
|
77
|
+
error: re_raised,
|
78
|
+
type: 'messages.produce_many_async'
|
79
|
+
)
|
80
|
+
|
81
|
+
raise re_raised
|
52
82
|
end
|
53
83
|
end
|
54
84
|
end
|
@@ -4,14 +4,6 @@ module WaterDrop
|
|
4
4
|
class Producer
|
5
5
|
# Component for buffered operations
|
6
6
|
module Buffer
|
7
|
-
# Exceptions we catch when dispatching messages from a buffer
|
8
|
-
RESCUED_ERRORS = [
|
9
|
-
Rdkafka::RdkafkaError,
|
10
|
-
Rdkafka::Producer::DeliveryHandle::WaitTimeoutError
|
11
|
-
].freeze
|
12
|
-
|
13
|
-
private_constant :RESCUED_ERRORS
|
14
|
-
|
15
7
|
# Adds given message into the internal producer buffer without flushing it to Kafka
|
16
8
|
#
|
17
9
|
# @param message [Hash] hash that complies with the {Contracts::Message} contract
|
@@ -85,39 +77,21 @@ module WaterDrop
|
|
85
77
|
# @param sync [Boolean] should it flush in a sync way
|
86
78
|
# @return [Array<Rdkafka::Producer::DeliveryHandle, Rdkafka::Producer::DeliveryReport>]
|
87
79
|
# delivery handles for async or delivery reports for sync
|
88
|
-
# @raise [Errors::
|
80
|
+
# @raise [Errors::ProduceManyError] when there was a failure in flushing
|
89
81
|
# @note We use this method underneath to provide a different instrumentation for sync and
|
90
82
|
# async flushing within the public API
|
91
83
|
def flush(sync)
|
92
84
|
data_for_dispatch = nil
|
93
|
-
dispatched = []
|
94
85
|
|
95
86
|
@buffer_mutex.synchronize do
|
96
87
|
data_for_dispatch = @messages
|
97
88
|
@messages = Concurrent::Array.new
|
98
89
|
end
|
99
90
|
|
100
|
-
|
101
|
-
|
102
|
-
return dispatched unless sync
|
103
|
-
|
104
|
-
dispatched.map do |handler|
|
105
|
-
handler.wait(
|
106
|
-
max_wait_timeout: @config.max_wait_timeout,
|
107
|
-
wait_timeout: @config.wait_timeout
|
108
|
-
)
|
109
|
-
end
|
110
|
-
rescue *RESCUED_ERRORS => e
|
111
|
-
@monitor.instrument(
|
112
|
-
'error.occurred',
|
113
|
-
caller: self,
|
114
|
-
error: e,
|
115
|
-
producer_id: id,
|
116
|
-
dispatched: dispatched,
|
117
|
-
type: sync ? 'buffer.flushed_sync.error' : 'buffer.flush_async.error'
|
118
|
-
)
|
91
|
+
# Do nothing if nothing to flush
|
92
|
+
return data_for_dispatch if data_for_dispatch.empty?
|
119
93
|
|
120
|
-
|
94
|
+
sync ? produce_many_sync(data_for_dispatch) : produce_many_async(data_for_dispatch)
|
121
95
|
end
|
122
96
|
end
|
123
97
|
end
|
@@ -26,13 +26,20 @@ module WaterDrop
|
|
26
26
|
producer_id: id,
|
27
27
|
message: message
|
28
28
|
) do
|
29
|
-
|
30
|
-
.produce(**message)
|
31
|
-
.wait(
|
32
|
-
max_wait_timeout: @config.max_wait_timeout,
|
33
|
-
wait_timeout: @config.wait_timeout
|
34
|
-
)
|
29
|
+
wait(produce(message))
|
35
30
|
end
|
31
|
+
rescue *SUPPORTED_FLOW_ERRORS
|
32
|
+
re_raised = Errors::ProduceError.new
|
33
|
+
|
34
|
+
@monitor.instrument(
|
35
|
+
'error.occurred',
|
36
|
+
producer_id: id,
|
37
|
+
message: message,
|
38
|
+
error: re_raised,
|
39
|
+
type: 'message.produce_sync'
|
40
|
+
)
|
41
|
+
|
42
|
+
raise re_raised
|
36
43
|
end
|
37
44
|
|
38
45
|
# Produces many messages to Kafka and waits for them to be delivered
|
@@ -48,21 +55,37 @@ module WaterDrop
|
|
48
55
|
# @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
|
49
56
|
# and the message could not be sent to Kafka
|
50
57
|
def produce_many_sync(messages)
|
51
|
-
ensure_active!
|
58
|
+
ensure_active! unless @closing_thread_id && @closing_thread_id == Thread.current.object_id
|
52
59
|
|
53
60
|
messages = middleware.run_many(messages)
|
54
61
|
messages.each { |message| validate_message!(message) }
|
55
62
|
|
63
|
+
dispatched = []
|
64
|
+
|
56
65
|
@monitor.instrument('messages.produced_sync', producer_id: id, messages: messages) do
|
57
|
-
messages
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
66
|
+
messages.each do |message|
|
67
|
+
dispatched << produce(message)
|
68
|
+
end
|
69
|
+
|
70
|
+
dispatched.map! do |handler|
|
71
|
+
wait(handler)
|
72
|
+
end
|
73
|
+
|
74
|
+
dispatched
|
65
75
|
end
|
76
|
+
rescue *SUPPORTED_FLOW_ERRORS
|
77
|
+
re_raised = Errors::ProduceManyError.new(dispatched)
|
78
|
+
|
79
|
+
@monitor.instrument(
|
80
|
+
'error.occurred',
|
81
|
+
producer_id: id,
|
82
|
+
messages: messages,
|
83
|
+
dispatched: dispatched,
|
84
|
+
error: re_raised,
|
85
|
+
type: 'messages.produce_many_sync'
|
86
|
+
)
|
87
|
+
|
88
|
+
raise re_raised
|
66
89
|
end
|
67
90
|
end
|
68
91
|
end
|
data/lib/waterdrop/producer.rb
CHANGED
@@ -8,6 +8,14 @@ module WaterDrop
|
|
8
8
|
include Async
|
9
9
|
include Buffer
|
10
10
|
|
11
|
+
# Which of the inline flow errors do we want to intercept and re-bind
|
12
|
+
SUPPORTED_FLOW_ERRORS = [
|
13
|
+
Rdkafka::RdkafkaError,
|
14
|
+
Rdkafka::Producer::DeliveryHandle::WaitTimeoutError
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
private_constant :SUPPORTED_FLOW_ERRORS
|
18
|
+
|
11
19
|
def_delegators :config, :middleware
|
12
20
|
|
13
21
|
# @return [String] uuid of the current producer
|
@@ -117,6 +125,10 @@ module WaterDrop
|
|
117
125
|
# This should be used only in case a producer was not closed properly and forgotten
|
118
126
|
ObjectSpace.undefine_finalizer(id)
|
119
127
|
|
128
|
+
# We save this thread id because we need to bypass the activity verification on the
|
129
|
+
# producer for final flush of buffers.
|
130
|
+
@closing_thread_id = Thread.current.object_id
|
131
|
+
|
120
132
|
# Flush has its own buffer mutex but even if it is blocked, flushing can still happen
|
121
133
|
# as we close the client after the flushing (even if blocked by the mutex)
|
122
134
|
flush(true)
|
@@ -125,7 +137,7 @@ module WaterDrop
|
|
125
137
|
# It is safe to run it several times but not exactly the same moment
|
126
138
|
# We also mark it as closed only if it was connected, if not, it would trigger a new
|
127
139
|
# connection that anyhow would be immediately closed
|
128
|
-
client.close if @client
|
140
|
+
client.close(@config.max_wait_timeout) if @client
|
129
141
|
|
130
142
|
# Remove callbacks runners that were registered
|
131
143
|
::Karafka::Core::Instrumentation.statistics_callbacks.delete(@id)
|
@@ -155,5 +167,53 @@ module WaterDrop
|
|
155
167
|
def validate_message!(message)
|
156
168
|
@contract.validate!(message, Errors::MessageInvalidError)
|
157
169
|
end
|
170
|
+
|
171
|
+
# Runs the client produce method with a given message
|
172
|
+
#
|
173
|
+
# @param message [Hash] message we want to send
|
174
|
+
def produce(message)
|
175
|
+
client.produce(**message)
|
176
|
+
rescue SUPPORTED_FLOW_ERRORS.first => e
|
177
|
+
# Unless we want to wait and retry and it's a full queue, we raise normally
|
178
|
+
raise unless @config.wait_on_queue_full
|
179
|
+
raise unless e.code == :queue_full
|
180
|
+
|
181
|
+
# We use this syntax here because we want to preserve the original `#cause` when we
|
182
|
+
# instrument the error and there is no way to manually assign `#cause` value. We want to keep
|
183
|
+
# the original cause to maintain the same API across all the errors dispatched to the
|
184
|
+
# notifications pipeline.
|
185
|
+
begin
|
186
|
+
raise Errors::ProduceError
|
187
|
+
rescue Errors::ProduceError => e
|
188
|
+
# We want to instrument on this event even when we restart it.
|
189
|
+
# The reason is simple: instrumentation and visibility.
|
190
|
+
# We can recover from this, but despite that we should be able to instrument this.
|
191
|
+
# If this type of event happens too often, it may indicate that the buffer settings are not
|
192
|
+
# well configured.
|
193
|
+
@monitor.instrument(
|
194
|
+
'error.occurred',
|
195
|
+
producer_id: id,
|
196
|
+
message: message,
|
197
|
+
error: e,
|
198
|
+
type: 'message.produce'
|
199
|
+
)
|
200
|
+
|
201
|
+
# We do not poll the producer because polling happens in a background thread
|
202
|
+
# It also should not be a frequent case (queue full), hence it's ok to just throttle.
|
203
|
+
sleep @config.wait_on_queue_full_timeout
|
204
|
+
end
|
205
|
+
|
206
|
+
retry
|
207
|
+
end
|
208
|
+
|
209
|
+
# Waits on a given handler
|
210
|
+
#
|
211
|
+
# @param handler [Rdkafka::Producer::DeliveryHandle]
|
212
|
+
def wait(handler)
|
213
|
+
handler.wait(
|
214
|
+
max_wait_timeout: @config.max_wait_timeout,
|
215
|
+
wait_timeout: @config.wait_timeout
|
216
|
+
)
|
217
|
+
end
|
158
218
|
end
|
159
219
|
end
|
data/lib/waterdrop/version.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
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.
|
4
|
+
version: 2.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maciej Mensfeld
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
Qf04B9ceLUaC4fPVEz10FyobjaFoY4i32xRto3XnrzeAgfEe4swLq8bQsR3w/EF3
|
36
36
|
MGU0FeSV2Yj7Xc2x/7BzLK8xQn5l7Yy75iPF+KP3vVmDHnNl
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2023-
|
38
|
+
date: 2023-03-09 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: karafka-core
|
@@ -107,8 +107,9 @@ files:
|
|
107
107
|
- lib/waterdrop/instrumentation/monitor.rb
|
108
108
|
- lib/waterdrop/instrumentation/notifications.rb
|
109
109
|
- lib/waterdrop/instrumentation/vendors/datadog/dashboard.json
|
110
|
-
- lib/waterdrop/instrumentation/vendors/datadog/
|
110
|
+
- lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb
|
111
111
|
- lib/waterdrop/middleware.rb
|
112
|
+
- lib/waterdrop/patches/rdkafka/client.rb
|
112
113
|
- lib/waterdrop/patches/rdkafka/metadata.rb
|
113
114
|
- lib/waterdrop/patches/rdkafka/producer.rb
|
114
115
|
- lib/waterdrop/producer.rb
|
metadata.gz.sig
CHANGED
Binary file
|