waterdrop 2.7.0 → 2.7.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: 5308262b20199b02906783387f294a58beb01fa8850db3db19bb7be39395121a
4
- data.tar.gz: d35c18c4b7352c20c8eeb623f54581476f108cb656912a571ef067cc796e884c
3
+ metadata.gz: 38010b0c3b164daabe0d7ea1ea9a20f20335cffc37586611d4c16b6524c2f28c
4
+ data.tar.gz: c0e2aed3b6d5e645f5b0c7f89e446aed37c91f54dfc6c3ff3b084ff4428ac531
5
5
  SHA512:
6
- metadata.gz: ac6693e44080e4edf9b201a5e735b283bb7fa81d36ae10bf0d7501faa00e5f099917144966beb7febc4b95c50ed78feb7c659e59753a78e4495111e3d00af322
7
- data.tar.gz: 100439b79cc59bd668f40e4fed8086c49f13bfedebb68409981d8c70a39c692eb2b4f453d9a45c367f2612578b2c6ac8303bd56acdbbd27d3a02d5aab803d57a
6
+ metadata.gz: b5a16ad7cf9dd8b2bda80e4e30ee74c9041b9beb6da90832d9f18b3bb3a1d8a6b7c7bfe82c6e56a314b625f034ca3847c0b99ea91e94ee80704cd4b50785e91f
7
+ data.tar.gz: b56658d71bf762d3bcdf22d152c3a84ced81c32a0a3d2b2dfd30c3f9177d655d93ba3fdac613684f4f1310ac361b37a677b1d4f6aa47c0468db078cb4e094111
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # WaterDrop changelog
2
2
 
3
+ ## 2.7.1 (2024-05-09)
4
+ - **[Feature]** Support context-base configuration with low-level topic settings alterations producer variants.
5
+ - [Enhancement] Prefix random default `SecureRandom.hex(6)` producers ids with `waterdrop-hex` to indicate type of object.
6
+
3
7
  ## 2.7.0 (2024-04-26)
4
8
 
5
9
  This release contains **BREAKING** changes. Make sure to read and apply upgrade notes.
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- waterdrop (2.7.0)
4
+ waterdrop (2.7.1)
5
5
  karafka-core (>= 2.4.0, < 3.0.0)
6
+ karafka-rdkafka (>= 0.15.1)
6
7
  zeitwerk (~> 2.3)
7
8
 
8
9
  GEM
@@ -19,7 +20,7 @@ GEM
19
20
  mutex_m
20
21
  tzinfo (~> 2.0)
21
22
  base64 (0.2.0)
22
- bigdecimal (3.1.7)
23
+ bigdecimal (3.1.8)
23
24
  byebug (11.1.3)
24
25
  concurrent-ruby (1.2.3)
25
26
  connection_pool (2.4.1)
@@ -29,11 +30,11 @@ GEM
29
30
  factory_bot (6.4.6)
30
31
  activesupport (>= 5.0.0)
31
32
  ffi (1.16.3)
32
- i18n (1.14.4)
33
+ i18n (1.14.5)
33
34
  concurrent-ruby (~> 1.0)
34
35
  karafka-core (2.4.0)
35
36
  karafka-rdkafka (>= 0.15.0, < 0.16.0)
36
- karafka-rdkafka (0.15.0)
37
+ karafka-rdkafka (0.15.1)
37
38
  ffi (~> 1.15)
38
39
  mini_portile2 (~> 2.6)
39
40
  rake (> 12)
@@ -50,7 +51,7 @@ GEM
50
51
  rspec-expectations (3.13.0)
51
52
  diff-lcs (>= 1.2.0, < 2.0)
52
53
  rspec-support (~> 3.13.0)
53
- rspec-mocks (3.13.0)
54
+ rspec-mocks (3.13.1)
54
55
  diff-lcs (>= 1.2.0, < 2.0)
55
56
  rspec-support (~> 3.13.0)
56
57
  rspec-support (3.13.1)
@@ -65,7 +66,7 @@ GEM
65
66
  zeitwerk (2.6.13)
66
67
 
67
68
  PLATFORMS
68
- arm64-darwin-22
69
+ ruby
69
70
  x86_64-linux
70
71
 
71
72
  DEPENDENCIES
data/README.md CHANGED
@@ -9,13 +9,13 @@ WaterDrop is a standalone gem that sends messages to Kafka easily with an extra
9
9
  It:
10
10
 
11
11
  - Is thread-safe
12
- - Supports sync producing
13
- - Supports async producing
12
+ - Supports sync and async producing
14
13
  - Supports transactions
15
14
  - Supports buffering
16
- - Supports producing messages to multiple clusters
15
+ - Supports producing to multiple clusters
17
16
  - Supports multiple delivery policies
18
- - Works with Kafka `1.0+` and Ruby `2.7+`
17
+ - Supports per-topic configuration alterations (variants)
18
+ - Works with Kafka `1.0+` and Ruby `3.0+`
19
19
  - Works with and without Karafka
20
20
 
21
21
  ## Documentation
@@ -19,6 +19,14 @@ en:
19
19
  max_attempts_on_transaction_command_format: must be an integer that is equal or bigger than 1
20
20
  oauth.token_provider_listener_format: 'must be false or respond to #on_oauthbearer_token_refresh'
21
21
 
22
+ variant:
23
+ missing: must be present
24
+ default_format: must be boolean
25
+ max_wait_timeout_format: must be an integer that is equal or bigger than 0
26
+ kafka_key_must_be_a_symbol: All keys under the kafka settings scope need to be symbols
27
+ kafka_key_not_per_topic: This config option cannot be set on a per topic basis
28
+ kafka_key_acks_not_changeable: Acks value cannot be redefined for a transactional producer
29
+
22
30
  message:
23
31
  missing: must be present
24
32
  partition_format: must be an integer greater or equal to -1
@@ -30,7 +30,7 @@ module WaterDrop
30
30
  setting(
31
31
  :id,
32
32
  default: false,
33
- constructor: ->(id) { id || SecureRandom.hex(6) }
33
+ constructor: ->(id) { id || "waterdrop-#{SecureRandom.hex(6)}" }
34
34
  )
35
35
  # option [Instance] logger that we want to use
36
36
  # @note Due to how rdkafka works, this setting is global for all the producers
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ module Contracts
5
+ # Variant validator to ensure basic sanity of the variant alteration data
6
+ class Variant < ::Karafka::Core::Contractable::Contract
7
+ # Taken from librdkafka config
8
+ # Those values can be changed on a per topic basis. We do not support experimental or
9
+ # deprecated values. We also do not support settings that would break rdkafka-ruby
10
+ #
11
+ # @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties
12
+ TOPIC_CONFIG_KEYS = %i[
13
+ acks
14
+ compression.codec
15
+ compression.level
16
+ compression.type
17
+ delivery.timeout.ms
18
+ message.timeout.ms
19
+ partitioner
20
+ request.required.acks
21
+ request.timeout.ms
22
+ ].freeze
23
+
24
+ # Boolean values
25
+ BOOLEANS = [true, false].freeze
26
+
27
+ private_constant :TOPIC_CONFIG_KEYS, :BOOLEANS
28
+
29
+ configure do |config|
30
+ config.error_messages = YAML.safe_load(
31
+ File.read(
32
+ File.join(WaterDrop.gem_root, 'config', 'locales', 'errors.yml')
33
+ )
34
+ ).fetch('en').fetch('validations').fetch('variant')
35
+ end
36
+
37
+ required(:default) { |val| BOOLEANS.include?(val) }
38
+ required(:max_wait_timeout) { |val| val.is_a?(Numeric) && val >= 0 }
39
+
40
+ # Checks if all keys are symbols
41
+ virtual do |config, errors|
42
+ next true unless errors.empty?
43
+
44
+ errors = []
45
+
46
+ config
47
+ .fetch(:topic_config)
48
+ .keys
49
+ .reject { |key| key.is_a?(Symbol) }
50
+ .each { |key| errors << [[:kafka, key], :kafka_key_must_be_a_symbol] }
51
+
52
+ errors
53
+ end
54
+
55
+ # Checks if we have any keys that are not allowed
56
+ virtual do |config, errors|
57
+ next true unless errors.empty?
58
+
59
+ errors = []
60
+
61
+ config
62
+ .fetch(:topic_config)
63
+ .keys
64
+ .reject { |key| TOPIC_CONFIG_KEYS.include?(key) }
65
+ .each { |key| errors << [[:kafka, key], :kafka_key_not_per_topic] }
66
+
67
+ errors
68
+ end
69
+
70
+ # Ensure, that acks is not changed when in transactional mode
71
+ # acks needs to be set to 'all' and should not be changed when working with transactional
72
+ # producer as it causes librdkafka to crash
73
+ virtual do |config, errors|
74
+ next true unless errors.empty?
75
+ # Relevant only for the transactional producer
76
+ next true unless config.fetch(:transactional)
77
+
78
+ errors = []
79
+
80
+ config
81
+ .fetch(:topic_config)
82
+ .keys
83
+ .select { |key| key.to_s.include?('acks') }
84
+ .each { |key| errors << [[:kafka, key], :kafka_key_acks_not_changeable] }
85
+
86
+ errors
87
+ end
88
+ end
89
+ end
90
+ end
@@ -9,6 +9,9 @@ module WaterDrop
9
9
  # Raised when configuration doesn't match with validation contract
10
10
  ConfigurationInvalidError = Class.new(BaseError)
11
11
 
12
+ # Raised when variant alteration is not valid
13
+ VariantInvalidError = Class.new(BaseError)
14
+
12
15
  # Raised when we want to use a producer that was not configured
13
16
  ProducerNotConfiguredError = Class.new(BaseError)
14
17
 
@@ -137,7 +137,7 @@ module WaterDrop
137
137
  client.send_offsets_to_transaction(
138
138
  consumer,
139
139
  tpl,
140
- @config.max_wait_timeout
140
+ current_variant.max_wait_timeout
141
141
  )
142
142
  end
143
143
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WaterDrop
4
+ class Producer
5
+ # Object that acts as a proxy allowing for alteration of certain low-level per-topic
6
+ # configuration and some other settings that users may find useful to alter, without having
7
+ # to create new producers with their underlying librdkafka instances.
8
+ #
9
+ # Since each librdkafka instance creates at least one TCP connection per broker, creating
10
+ # separate objects just to alter thing like `acks` may not be efficient and may lead to
11
+ # extensive usage of TCP connections, especially in bigger clusters.
12
+ #
13
+ # This variant object allows for "wrapping" of the producer with alteration of those settings
14
+ # in such a way, that two or more alterations can co-exist and share the same producer,
15
+ # effectively sharing the librdkafka client.
16
+ #
17
+ # Since this is an enhanced `SimpleDelegator` all `WaterDrop::Producer` APIs are preserved and
18
+ # a variant alteration can be used as a regular producer. The only important thing is to
19
+ # remember to only close it once.
20
+ #
21
+ # @note Not all settings are alterable. We only allow to alter things that are safe to be
22
+ # altered as they have no impact on the producer. If there is a setting you consider
23
+ # important and want to make it alterable, please open a GH issue for evaluation.
24
+ #
25
+ # @note Please be aware, that variant changes also affect buffers. If you overwrite the
26
+ # `max_wait_timeout`, since buffers are shared (as they exist on producer level), flushing
27
+ # may be impacted.
28
+ #
29
+ # @note `topic_config` is validated when created for the first time during message production.
30
+ # This means, that configuration error may be raised only during dispatch. There is no
31
+ # way out of this, since we need `librdkafka` instance to create the references.
32
+ class Variant < SimpleDelegator
33
+ # Empty hash we use as defaults for topic config.
34
+ # When rdkafka-ruby detects empty hash, it will use the librdkafka defaults
35
+ EMPTY_HASH = {}.freeze
36
+
37
+ private_constant :EMPTY_HASH
38
+
39
+ attr_reader :max_wait_timeout, :topic_config, :producer
40
+
41
+ # @param producer [WaterDrop::Producer] producer for which we want to have a variant
42
+ # @param max_wait_timeout [Integer, nil] alteration to max wait timeout or nil to use
43
+ # default
44
+ # @param topic_config [Hash] extra topic configuration that can be altered.
45
+ # @param default [Boolean] is this a default variant or an altered one
46
+ # @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties
47
+ def initialize(
48
+ producer,
49
+ max_wait_timeout: producer.config.max_wait_timeout,
50
+ topic_config: EMPTY_HASH,
51
+ default: false
52
+ )
53
+ @producer = producer
54
+ @max_wait_timeout = max_wait_timeout
55
+ @topic_config = topic_config
56
+ @default = default
57
+ super(producer)
58
+
59
+ Contracts::Variant.new.validate!(to_h, Errors::VariantInvalidError)
60
+ end
61
+
62
+ # @return [Boolean] is this a default variant for this producer
63
+ def default?
64
+ @default
65
+ end
66
+
67
+ # We need to wrap any methods from our API that could use a variant alteration with the
68
+ # per thread variant injection. Since method_missing can be slow and problematic, it is just
69
+ # easier to use our public API components methods to ensure the variant is being injected.
70
+ [
71
+ Async,
72
+ Buffer,
73
+ Sync,
74
+ Transactions
75
+ ].each do |scope|
76
+ scope.instance_methods(false).each do |method_name|
77
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
78
+ def #{method_name}(*args, &block)
79
+ Thread.current[@producer.id] = self
80
+
81
+ @producer.#{method_name}(*args, &block)
82
+ ensure
83
+ Thread.current[@producer.id] = nil
84
+ end
85
+ RUBY
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # @return [Hash] hash representation for contract validation to ensure basic sanity of the
92
+ # settings.
93
+ def to_h
94
+ {
95
+ default: default?,
96
+ max_wait_timeout: max_wait_timeout,
97
+ topic_config: topic_config,
98
+ # We pass this to validation, to make sure no-one alters the `acks` value when operating
99
+ # in the transactional mode as it causes librdkafka to crash ruby
100
+ # @see https://github.com/confluentinc/librdkafka/issues/4710
101
+ transactional: @producer.transactional?
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
@@ -65,6 +65,7 @@ module WaterDrop
65
65
  @id = @config.id
66
66
  @monitor = @config.monitor
67
67
  @contract = Contracts::Message.new(max_payload_size: @config.max_payload_size)
68
+ @default_variant = Variant.new(self, default: true)
68
69
  @status.configured!
69
70
  end
70
71
 
@@ -181,7 +182,7 @@ module WaterDrop
181
182
  # The linger.ms time will be ignored for the duration of the call,
182
183
  # queued messages will be sent to the broker as soon as possible.
183
184
  begin
184
- @client.flush(@config.max_wait_timeout) unless @client.closed?
185
+ @client.flush(current_variant.max_wait_timeout) unless @client.closed?
185
186
  # We can safely ignore timeouts here because any left outstanding requests
186
187
  # will anyhow force wait on close if not forced.
187
188
  # If forced, we will purge the queue and just close
@@ -209,6 +210,16 @@ module WaterDrop
209
210
  end
210
211
  end
211
212
 
213
+ # Builds the variant alteration and returns it.
214
+ #
215
+ # @param args [Object] anything `Producer::Variant` initializer accepts
216
+ # @return [WaterDrop::Producer::Variant] variant proxy to use with alterations
217
+ def with(**args)
218
+ ensure_active!
219
+
220
+ Variant.new(self, **args)
221
+ end
222
+
212
223
  # Closes the producer with forced close after timeout, purging any outgoing data
213
224
  def close!
214
225
  close(force: true)
@@ -244,10 +255,16 @@ module WaterDrop
244
255
  def wait(handler)
245
256
  handler.wait(
246
257
  # rdkafka max_wait_timeout is in seconds and we use ms
247
- max_wait_timeout: @config.max_wait_timeout / 1_000.0
258
+ max_wait_timeout: current_variant.max_wait_timeout / 1_000.0
248
259
  )
249
260
  end
250
261
 
262
+ # @return [Producer::Context] the variant config. Either custom if built using `#with` or
263
+ # a default one.
264
+ def current_variant
265
+ Thread.current[id] || @default_variant
266
+ end
267
+
251
268
  # Runs the client produce method with a given message
252
269
  #
253
270
  # @param message [Hash] message we want to send
@@ -263,9 +280,17 @@ module WaterDrop
263
280
  ensure_active!
264
281
  end
265
282
 
266
- # In case someone defines topic as a symbol, we need to convert it into a string as
267
- # librdkafka does not accept symbols
268
- message = message.merge(topic: message[:topic].to_s) if message[:topic].is_a?(Symbol)
283
+ # We basically only duplicate the message hash only if it is needed.
284
+ # It is needed when user is using a custom settings variant or when symbol is provided as
285
+ # the topic name. We should never mutate user input message as it may be a hash that the
286
+ # user is using for some other operations
287
+ if message[:topic].is_a?(Symbol) || !current_variant.default?
288
+ message = message.dup
289
+ # In case someone defines topic as a symbol, we need to convert it into a string as
290
+ # librdkafka does not accept symbols
291
+ message[:topic] = message[:topic].to_s
292
+ message[:topic_config] = current_variant.topic_config
293
+ end
269
294
 
270
295
  if transactional?
271
296
  transaction { client.produce(**message) }
@@ -3,5 +3,5 @@
3
3
  # WaterDrop library
4
4
  module WaterDrop
5
5
  # Current WaterDrop version
6
- VERSION = '2.7.0'
6
+ VERSION = '2.7.1'
7
7
  end
data/waterdrop.gemspec CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.license = 'MIT'
18
18
 
19
19
  spec.add_dependency 'karafka-core', '>= 2.4.0', '< 3.0.0'
20
+ spec.add_dependency 'karafka-rdkafka', '>= 0.15.1'
20
21
  spec.add_dependency 'zeitwerk', '~> 2.3'
21
22
 
22
23
  spec.required_ruby_version = '>= 3.0.0'
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.7.0
4
+ version: 2.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -35,7 +35,7 @@ cert_chain:
35
35
  AnG1dJU+yL2BK7vaVytLTstJME5mepSZ46qqIJXMuWob/YPDmVaBF39TDSG9e34s
36
36
  msG3BiCqgOgHAnL23+CN3Rt8MsuRfEtoTKpJVcCfoEoNHOkc
37
37
  -----END CERTIFICATE-----
38
- date: 2024-04-26 00:00:00.000000000 Z
38
+ date: 2024-05-09 00:00:00.000000000 Z
39
39
  dependencies:
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: karafka-core
@@ -57,6 +57,20 @@ dependencies:
57
57
  - - "<"
58
58
  - !ruby/object:Gem::Version
59
59
  version: 3.0.0
60
+ - !ruby/object:Gem::Dependency
61
+ name: karafka-rdkafka
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 0.15.1
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 0.15.1
60
74
  - !ruby/object:Gem::Dependency
61
75
  name: zeitwerk
62
76
  requirement: !ruby/object:Gem::Requirement
@@ -103,6 +117,7 @@ files:
103
117
  - lib/waterdrop/contracts/config.rb
104
118
  - lib/waterdrop/contracts/message.rb
105
119
  - lib/waterdrop/contracts/transactional_offset.rb
120
+ - lib/waterdrop/contracts/variant.rb
106
121
  - lib/waterdrop/errors.rb
107
122
  - lib/waterdrop/helpers/counter.rb
108
123
  - lib/waterdrop/instrumentation/callbacks/delivery.rb
@@ -122,6 +137,7 @@ files:
122
137
  - lib/waterdrop/producer/status.rb
123
138
  - lib/waterdrop/producer/sync.rb
124
139
  - lib/waterdrop/producer/transactions.rb
140
+ - lib/waterdrop/producer/variant.rb
125
141
  - lib/waterdrop/version.rb
126
142
  - log/.gitkeep
127
143
  - renovate.json
metadata.gz.sig CHANGED
Binary file