waterdrop 2.0.7 → 2.6.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) 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 +22 -11
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +200 -0
  7. data/Gemfile +0 -2
  8. data/Gemfile.lock +32 -75
  9. data/README.md +22 -275
  10. data/certs/cert_chain.pem +26 -0
  11. data/config/locales/errors.yml +33 -0
  12. data/docker-compose.yml +19 -12
  13. data/lib/waterdrop/clients/buffered.rb +90 -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/{water_drop → waterdrop}/errors.rb +14 -7
  20. data/lib/waterdrop/instrumentation/callbacks/delivery.rb +102 -0
  21. data/lib/{water_drop → waterdrop}/instrumentation/callbacks/error.rb +6 -2
  22. data/lib/{water_drop → waterdrop}/instrumentation/callbacks/statistics.rb +1 -1
  23. data/lib/{water_drop/instrumentation/stdout_listener.rb → waterdrop/instrumentation/logger_listener.rb} +66 -21
  24. data/lib/waterdrop/instrumentation/monitor.rb +20 -0
  25. data/lib/{water_drop/instrumentation/monitor.rb → waterdrop/instrumentation/notifications.rb} +12 -14
  26. data/lib/waterdrop/instrumentation/vendors/datadog/dashboard.json +1 -0
  27. data/lib/waterdrop/instrumentation/vendors/datadog/metrics_listener.rb +210 -0
  28. data/lib/waterdrop/middleware.rb +50 -0
  29. data/lib/{water_drop → waterdrop}/producer/async.rb +40 -4
  30. data/lib/{water_drop → waterdrop}/producer/buffer.rb +12 -30
  31. data/lib/{water_drop → waterdrop}/producer/builder.rb +6 -11
  32. data/lib/{water_drop → waterdrop}/producer/sync.rb +44 -15
  33. data/lib/waterdrop/producer/transactions.rb +170 -0
  34. data/lib/waterdrop/producer.rb +308 -0
  35. data/lib/{water_drop → waterdrop}/version.rb +1 -1
  36. data/lib/waterdrop.rb +28 -2
  37. data/renovate.json +6 -0
  38. data/waterdrop.gemspec +14 -11
  39. data.tar.gz.sig +0 -0
  40. metadata +71 -111
  41. metadata.gz.sig +0 -0
  42. data/certs/mensfeld.pem +0 -25
  43. data/config/errors.yml +0 -6
  44. data/lib/water_drop/contracts/config.rb +0 -26
  45. data/lib/water_drop/contracts/message.rb +0 -42
  46. data/lib/water_drop/instrumentation/callbacks/delivery.rb +0 -30
  47. data/lib/water_drop/instrumentation/callbacks/statistics_decorator.rb +0 -77
  48. data/lib/water_drop/instrumentation/callbacks_manager.rb +0 -39
  49. data/lib/water_drop/instrumentation.rb +0 -20
  50. data/lib/water_drop/patches/rdkafka/bindings.rb +0 -42
  51. data/lib/water_drop/patches/rdkafka/producer.rb +0 -20
  52. data/lib/water_drop/producer/dummy_client.rb +0 -32
  53. data/lib/water_drop/producer.rb +0 -162
  54. data/lib/water_drop.rb +0 -36
  55. /data/lib/{water_drop → waterdrop}/contracts.rb +0 -0
  56. /data/lib/{water_drop → waterdrop}/producer/status.rb +0 -0
@@ -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
85
  @messages = Concurrent::Array.new
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