waterdrop 2.0.7 → 2.6.14

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/workflows/ci.yml +39 -13
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +212 -0
  7. data/Gemfile +0 -2
  8. data/Gemfile.lock +45 -75
  9. data/README.md +22 -275
  10. data/certs/cert_chain.pem +26 -0
  11. data/config/locales/errors.yml +39 -0
  12. data/docker-compose.yml +21 -12
  13. data/lib/waterdrop/clients/buffered.rb +95 -0
  14. data/lib/waterdrop/clients/dummy.rb +69 -0
  15. data/lib/waterdrop/clients/rdkafka.rb +34 -0
  16. data/lib/{water_drop → waterdrop}/config.rb +39 -16
  17. data/lib/waterdrop/contracts/config.rb +43 -0
  18. data/lib/waterdrop/contracts/message.rb +64 -0
  19. data/lib/waterdrop/contracts/transactional_offset.rb +21 -0
  20. data/lib/{water_drop → waterdrop}/errors.rb +23 -7
  21. data/lib/waterdrop/helpers/counter.rb +27 -0
  22. data/lib/waterdrop/instrumentation/callbacks/delivery.rb +106 -0
  23. data/lib/{water_drop → waterdrop}/instrumentation/callbacks/error.rb +6 -2
  24. data/lib/{water_drop → waterdrop}/instrumentation/callbacks/statistics.rb +1 -1
  25. data/lib/{water_drop/instrumentation/stdout_listener.rb → waterdrop/instrumentation/logger_listener.rb} +91 -21
  26. data/lib/waterdrop/instrumentation/monitor.rb +20 -0
  27. data/lib/{water_drop/instrumentation/monitor.rb → waterdrop/instrumentation/notifications.rb} +15 -14
  28. data/lib/waterdrop/instrumentation/vendors/datadog/dashboard.json +1 -0
  29. data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +210 -0
  30. data/lib/waterdrop/middleware.rb +50 -0
  31. data/lib/{water_drop → waterdrop}/producer/async.rb +40 -4
  32. data/lib/{water_drop → waterdrop}/producer/buffer.rb +13 -31
  33. data/lib/{water_drop → waterdrop}/producer/builder.rb +6 -11
  34. data/lib/{water_drop → waterdrop}/producer/sync.rb +44 -15
  35. data/lib/waterdrop/producer/transactions.rb +219 -0
  36. data/lib/waterdrop/producer.rb +324 -0
  37. data/lib/{water_drop → waterdrop}/version.rb +1 -1
  38. data/lib/waterdrop.rb +27 -2
  39. data/renovate.json +6 -0
  40. data/waterdrop.gemspec +14 -11
  41. data.tar.gz.sig +0 -0
  42. metadata +73 -111
  43. metadata.gz.sig +0 -0
  44. data/certs/mensfeld.pem +0 -25
  45. data/config/errors.yml +0 -6
  46. data/lib/water_drop/contracts/config.rb +0 -26
  47. data/lib/water_drop/contracts/message.rb +0 -42
  48. data/lib/water_drop/instrumentation/callbacks/delivery.rb +0 -30
  49. data/lib/water_drop/instrumentation/callbacks/statistics_decorator.rb +0 -77
  50. data/lib/water_drop/instrumentation/callbacks_manager.rb +0 -39
  51. data/lib/water_drop/instrumentation.rb +0 -20
  52. data/lib/water_drop/patches/rdkafka/bindings.rb +0 -42
  53. data/lib/water_drop/patches/rdkafka/producer.rb +0 -20
  54. data/lib/water_drop/producer/dummy_client.rb +0 -32
  55. data/lib/water_drop/producer.rb +0 -162
  56. data/lib/water_drop.rb +0 -36
  57. /data/lib/{water_drop → waterdrop}/contracts.rb +0 -0
  58. /data/lib/{water_drop → waterdrop}/producer/status.rb +0 -0
@@ -2,43 +2,44 @@
2
2
 
3
3
  module WaterDrop
4
4
  module Instrumentation
5
- # Monitor is used to hookup external monitoring services to monitor how WaterDrop works
6
- # Since it is a pub-sub based on dry-monitor, you can use as many subscribers/loggers at the
7
- # same time, which means that you might have for example file logging and NewRelic at the same
8
- # time
9
- # @note This class acts as a singleton because we are only permitted to have single monitor
10
- # per running process (just as logger)
11
- class Monitor < Dry::Monitor::Notifications
5
+ # Instrumented is used to hookup external monitoring services to monitor how WaterDrop works
6
+ class Notifications < ::Karafka::Core::Monitoring::Notifications
12
7
  # List of events that we support in the system and to which a monitor client can hook up
13
8
  # @note The non-error once support timestamp benchmarking
14
9
  EVENTS = %w[
10
+ producer.connected
11
+ producer.closing
15
12
  producer.closed
16
13
 
17
14
  message.produced_async
18
15
  message.produced_sync
19
16
  message.acknowledged
17
+ message.purged
20
18
  message.buffered
21
19
 
22
20
  messages.produced_async
23
21
  messages.produced_sync
24
22
  messages.buffered
25
23
 
24
+ transaction.started
25
+ transaction.committed
26
+ transaction.aborted
27
+ transaction.marked_as_consumed
28
+ transaction.finished
29
+
26
30
  buffer.flushed_async
27
- buffer.flushed_async.error
28
31
  buffer.flushed_sync
29
- buffer.flushed_sync.error
32
+ buffer.purged
30
33
 
31
34
  statistics.emitted
32
35
 
33
- error.emitted
36
+ error.occurred
34
37
  ].freeze
35
38
 
36
- private_constant :EVENTS
37
-
38
39
  # @return [WaterDrop::Instrumentation::Monitor] monitor instance for system instrumentation
39
40
  def initialize
40
- super(:waterdrop)
41
- EVENTS.each(&method(:register_event))
41
+ super
42
+ EVENTS.each { |event| register_event(event) }
42
43
  end
43
44
  end
44
45
  end
@@ -0,0 +1 @@
1
+ {"title":"WaterDrop producer example dashboard","description":"This dashboard include example setup for monitoring activity of your WaterDrop producer","widgets":[{"id":243951318,"definition":{"title":"Messages produced","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"alias":"produced sync","formula":"query1"},{"alias":"produced async","formula":"query2"},{"alias":"flushed sync","formula":"query3"},{"alias":"flushed async","formula":"query4"},{"alias":"acknowledged","formula":"query5"}],"response_format":"timeseries","queries":[{"query":"sum:waterdrop.produced_sync{*}.as_count()","data_source":"metrics","name":"query1"},{"query":"sum:waterdrop.produced_async{*}.as_count()","data_source":"metrics","name":"query2"},{"query":"sum:waterdrop.flushed_sync{*}.as_count()","data_source":"metrics","name":"query3"},{"query":"sum:waterdrop.flushed_async{*}.as_count()","data_source":"metrics","name":"query4"},{"query":"sum:waterdrop.acknowledged{*}.as_count()","data_source":"metrics","name":"query5"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}],"yaxis":{"include_zero":true,"scale":"linear","label":"","min":"auto","max":"auto"}}},{"id":1979626566852990,"definition":{"title":"Messages buffer size","title_size":"16","title_align":"left","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"alias":"max","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.buffer.size.max{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}]}},{"id":243951221,"definition":{"title":"Kafka broker API calls","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"alias":"API calls","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"sum:waterdrop.calls{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}],"yaxis":{"include_zero":true,"scale":"linear","label":"","min":"auto","max":"auto"}}},{"id":243951952,"definition":{"title":"Producer queue size","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"alias":"Queue size average","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"max:waterdrop.queue.size.avg{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"},{"formulas":[{"alias":"Queue size max","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"max:waterdrop.queue.size.max{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}],"yaxis":{"include_zero":true,"scale":"linear","label":"","min":"auto","max":"auto"}}},{"id":243951263,"definition":{"title":"Producer queue latency","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"alias":"Average latency","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.queue.latency.avg{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"},{"formulas":[{"alias":"Latency p95","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.queue.latency.p95{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"},{"formulas":[{"alias":"Latency p99","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.queue.latency.p99{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}],"yaxis":{"include_zero":true,"scale":"linear","label":"","min":"auto","max":"auto"}}},{"id":243951276,"definition":{"title":"Producer network latency","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"alias":"Average latency","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.request_size.avg{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"},{"formulas":[{"alias":"Latency p95","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.network.latency.p95{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"},{"formulas":[{"alias":"Latency p99","formula":"query1"}],"response_format":"timeseries","queries":[{"query":"avg:waterdrop.network.latency.p99{*}","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}],"yaxis":{"include_zero":true,"scale":"linear","label":"","min":"auto","max":"auto"}}},{"id":243954928,"definition":{"title":"Producer errors","show_legend":true,"legend_layout":"auto","legend_columns":["avg","min","max","value","sum"],"type":"timeseries","requests":[{"formulas":[{"formula":"query1"}],"response_format":"timeseries","queries":[{"query":"sum:waterdrop.error_occurred{*}.as_count()","data_source":"metrics","name":"query1"}],"style":{"palette":"dog_classic","line_type":"solid","line_width":"normal"},"display_type":"line"}],"yaxis":{"include_zero":true,"scale":"linear","label":"","min":"auto","max":"auto"}}}],"template_variables":[],"layout_type":"ordered","is_read_only":false,"notify_list":[],"reflow_type":"auto","id":"rnr-kgh-dna"}
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Instrumentation
5
+ # Namespace for vendor specific instrumentation
6
+ module Vendors
7
+ # Datadog specific instrumentation
8
+ module Datadog
9
+ # Listener that can be used to subscribe to WaterDrop producer to receive stats via StatsD
10
+ # and/or Datadog
11
+ #
12
+ # @note You need to setup the `dogstatsd-ruby` client and assign it
13
+ class MetricsListener
14
+ include ::Karafka::Core::Configurable
15
+ extend Forwardable
16
+
17
+ def_delegators :config, :client, :rd_kafka_metrics, :namespace, :default_tags
18
+
19
+ # Value object for storing a single rdkafka metric publishing details
20
+ RdKafkaMetric = Struct.new(:type, :scope, :name, :key_location)
21
+
22
+ # Namespace under which the DD metrics should be published
23
+ setting :namespace, default: 'waterdrop'
24
+
25
+ # Datadog client that we should use to publish the metrics
26
+ setting :client
27
+
28
+ # Default tags we want to publish (for example hostname)
29
+ # Format as followed (example for hostname): `["host:#{Socket.gethostname}"]`
30
+ setting :default_tags, default: []
31
+
32
+ # All the rdkafka metrics we want to publish
33
+ #
34
+ # By default we publish quite a lot so this can be tuned
35
+ # Note, that the once with `_d` come from WaterDrop, not rdkafka or Kafka
36
+ setting :rd_kafka_metrics, default: [
37
+ # Client metrics
38
+ RdKafkaMetric.new(:count, :root, 'calls', 'tx_d'),
39
+ RdKafkaMetric.new(:histogram, :root, 'queue.size', 'msg_cnt_d'),
40
+
41
+ # Broker metrics
42
+ RdKafkaMetric.new(:count, :brokers, 'deliver.attempts', 'txretries_d'),
43
+ RdKafkaMetric.new(:count, :brokers, 'deliver.errors', 'txerrs_d'),
44
+ RdKafkaMetric.new(:count, :brokers, 'receive.errors', 'rxerrs_d'),
45
+ RdKafkaMetric.new(:gauge, :brokers, 'queue.latency.avg', %w[outbuf_latency avg]),
46
+ RdKafkaMetric.new(:gauge, :brokers, 'queue.latency.p95', %w[outbuf_latency p95]),
47
+ RdKafkaMetric.new(:gauge, :brokers, 'queue.latency.p99', %w[outbuf_latency p99]),
48
+ RdKafkaMetric.new(:gauge, :brokers, 'network.latency.avg', %w[rtt avg]),
49
+ RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p95', %w[rtt p95]),
50
+ RdKafkaMetric.new(:gauge, :brokers, 'network.latency.p99', %w[rtt p99])
51
+ ].freeze
52
+
53
+ configure
54
+
55
+ # @param block [Proc] configuration block
56
+ def initialize(&block)
57
+ configure
58
+ setup(&block) if block
59
+ end
60
+
61
+ # @param block [Proc] configuration block
62
+ # @note We define this alias to be consistent with `WaterDrop#setup`
63
+ def setup(&block)
64
+ configure(&block)
65
+ end
66
+
67
+ # Hooks up to WaterDrop instrumentation for emitted statistics
68
+ #
69
+ # @param event [Karafka::Core::Monitoring::Event]
70
+ def on_statistics_emitted(event)
71
+ statistics = event[:statistics]
72
+
73
+ rd_kafka_metrics.each do |metric|
74
+ report_metric(metric, statistics)
75
+ end
76
+ end
77
+
78
+ # Increases the errors count by 1
79
+ #
80
+ # @param _event [Karafka::Core::Monitoring::Event]
81
+ def on_error_occurred(_event)
82
+ count('error_occurred', 1, tags: default_tags)
83
+ end
84
+
85
+ # Increases acknowledged messages counter
86
+ # @param _event [Karafka::Core::Monitoring::Event]
87
+ def on_message_acknowledged(_event)
88
+ increment('acknowledged', tags: default_tags)
89
+ end
90
+
91
+ %i[
92
+ produced_sync
93
+ produced_async
94
+ ].each do |event_scope|
95
+ class_eval <<~METHODS, __FILE__, __LINE__ + 1
96
+ # @param event [Karafka::Core::Monitoring::Event]
97
+ def on_message_#{event_scope}(event)
98
+ report_message(event[:message][:topic], :#{event_scope})
99
+ end
100
+
101
+ # @param event [Karafka::Core::Monitoring::Event]
102
+ def on_messages_#{event_scope}(event)
103
+ event[:messages].each do |message|
104
+ report_message(message[:topic], :#{event_scope})
105
+ end
106
+ end
107
+ METHODS
108
+ end
109
+
110
+ # Reports the buffer usage when anything is added to the buffer
111
+ %i[
112
+ message_buffered
113
+ messages_buffered
114
+ ].each do |event_scope|
115
+ class_eval <<~METHODS, __FILE__, __LINE__ + 1
116
+ # @param event [Karafka::Core::Monitoring::Event]
117
+ def on_#{event_scope}(event)
118
+ histogram(
119
+ 'buffer.size',
120
+ event[:buffer].size,
121
+ tags: default_tags
122
+ )
123
+ end
124
+ METHODS
125
+ end
126
+
127
+ # Events that support many messages only
128
+ # Reports data flushing operation (production from the buffer)
129
+ %i[
130
+ flushed_sync
131
+ flushed_async
132
+ ].each do |event_scope|
133
+ class_eval <<~METHODS, __FILE__, __LINE__ + 1
134
+ # @param event [Karafka::Core::Monitoring::Event]
135
+ def on_buffer_#{event_scope}(event)
136
+ event[:messages].each do |message|
137
+ report_message(message[:topic], :#{event_scope})
138
+ end
139
+ end
140
+ METHODS
141
+ end
142
+
143
+ private
144
+
145
+ %i[
146
+ count
147
+ gauge
148
+ histogram
149
+ increment
150
+ decrement
151
+ ].each do |metric_type|
152
+ class_eval <<~METHODS, __FILE__, __LINE__ + 1
153
+ def #{metric_type}(key, *args)
154
+ client.#{metric_type}(
155
+ namespaced_metric(key),
156
+ *args
157
+ )
158
+ end
159
+ METHODS
160
+ end
161
+
162
+ # Report that a message has been produced to a topic.
163
+ # @param topic [String] Kafka topic
164
+ # @param method_name [Symbol] method from which this message operation comes
165
+ def report_message(topic, method_name)
166
+ increment(method_name, tags: default_tags + ["topic:#{topic}"])
167
+ end
168
+
169
+ # Wraps metric name in listener's namespace
170
+ # @param metric_name [String] RdKafkaMetric name
171
+ # @return [String]
172
+ def namespaced_metric(metric_name)
173
+ "#{namespace}.#{metric_name}"
174
+ end
175
+
176
+ # Reports a given metric statistics to Datadog
177
+ # @param metric [RdKafkaMetric] metric value object
178
+ # @param statistics [Hash] hash with all the statistics emitted
179
+ def report_metric(metric, statistics)
180
+ case metric.scope
181
+ when :root
182
+ public_send(
183
+ metric.type,
184
+ metric.name,
185
+ statistics.fetch(*metric.key_location),
186
+ tags: default_tags
187
+ )
188
+ when :brokers
189
+ statistics.fetch('brokers').each_value do |broker_statistics|
190
+ # Skip bootstrap nodes
191
+ # Bootstrap nodes have nodeid -1, other nodes have positive
192
+ # node ids
193
+ next if broker_statistics['nodeid'] == -1
194
+
195
+ public_send(
196
+ metric.type,
197
+ metric.name,
198
+ broker_statistics.dig(*metric.key_location),
199
+ tags: default_tags + ["broker:#{broker_statistics['nodename']}"]
200
+ )
201
+ end
202
+ else
203
+ raise ArgumentError, metric.scope
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ # Simple middleware layer for manipulating messages prior to their validation
5
+ class Middleware
6
+ def initialize
7
+ @mutex = Mutex.new
8
+ @steps = []
9
+ end
10
+
11
+ # Runs middleware on a single message prior to validation
12
+ #
13
+ # @param message [Hash] message hash
14
+ # @return [Hash] message hash. Either the same if transformed in place, or a copy if modified
15
+ # into a new object.
16
+ # @note You need to decide yourself whether you don't use the message hash data anywhere else
17
+ # and you want to save on memory by modifying it in place or do you want to do a deep copy
18
+ def run(message)
19
+ @steps.each do |step|
20
+ message = step.call(message)
21
+ end
22
+
23
+ message
24
+ end
25
+
26
+ # @param messages [Array<Hash>] messages on which we want to run middlewares
27
+ # @return [Array<Hash>] transformed messages
28
+ def run_many(messages)
29
+ messages.map do |message|
30
+ run(message)
31
+ end
32
+ end
33
+
34
+ # Register given middleware as the first one in the chain
35
+ # @param step [#call] step that needs to return the message
36
+ def prepend(step)
37
+ @mutex.synchronize do
38
+ @steps.prepend step
39
+ end
40
+ end
41
+
42
+ # Register given middleware as the last one in the chain
43
+ # @param step [#call] step that needs to return the message
44
+ def append(step)
45
+ @mutex.synchronize do
46
+ @steps.append step
47
+ end
48
+ end
49
+ end
50
+ end
@@ -14,14 +14,30 @@ module WaterDrop
14
14
  # @raise [Errors::MessageInvalidError] When provided message details are invalid and the
15
15
  # message could not be sent to Kafka
16
16
  def produce_async(message)
17
- ensure_active!
17
+ message = middleware.run(message)
18
18
  validate_message!(message)
19
19
 
20
20
  @monitor.instrument(
21
21
  'message.produced_async',
22
22
  producer_id: id,
23
23
  message: message
24
- ) { client.produce(**message) }
24
+ ) { produce(message) }
25
+ rescue *SUPPORTED_FLOW_ERRORS => e
26
+ # We use this syntax here because we want to preserve the original `#cause` when we
27
+ # instrument the error and there is no way to manually assign `#cause` value
28
+ begin
29
+ raise Errors::ProduceError, e.inspect
30
+ rescue Errors::ProduceError => ex
31
+ @monitor.instrument(
32
+ 'error.occurred',
33
+ producer_id: id,
34
+ message: message,
35
+ error: ex,
36
+ type: 'message.produce_async'
37
+ )
38
+
39
+ raise ex
40
+ end
25
41
  end
26
42
 
27
43
  # Produces many messages to Kafka and does not wait for them to be delivered
@@ -35,7 +51,8 @@ module WaterDrop
35
51
  # @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
36
52
  # and the message could not be sent to Kafka
37
53
  def produce_many_async(messages)
38
- ensure_active!
54
+ dispatched = []
55
+ messages = middleware.run_many(messages)
39
56
  messages.each { |message| validate_message!(message) }
40
57
 
41
58
  @monitor.instrument(
@@ -43,8 +60,27 @@ module WaterDrop
43
60
  producer_id: id,
44
61
  messages: messages
45
62
  ) do
46
- messages.map { |message| client.produce(**message) }
63
+ with_transaction_if_transactional do
64
+ messages.each do |message|
65
+ dispatched << produce(message)
66
+ end
67
+ end
68
+
69
+ dispatched
47
70
  end
71
+ rescue *SUPPORTED_FLOW_ERRORS => e
72
+ re_raised = Errors::ProduceManyError.new(dispatched, e.inspect)
73
+
74
+ @monitor.instrument(
75
+ 'error.occurred',
76
+ producer_id: id,
77
+ messages: messages,
78
+ dispatched: dispatched,
79
+ error: re_raised,
80
+ type: 'messages.produce_many_async'
81
+ )
82
+
83
+ raise re_raised
48
84
  end
49
85
  end
50
86
  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
@@ -19,12 +11,15 @@ module WaterDrop
19
11
  # message could not be sent to Kafka
20
12
  def buffer(message)
21
13
  ensure_active!
14
+
15
+ message = middleware.run(message)
22
16
  validate_message!(message)
23
17
 
24
18
  @monitor.instrument(
25
19
  'message.buffered',
26
20
  producer_id: id,
27
- message: message
21
+ message: message,
22
+ buffer: @messages
28
23
  ) { @messages << message }
29
24
  end
30
25
 
@@ -36,12 +31,15 @@ module WaterDrop
36
31
  # and the message could not be sent to Kafka
37
32
  def buffer_many(messages)
38
33
  ensure_active!
34
+
35
+ messages = middleware.run_many(messages)
39
36
  messages.each { |message| validate_message!(message) }
40
37
 
41
38
  @monitor.instrument(
42
39
  'messages.buffered',
43
40
  producer_id: id,
44
- messages: messages
41
+ messages: messages,
42
+ buffer: @messages
45
43
  ) do
46
44
  messages.each { |message| @messages << message }
47
45
  messages
@@ -52,8 +50,6 @@ module WaterDrop
52
50
  # @return [Array<Rdkafka::Producer::DeliveryHandle>] delivery handles for messages that were
53
51
  # flushed
54
52
  def flush_async
55
- ensure_active!
56
-
57
53
  @monitor.instrument(
58
54
  'buffer.flushed_async',
59
55
  producer_id: id,
@@ -65,8 +61,6 @@ module WaterDrop
65
61
  # @return [Array<Rdkafka::Producer::DeliveryReport>] delivery reports for messages that were
66
62
  # flushed
67
63
  def flush_sync
68
- ensure_active!
69
-
70
64
  @monitor.instrument(
71
65
  'buffer.flushed_sync',
72
66
  producer_id: id,
@@ -80,33 +74,21 @@ module WaterDrop
80
74
  # @param sync [Boolean] should it flush in a sync way
81
75
  # @return [Array<Rdkafka::Producer::DeliveryHandle, Rdkafka::Producer::DeliveryReport>]
82
76
  # delivery handles for async or delivery reports for sync
83
- # @raise [Errors::FlushFailureError] when there was a failure in flushing
77
+ # @raise [Errors::ProduceManyError] when there was a failure in flushing
84
78
  # @note We use this method underneath to provide a different instrumentation for sync and
85
79
  # async flushing within the public API
86
80
  def flush(sync)
87
81
  data_for_dispatch = nil
88
- dispatched = []
89
82
 
90
83
  @buffer_mutex.synchronize do
91
84
  data_for_dispatch = @messages
92
- @messages = Concurrent::Array.new
85
+ @messages = []
93
86
  end
94
87
 
95
- dispatched = data_for_dispatch.map { |message| client.produce(**message) }
96
-
97
- return dispatched unless sync
98
-
99
- dispatched.map do |handler|
100
- handler.wait(
101
- max_wait_timeout: @config.max_wait_timeout,
102
- wait_timeout: @config.wait_timeout
103
- )
104
- end
105
- rescue *RESCUED_ERRORS => e
106
- key = sync ? 'buffer.flushed_sync.error' : 'buffer.flush_async.error'
107
- @monitor.instrument(key, producer_id: id, error: e, dispatched: dispatched)
88
+ # Do nothing if nothing to flush
89
+ return data_for_dispatch if data_for_dispatch.empty?
108
90
 
109
- raise Errors::FlushFailureError.new(dispatched)
91
+ sync ? produce_many_sync(data_for_dispatch) : produce_many_async(data_for_dispatch)
110
92
  end
111
93
  end
112
94
  end
@@ -10,18 +10,13 @@ module WaterDrop
10
10
  # @return [Rdkafka::Producer, Producer::DummyClient] raw rdkafka producer or a dummy producer
11
11
  # when we don't want to dispatch any messages
12
12
  def call(producer, config)
13
- return DummyClient.new unless config.deliver
13
+ klass = config.client_class
14
+ # This allows us to have backwards compatibility.
15
+ # If it is the default client and delivery is set to false, we use dummy as we used to
16
+ # before `client_class` was introduced
17
+ klass = Clients::Dummy if klass == Clients::Rdkafka && !config.deliver
14
18
 
15
- client = Rdkafka::Config.new(config.kafka.to_h).producer
16
-
17
- # This callback is not global and is per client, thus we do not have to wrap it with a
18
- # callbacks manager to make it work
19
- client.delivery_callback = Instrumentation::Callbacks::Delivery.new(
20
- producer.id,
21
- config.monitor
22
- )
23
-
24
- client
19
+ klass.new(producer)
25
20
  end
26
21
  end
27
22
  end
@@ -16,7 +16,7 @@ module WaterDrop
16
16
  # @raise [Errors::MessageInvalidError] When provided message details are invalid and the
17
17
  # message could not be sent to Kafka
18
18
  def produce_sync(message)
19
- ensure_active!
19
+ message = middleware.run(message)
20
20
  validate_message!(message)
21
21
 
22
22
  @monitor.instrument(
@@ -24,12 +24,23 @@ module WaterDrop
24
24
  producer_id: id,
25
25
  message: message
26
26
  ) do
27
- client
28
- .produce(**message)
29
- .wait(
30
- max_wait_timeout: @config.max_wait_timeout,
31
- wait_timeout: @config.wait_timeout
32
- )
27
+ wait(produce(message))
28
+ end
29
+ rescue *SUPPORTED_FLOW_ERRORS => e
30
+ # We use this syntax here because we want to preserve the original `#cause` when we
31
+ # instrument the error and there is no way to manually assign `#cause` value
32
+ begin
33
+ raise Errors::ProduceError, e.inspect
34
+ rescue Errors::ProduceError => ex
35
+ @monitor.instrument(
36
+ 'error.occurred',
37
+ producer_id: id,
38
+ message: message,
39
+ error: ex,
40
+ type: 'message.produce_sync'
41
+ )
42
+
43
+ raise ex
33
44
  end
34
45
  end
35
46
 
@@ -46,19 +57,37 @@ module WaterDrop
46
57
  # @raise [Errors::MessageInvalidError] When any of the provided messages details are invalid
47
58
  # and the message could not be sent to Kafka
48
59
  def produce_many_sync(messages)
49
- ensure_active!
60
+ messages = middleware.run_many(messages)
50
61
  messages.each { |message| validate_message!(message) }
51
62
 
63
+ dispatched = []
64
+
52
65
  @monitor.instrument('messages.produced_sync', producer_id: id, messages: messages) do
53
- messages
54
- .map { |message| client.produce(**message) }
55
- .map! do |handler|
56
- handler.wait(
57
- max_wait_timeout: @config.max_wait_timeout,
58
- wait_timeout: @config.wait_timeout
59
- )
66
+ with_transaction_if_transactional do
67
+ messages.each do |message|
68
+ dispatched << produce(message)
60
69
  end
70
+ end
71
+
72
+ dispatched.map! do |handler|
73
+ wait(handler)
74
+ end
75
+
76
+ dispatched
61
77
  end
78
+ rescue *SUPPORTED_FLOW_ERRORS => e
79
+ re_raised = Errors::ProduceManyError.new(dispatched, e.inspect)
80
+
81
+ @monitor.instrument(
82
+ 'error.occurred',
83
+ producer_id: id,
84
+ messages: messages,
85
+ dispatched: dispatched,
86
+ error: re_raised,
87
+ type: 'messages.produce_many_sync'
88
+ )
89
+
90
+ raise re_raised
62
91
  end
63
92
  end
64
93
  end