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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5190e7c6d460afe6d9a55cb177220e23cd5e89262430f361c3e29ab7df685e6
4
- data.tar.gz: 8cf622a618610bc1bdd283ba2046e0d233d660c0e71dfc70c78ecea2f41db22e
3
+ metadata.gz: 2b66f5a9cb1c6fe80fe594777cb60f9fd20f120c2a897ab439404c825503bb37
4
+ data.tar.gz: 985491a90694c7c729e5c2dd8a581127c96ec26a5eb5eda53d03bc32ab463ee6
5
5
  SHA512:
6
- metadata.gz: 48b3d383c175c392acbdd4a3a41a543192c0ca0e404d755f7fc8eae862350e8d62c9855fb8fea8dc354e358162574f4ff052c850c2ae0fc13a763f951330b97d
7
- data.tar.gz: 7da204b9493122e752933dac8856057c80c4ca640ccd31c8c5787cb9f20fbd8add2ef1851a447ef04010a72e70722d54689978697c4ef1def6ee5c3f2dc23fbe
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.11)
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.0)
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.17.0)
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.6
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 | Description |
96
- |--------------------|-----------------------------------------------------------------|
97
- | `id` | id of the producer for instrumentation and logging |
98
- | `logger` | Logger that we want to use |
99
- | `deliver` | Should we send messages to Kafka or just fake the delivery |
100
- | `max_wait_timeout` | Waits that long for the delivery report or raises an error |
101
- | `wait_timeout` | Waits that long before re-check of delivery report availability |
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/listener'
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::Listener.new do |config|
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}"]
@@ -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
@@ -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 messages flushing something bad happened
33
- class FlushFailureError < BaseError
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
- # @param dispatched_messages [Array<Rdkafka::Producer::DeliveryHandle>] handlers of the
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(dispatched_messages)
41
+ def initialize(dispatched)
39
42
  super()
40
- @dispatched_messages = dispatched_messages
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)
@@ -10,7 +10,7 @@ module WaterDrop
10
10
  # and/or Datadog
11
11
  #
12
12
  # @note You need to setup the `dogstatsd-ruby` client and assign it
13
- class Listener
13
+ class MetricsListener
14
14
  include ::Karafka::Core::Configurable
15
15
  extend Forwardable
16
16
 
@@ -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
- ) { client.produce(**message) }
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.map { |message| client.produce(**message) }
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::FlushFailureError] when there was a failure in flushing
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
- dispatched = data_for_dispatch.map { |message| client.produce(**message) }
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
- raise Errors::FlushFailureError.new(dispatched)
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
- client
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
- .map { |message| client.produce(**message) }
59
- .map! do |handler|
60
- handler.wait(
61
- max_wait_timeout: @config.max_wait_timeout,
62
- wait_timeout: @config.wait_timeout
63
- )
64
- end
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
@@ -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
@@ -3,5 +3,5 @@
3
3
  # WaterDrop library
4
4
  module WaterDrop
5
5
  # Current WaterDrop version
6
- VERSION = '2.4.11'
6
+ VERSION = '2.5.1'
7
7
  end
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.11
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-02-24 00:00:00.000000000 Z
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/listener.rb
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