waterdrop 2.7.0 → 2.7.1

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.
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