waterdrop 1.4.0
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.coditsu/ci.yml +3 -0
- data/.github/FUNDING.yml +1 -0
- data/.gitignore +68 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +35 -0
- data/CHANGELOG.md +158 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +102 -0
- data/MIT-LICENCE +18 -0
- data/README.md +127 -0
- data/certs/mensfeld.pem +25 -0
- data/config/errors.yml +19 -0
- data/lib/water_drop.rb +50 -0
- data/lib/water_drop/async_producer.rb +26 -0
- data/lib/water_drop/base_producer.rb +57 -0
- data/lib/water_drop/config.rb +162 -0
- data/lib/water_drop/config_applier.rb +52 -0
- data/lib/water_drop/contracts.rb +9 -0
- data/lib/water_drop/contracts/config.rb +139 -0
- data/lib/water_drop/contracts/message_options.rb +19 -0
- data/lib/water_drop/errors.rb +18 -0
- data/lib/water_drop/instrumentation/monitor.rb +46 -0
- data/lib/water_drop/instrumentation/stdout_listener.rb +45 -0
- data/lib/water_drop/sync_producer.rb +24 -0
- data/lib/water_drop/version.rb +7 -0
- data/lib/waterdrop.rb +4 -0
- data/log/.gitkeep +0 -0
- data/waterdrop.gemspec +36 -0
- metadata +189 -0
- metadata.gz.sig +0 -0
data/certs/mensfeld.pem
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIEODCCAqCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhtYWNp
|
3
|
+
ZWovREM9bWVuc2ZlbGQvREM9cGwwHhcNMjAwODExMDkxNTM3WhcNMjEwODExMDkx
|
4
|
+
NTM3WjAjMSEwHwYDVQQDDBhtYWNpZWovREM9bWVuc2ZlbGQvREM9cGwwggGiMA0G
|
5
|
+
CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDCpXsCgmINb6lHBXXBdyrgsBPSxC4/
|
6
|
+
2H+weJ6L9CruTiv2+2/ZkQGtnLcDgrD14rdLIHK7t0o3EKYlDT5GhD/XUVhI15JE
|
7
|
+
N7IqnPUgexe1fbZArwQ51afxz2AmPQN2BkB2oeQHXxnSWUGMhvcEZpfbxCCJH26w
|
8
|
+
hS0Ccsma8yxA6hSlGVhFVDuCr7c2L1di6cK2CtIDpfDaWqnVNJEwBYHIxrCoWK5g
|
9
|
+
sIGekVt/admS9gRhIMaIBg+Mshth5/DEyWO2QjteTodItlxfTctrfmiAl8X8T5JP
|
10
|
+
VXeLp5SSOJ5JXE80nShMJp3RFnGw5fqjX/ffjtISYh78/By4xF3a25HdWH9+qO2Z
|
11
|
+
tx0wSGc9/4gqNM0APQnjN/4YXrGZ4IeSjtE+OrrX07l0TiyikzSLFOkZCAp8oBJi
|
12
|
+
Fhlosz8xQDJf7mhNxOaZziqASzp/hJTU/tuDKl5+ql2icnMv5iV/i6SlmvU29QNg
|
13
|
+
LCV71pUv0pWzN+OZbHZKWepGhEQ3cG9MwvkCAwEAAaN3MHUwCQYDVR0TBAIwADAL
|
14
|
+
BgNVHQ8EBAMCBLAwHQYDVR0OBBYEFImGed2AXS070ohfRidiCEhXEUN+MB0GA1Ud
|
15
|
+
EQQWMBSBEm1hY2llakBtZW5zZmVsZC5wbDAdBgNVHRIEFjAUgRJtYWNpZWpAbWVu
|
16
|
+
c2ZlbGQucGwwDQYJKoZIhvcNAQELBQADggGBAKiHpwoENVrMi94V1zD4o8/6G3AU
|
17
|
+
gWz4udkPYHTZLUy3dLznc/sNjdkJFWT3E6NKYq7c60EpJ0m0vAEg5+F5pmNOsvD3
|
18
|
+
2pXLj9kisEeYhR516HwXAvtngboUcb75skqvBCU++4Pu7BRAPjO1/ihLSBexbwSS
|
19
|
+
fF+J5OWNuyHHCQp+kGPLtXJe2yUYyvSWDj3I2//Vk0VhNOIlaCS1+5/P3ZJThOtm
|
20
|
+
zJUBI7h3HgovwRpcnmk2mXTmU4Zx/bCzX8EA6VY0khEvnmiq7S6eBF0H9qH8KyQ6
|
21
|
+
EkVLpvmUDFcf/uNaBQdazEMB5jYtwoA8gQlANETNGPi51KlkukhKgaIEDMkBDJOx
|
22
|
+
65N7DzmkcyY0/GwjIVIxmRhcrCt1YeCUElmfFx0iida1/YRm6sB2AXqScc1+ECRi
|
23
|
+
2DND//YJUikn1zwbz1kT70XmHd97B4Eytpln7K+M1u2g1pHVEPW4owD/ammXNpUy
|
24
|
+
nt70FcDD4yxJQ+0YNiHd0N8IcVBM1TMIVctMNQ==
|
25
|
+
-----END CERTIFICATE-----
|
data/config/errors.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
en:
|
2
|
+
dry_validation:
|
3
|
+
errors:
|
4
|
+
broker_schema: >
|
5
|
+
has an invalid format.
|
6
|
+
Expected schema, host and port number.
|
7
|
+
Example: kafka://127.0.0.1:9092 or kafka+ssl://127.0.0.1:9092
|
8
|
+
ssl_client_cert_with_ssl_client_cert_key: >
|
9
|
+
Both ssl_client_cert and ssl_client_cert_key need to be provided.
|
10
|
+
ssl_client_cert_key_with_ssl_client_cert: >
|
11
|
+
Both ssl_client_cert_key and ssl_client_cert need to be provided.
|
12
|
+
ssl_client_cert_chain_with_ssl_client_cert: >
|
13
|
+
Both ssl_client_cert_chain and ssl_client_cert need to be provided.
|
14
|
+
ssl_client_cert_chain_with_ssl_client_cert_key: >
|
15
|
+
Both ssl_client_cert_chain and ssl_client_cert_key need to be provided.
|
16
|
+
ssl_client_cert_key_password_with_ssl_client_cert_key: >
|
17
|
+
Both ssl_client_cert_key_password and ssl_client_cert_key need to be provided.
|
18
|
+
sasl_oauth_token_provider_respond_to_token: >
|
19
|
+
sasl_oauth_token_provider needs to respond to a #token method.
|
data/lib/water_drop.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# External components
|
4
|
+
# delegate should be removed because we don't need it, we just add it because of ruby-kafka
|
5
|
+
%w[
|
6
|
+
delegate
|
7
|
+
json
|
8
|
+
delivery_boy
|
9
|
+
singleton
|
10
|
+
dry-configurable
|
11
|
+
dry/monitor/notifications
|
12
|
+
dry-validation
|
13
|
+
zeitwerk
|
14
|
+
].each { |lib| require lib }
|
15
|
+
|
16
|
+
# WaterDrop library
|
17
|
+
module WaterDrop
|
18
|
+
class << self
|
19
|
+
attr_accessor :logger
|
20
|
+
|
21
|
+
# Sets up the whole configuration
|
22
|
+
# @param [Block] block configuration block
|
23
|
+
def setup(&block)
|
24
|
+
Config.setup(&block)
|
25
|
+
DeliveryBoy.logger = self.logger = config.logger
|
26
|
+
ConfigApplier.call(DeliveryBoy.config, Config.config.to_h)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [WaterDrop::Config] config instance
|
30
|
+
def config
|
31
|
+
Config.config
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [::WaterDrop::Monitor] monitor that we want to use
|
35
|
+
def monitor
|
36
|
+
config.monitor
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String] root path of this gem
|
40
|
+
def gem_root
|
41
|
+
Pathname.new(File.expand_path('..', __dir__))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
Zeitwerk::Loader
|
47
|
+
.for_gem
|
48
|
+
.tap { |loader| loader.ignore("#{__dir__}/waterdrop.rb") }
|
49
|
+
.tap(&:setup)
|
50
|
+
.tap(&:eager_load)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# WaterDrop library
|
4
|
+
module WaterDrop
|
5
|
+
# Async producer for messages
|
6
|
+
class AsyncProducer < BaseProducer
|
7
|
+
# Performs message delivery using deliver_async method
|
8
|
+
# @param message [String] message that we want to send to Kafka
|
9
|
+
# @param options [Hash] options (including topic) for producer
|
10
|
+
# @raise [WaterDrop::Errors::InvalidMessageOptions] raised when message options are
|
11
|
+
# somehow invalid and we cannot perform delivery because of that
|
12
|
+
def self.call(message, options)
|
13
|
+
attempts_count ||= 0
|
14
|
+
attempts_count += 1
|
15
|
+
|
16
|
+
validate!(options)
|
17
|
+
return unless WaterDrop.config.deliver
|
18
|
+
|
19
|
+
d_method = WaterDrop.config.raise_on_buffer_overflow ? :deliver_async! : :deliver_async
|
20
|
+
|
21
|
+
DeliveryBoy.send(d_method, message, **options)
|
22
|
+
rescue Kafka::Error => e
|
23
|
+
graceful_attempt?(attempts_count, message, options, e) ? retry : raise(e)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WaterDrop
|
4
|
+
# Base messages producer that contains all the logic that is exactly the same for both
|
5
|
+
# sync and async producers
|
6
|
+
class BaseProducer
|
7
|
+
# Contract for checking the correctness of the provided data that someone wants to
|
8
|
+
# dispatch to Kafka
|
9
|
+
SCHEMA = Contracts::MessageOptions.new.freeze
|
10
|
+
|
11
|
+
private_constant :SCHEMA
|
12
|
+
|
13
|
+
class << self
|
14
|
+
private
|
15
|
+
|
16
|
+
# Runs the message options validations and raises an error if anything is invalid
|
17
|
+
# @param options [Hash] hash that we want to validate
|
18
|
+
# @raise [WaterDrop::Errors::InvalidMessageOptions] raised when message options are
|
19
|
+
# somehow invalid and we cannot perform delivery because of that
|
20
|
+
def validate!(options)
|
21
|
+
validation_result = SCHEMA.call(options)
|
22
|
+
return true if validation_result.success?
|
23
|
+
|
24
|
+
raise Errors::InvalidMessageOptions, validation_result.errors
|
25
|
+
end
|
26
|
+
|
27
|
+
# Upon failed delivery, we may try to resend a message depending on the attempt number
|
28
|
+
# or re-raise an error if we're unable to do that after given number of retries
|
29
|
+
# This method checks that and also instruments errors and retries for the delivery
|
30
|
+
# @param attempts_count [Integer] number of attempt (starting from 1) for the delivery
|
31
|
+
# @param message [String] message that we want to send to Kafka
|
32
|
+
# @param options [Hash] options (including topic) for producer
|
33
|
+
# @param error [Kafka::Error] error that occurred
|
34
|
+
# @return [Boolean] true if this is a graceful attempt and we can retry or false it this
|
35
|
+
# was the final one and we should deal with the fact, that we cannot deliver a given
|
36
|
+
# message
|
37
|
+
def graceful_attempt?(attempts_count, message, options, error)
|
38
|
+
scope = "#{to_s.split('::').last.sub('Producer', '_producer').downcase}.call"
|
39
|
+
payload = {
|
40
|
+
caller: self,
|
41
|
+
message: message,
|
42
|
+
options: options,
|
43
|
+
error: error,
|
44
|
+
attempts_count: attempts_count
|
45
|
+
}
|
46
|
+
|
47
|
+
if attempts_count > WaterDrop.config.kafka.max_retries
|
48
|
+
WaterDrop.monitor.instrument("#{scope}.error", payload)
|
49
|
+
false
|
50
|
+
else
|
51
|
+
WaterDrop.monitor.instrument("#{scope}.retry", payload)
|
52
|
+
true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configuration and descriptions are based on the delivery boy zendesk gem
|
4
|
+
# @see https://github.com/zendesk/delivery_boy
|
5
|
+
module WaterDrop
|
6
|
+
# Configuration object for setting up all options required by WaterDrop
|
7
|
+
class Config
|
8
|
+
extend Dry::Configurable
|
9
|
+
|
10
|
+
# Config schema definition
|
11
|
+
# @note We use a single instance not to create new one upon each usage
|
12
|
+
SCHEMA = Contracts::Config.new.freeze
|
13
|
+
|
14
|
+
private_constant :SCHEMA
|
15
|
+
|
16
|
+
# WaterDrop options
|
17
|
+
# option client_id [String] identifier of this producer
|
18
|
+
setting :client_id, 'waterdrop'
|
19
|
+
# option [Instance, nil] logger that we want to use or nil to fallback to ruby-kafka logger
|
20
|
+
setting :logger, Logger.new($stdout, level: Logger::WARN)
|
21
|
+
# option [Instance] monitor that we want to use. See instrumentation part of the README for
|
22
|
+
# more details
|
23
|
+
setting :monitor, WaterDrop::Instrumentation::Monitor.new
|
24
|
+
# option [Boolean] should we send messages. Setting this to false can be really useful when
|
25
|
+
# testing and or developing because when set to false, won't actually ping Kafka
|
26
|
+
setting :deliver, true
|
27
|
+
# option [Boolean] if you're producing messages faster than the framework or the network can
|
28
|
+
# send them off, ruby-kafka might reject them. If that happens, WaterDrop will either raise
|
29
|
+
# or ignore - this setting manages that behavior. This only applies to async producer as
|
30
|
+
# sync producer will always raise upon problems
|
31
|
+
setting :raise_on_buffer_overflow, true
|
32
|
+
|
33
|
+
# Settings directly related to the Kafka driver
|
34
|
+
setting :kafka do
|
35
|
+
# option [Array<String>] Array that contains Kafka seed broker hosts with ports
|
36
|
+
setting :seed_brokers
|
37
|
+
|
38
|
+
# Network timeouts
|
39
|
+
# option connect_timeout [Integer] Sets the number of seconds to wait while connecting to
|
40
|
+
# a broker for the first time. When ruby-kafka initializes, it needs to connect to at
|
41
|
+
# least one host.
|
42
|
+
setting :connect_timeout, 10
|
43
|
+
# option socket_timeout [Integer] Sets the number of seconds to wait when reading from or
|
44
|
+
# writing to a socket connection to a broker. After this timeout expires the connection
|
45
|
+
# will be killed. Note that some Kafka operations are by definition long-running, such as
|
46
|
+
# waiting for new messages to arrive in a partition, so don't set this value too low
|
47
|
+
setting :socket_timeout, 30
|
48
|
+
|
49
|
+
# Buffering for async producer
|
50
|
+
# @option [Integer] The maximum number of bytes allowed in the buffer before new messages
|
51
|
+
# are rejected.
|
52
|
+
setting :max_buffer_bytesize, 10_000_000
|
53
|
+
# @option [Integer] The maximum number of messages allowed in the buffer before new messages
|
54
|
+
# are rejected.
|
55
|
+
setting :max_buffer_size, 1000
|
56
|
+
# @option [Integer] The maximum number of messages allowed in the queue before new messages
|
57
|
+
# are rejected. The queue is used to ferry messages from the foreground threads of your
|
58
|
+
# application to the background thread that buffers and delivers messages.
|
59
|
+
setting :max_queue_size, 1000
|
60
|
+
|
61
|
+
# option [Integer] A timeout executed by a broker when the client is sending messages to it.
|
62
|
+
# It defines the number of seconds the broker should wait for replicas to acknowledge the
|
63
|
+
# write before responding to the client with an error. As such, it relates to the
|
64
|
+
# required_acks setting. It should be set lower than socket_timeout.
|
65
|
+
setting :ack_timeout, 5
|
66
|
+
# option [Integer] The number of seconds between background message
|
67
|
+
# deliveries. Default is 10 seconds. Disable timer-based background deliveries by
|
68
|
+
# setting this to 0.
|
69
|
+
setting :delivery_interval, 10
|
70
|
+
# option [Integer] The number of buffered messages that will trigger a background message
|
71
|
+
# delivery. Default is 100 messages. Disable buffer size based background deliveries by
|
72
|
+
# setting this to 0.
|
73
|
+
setting :delivery_threshold, 100
|
74
|
+
# option [Boolean]
|
75
|
+
setting :idempotent, false
|
76
|
+
# option [Boolean]
|
77
|
+
setting :transactional, false
|
78
|
+
# option [Integer]
|
79
|
+
setting :transactional_timeout, 60
|
80
|
+
|
81
|
+
# option [Integer] The number of retries when attempting to deliver messages.
|
82
|
+
setting :max_retries, 2
|
83
|
+
# option [Integer]
|
84
|
+
setting :required_acks, -1
|
85
|
+
# option [Integer]
|
86
|
+
setting :retry_backoff, 1
|
87
|
+
|
88
|
+
# option [Integer] The minimum number of messages that must be buffered before compression is
|
89
|
+
# attempted. By default only one message is required. Only relevant if compression_codec
|
90
|
+
# is set.
|
91
|
+
setting :compression_threshold, 1
|
92
|
+
# option [Symbol] The codec used to compress messages. Must be either snappy or gzip.
|
93
|
+
setting :compression_codec, nil
|
94
|
+
|
95
|
+
# SSL authentication related settings
|
96
|
+
# option ca_cert [String, nil] SSL CA certificate
|
97
|
+
setting :ssl_ca_cert, nil
|
98
|
+
# option ssl_ca_cert_file_path [String, nil] SSL CA certificate file path
|
99
|
+
setting :ssl_ca_cert_file_path, nil
|
100
|
+
# option ssl_ca_certs_from_system [Boolean] Use the CA certs from your system's default
|
101
|
+
# certificate store
|
102
|
+
setting :ssl_ca_certs_from_system, false
|
103
|
+
# option ssl_verify_hostname [Boolean] Verify the hostname for client certs
|
104
|
+
setting :ssl_verify_hostname, true
|
105
|
+
# option ssl_client_cert [String, nil] SSL client certificate
|
106
|
+
setting :ssl_client_cert, nil
|
107
|
+
# option ssl_client_cert_key [String, nil] SSL client certificate password
|
108
|
+
setting :ssl_client_cert_key, nil
|
109
|
+
# option sasl_gssapi_principal [String, nil] sasl principal
|
110
|
+
setting :sasl_gssapi_principal, nil
|
111
|
+
# option sasl_gssapi_keytab [String, nil] sasl keytab
|
112
|
+
setting :sasl_gssapi_keytab, nil
|
113
|
+
# option sasl_plain_authzid [String] The authorization identity to use
|
114
|
+
setting :sasl_plain_authzid, ''
|
115
|
+
# option sasl_plain_username [String, nil] The username used to authenticate
|
116
|
+
setting :sasl_plain_username, nil
|
117
|
+
# option sasl_plain_password [String, nil] The password used to authenticate
|
118
|
+
setting :sasl_plain_password, nil
|
119
|
+
# option sasl_scram_username [String, nil] The username used to authenticate
|
120
|
+
setting :sasl_scram_username, nil
|
121
|
+
# option sasl_scram_password [String, nil] The password used to authenticate
|
122
|
+
setting :sasl_scram_password, nil
|
123
|
+
# option sasl_scram_mechanism [String, nil] Scram mechanism, either 'sha256' or 'sha512'
|
124
|
+
setting :sasl_scram_mechanism, nil
|
125
|
+
# option sasl_over_ssl [Boolean] whether to enforce SSL with SASL
|
126
|
+
setting :sasl_over_ssl, true
|
127
|
+
# option ssl_client_cert_chain [String, nil] client cert chain or nil if not used
|
128
|
+
setting :ssl_client_cert_chain, nil
|
129
|
+
# option ssl_client_cert_key_password [String, nil] the password required to read
|
130
|
+
# the ssl_client_cert_key
|
131
|
+
setting :ssl_client_cert_key_password, nil
|
132
|
+
# @param sasl_oauth_token_provider [Object, nil] OAuthBearer Token Provider instance that
|
133
|
+
# implements method token.
|
134
|
+
setting :sasl_oauth_token_provider, nil
|
135
|
+
end
|
136
|
+
|
137
|
+
class << self
|
138
|
+
# Configuration method
|
139
|
+
# @yield Runs a block of code providing a config singleton instance to it
|
140
|
+
# @yieldparam [WaterDrop::Config] WaterDrop config instance
|
141
|
+
def setup
|
142
|
+
configure do |config|
|
143
|
+
yield(config)
|
144
|
+
validate!(config.to_h)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
# Validates the configuration and if anything is wrong, will raise an exception
|
151
|
+
# @param config_hash [Hash] config hash with setup details
|
152
|
+
# @raise [WaterDrop::Errors::InvalidConfiguration] raised when something is wrong with
|
153
|
+
# the configuration
|
154
|
+
def validate!(config_hash)
|
155
|
+
validation_result = SCHEMA.call(config_hash)
|
156
|
+
return true if validation_result.success?
|
157
|
+
|
158
|
+
raise Errors::InvalidConfiguration, validation_result.errors.to_h
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WaterDrop
|
4
|
+
# Engine used to propagate config application to DeliveryBoy with corner case handling
|
5
|
+
module ConfigApplier
|
6
|
+
class << self
|
7
|
+
# @param delivery_boy_config [DeliveryBoy::Config] delivery boy config instance
|
8
|
+
# @param settings [Hash] hash with WaterDrop settings
|
9
|
+
def call(delivery_boy_config, settings)
|
10
|
+
# Recursive lambda for mapping config down to delivery boy
|
11
|
+
settings.each do |key, value|
|
12
|
+
call(delivery_boy_config, value) && next if value.is_a?(Hash)
|
13
|
+
|
14
|
+
# If this is a special case that needs manual setup instead of a direct reassignment
|
15
|
+
if respond_to?(key, true)
|
16
|
+
send(key, delivery_boy_config, value)
|
17
|
+
else
|
18
|
+
# If this setting is our internal one, we should not sync it with the delivery boy
|
19
|
+
next unless delivery_boy_config.respond_to?(:"#{key}=")
|
20
|
+
|
21
|
+
delivery_boy_config.public_send(:"#{key}=", value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Extra setup for the compression codec as it behaves differently than other settings
|
29
|
+
# that are ported 1:1 from ruby-kafka
|
30
|
+
# For some crazy reason, delivery boy requires compression codec as a string, when
|
31
|
+
# ruby-kafka as a symbol. We follow ruby-kafka internal design, so we had to mimic
|
32
|
+
# that by assigning a string version that down the road will be symbolized again
|
33
|
+
# by delivery boy
|
34
|
+
# @param delivery_boy_config [DeliveryBoy::Config] delivery boy config instance
|
35
|
+
# @param codec_name [Symbol] codec name as a symbol
|
36
|
+
def compression_codec(delivery_boy_config, codec_name)
|
37
|
+
# If there is no compression codec, we don't apply anything
|
38
|
+
return unless codec_name
|
39
|
+
|
40
|
+
delivery_boy_config.compression_codec = codec_name.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
# We use the "seed_brokers" name and DeliveryBoy uses "brokers" so we pass the values
|
44
|
+
# manually
|
45
|
+
# @param delivery_boy_config [DeliveryBoy::Config] delivery boy config instance
|
46
|
+
# @param seed_brokers [Array<String>] kafka seed brokers
|
47
|
+
def seed_brokers(delivery_boy_config, seed_brokers)
|
48
|
+
delivery_boy_config.brokers = seed_brokers
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WaterDrop
|
4
|
+
module Contracts
|
5
|
+
# Contract with validation rules for WaterDrop configuration details
|
6
|
+
class Config < Dry::Validation::Contract
|
7
|
+
# Valid uri schemas of Kafka broker url
|
8
|
+
URI_SCHEMES = %w[kafka kafka+ssl plaintext ssl].freeze
|
9
|
+
|
10
|
+
# Available sasl scram mechanism of authentication (plus nil)
|
11
|
+
SASL_SCRAM_MECHANISMS = %w[sha256 sha512].freeze
|
12
|
+
|
13
|
+
# Supported compression codecs
|
14
|
+
COMPRESSION_CODECS = %i[snappy gzip lz4 zstd].freeze
|
15
|
+
|
16
|
+
config.messages.load_paths << File.join(WaterDrop.gem_root, 'config', 'errors.yml')
|
17
|
+
|
18
|
+
class << self
|
19
|
+
private
|
20
|
+
|
21
|
+
# Builder for kafka scoped data custom rules
|
22
|
+
# @param keys [Symbol, Hash] the keys names
|
23
|
+
# @param block [Proc] block we want to run with validations within the kafka scope
|
24
|
+
def kafka_scope_rule(*keys, &block)
|
25
|
+
rule(*[:kafka].product(keys)) do
|
26
|
+
instance_exec(values[:kafka], &block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Uri validator to check if uri is in a Kafka acceptable format
|
34
|
+
# @param uri [String] uri we want to validate
|
35
|
+
# @return [Boolean] true if it is a valid uri, otherwise false
|
36
|
+
def broker_schema?(uri)
|
37
|
+
uri = URI.parse(uri)
|
38
|
+
URI_SCHEMES.include?(uri.scheme) && uri.port
|
39
|
+
rescue URI::InvalidURIError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
params do
|
44
|
+
required(:client_id).filled(:str?, format?: Contracts::TOPIC_REGEXP)
|
45
|
+
required(:logger).filled
|
46
|
+
required(:deliver).filled(:bool?)
|
47
|
+
required(:raise_on_buffer_overflow).filled(:bool?)
|
48
|
+
|
49
|
+
required(:kafka).schema do
|
50
|
+
required(:seed_brokers).value(:array, :filled?).each(:str?)
|
51
|
+
required(:connect_timeout).filled(:int?, gt?: 0)
|
52
|
+
required(:socket_timeout).filled(:int?, gt?: 0)
|
53
|
+
required(:compression_threshold).filled(:int?, gteq?: 1)
|
54
|
+
optional(:compression_codec).maybe(included_in?: COMPRESSION_CODECS)
|
55
|
+
|
56
|
+
required(:max_buffer_bytesize).filled(:int?, gt?: 0)
|
57
|
+
required(:max_buffer_size).filled(:int?, gt?: 0)
|
58
|
+
required(:max_queue_size).filled(:int?, gt?: 0)
|
59
|
+
|
60
|
+
required(:ack_timeout).filled(:int?, gt?: 0)
|
61
|
+
required(:delivery_interval).filled(:int?, gteq?: 0)
|
62
|
+
required(:delivery_threshold).filled(:int?, gteq?: 0)
|
63
|
+
|
64
|
+
required(:max_retries).filled(:int?, gteq?: 0)
|
65
|
+
required(:retry_backoff).filled(:int?, gteq?: 0)
|
66
|
+
required(:required_acks).filled(included_in?: [1, 0, -1, :all])
|
67
|
+
|
68
|
+
%i[
|
69
|
+
ssl_ca_cert
|
70
|
+
ssl_ca_cert_file_path
|
71
|
+
ssl_client_cert
|
72
|
+
ssl_client_cert_key
|
73
|
+
ssl_client_cert_chain
|
74
|
+
ssl_client_cert_key_password
|
75
|
+
sasl_gssapi_principal
|
76
|
+
sasl_gssapi_keytab
|
77
|
+
sasl_plain_authzid
|
78
|
+
sasl_plain_username
|
79
|
+
sasl_plain_password
|
80
|
+
sasl_scram_username
|
81
|
+
sasl_scram_password
|
82
|
+
].each do |encryption_attribute|
|
83
|
+
optional(encryption_attribute).maybe(:str?)
|
84
|
+
end
|
85
|
+
|
86
|
+
optional(:ssl_verify_hostname).maybe(:bool?)
|
87
|
+
optional(:ssl_ca_certs_from_system).maybe(:bool?)
|
88
|
+
optional(:sasl_over_ssl).maybe(:bool?)
|
89
|
+
optional(:sasl_oauth_token_provider).value(:any)
|
90
|
+
|
91
|
+
# It's not with other encryptions as it has some more rules
|
92
|
+
optional(:sasl_scram_mechanism)
|
93
|
+
.maybe(:str?, included_in?: SASL_SCRAM_MECHANISMS)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
kafka_scope_rule(:seed_brokers) do |kafka|
|
98
|
+
unless kafka[:seed_brokers].all?(&method(:broker_schema?))
|
99
|
+
key(%i[kafka seed_brokers]).failure(:broker_schema)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
kafka_scope_rule(:ssl_client_cert, :ssl_client_cert_key) do |kafka|
|
104
|
+
if kafka[:ssl_client_cert] &&
|
105
|
+
kafka[:ssl_client_cert_key].nil?
|
106
|
+
key(%i[kafka ssl_client_cert_key]).failure(:ssl_client_cert_with_ssl_client_cert_key)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
kafka_scope_rule(:ssl_client_cert_key, :ssl_client_cert) do |kafka|
|
111
|
+
if kafka[:ssl_client_cert_key] &&
|
112
|
+
kafka[:ssl_client_cert].nil?
|
113
|
+
key.failure(:ssl_client_cert_key_with_ssl_client_cert)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
kafka_scope_rule(:ssl_client_cert_chain, :ssl_client_cert) do |kafka|
|
118
|
+
if kafka[:ssl_client_cert_chain] &&
|
119
|
+
kafka[:ssl_client_cert].nil?
|
120
|
+
key.failure(:ssl_client_cert_chain_with_ssl_client_cert)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
kafka_scope_rule(:ssl_client_cert_key_password, :ssl_client_cert_key) do |kafka|
|
125
|
+
if kafka[:ssl_client_cert_key_password] &&
|
126
|
+
kafka[:ssl_client_cert_key].nil?
|
127
|
+
key.failure(:ssl_client_cert_key_password_with_ssl_client_cert_key)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
kafka_scope_rule(:sasl_oauth_token_provider) do |kafka|
|
132
|
+
if kafka[:sasl_oauth_token_provider] &&
|
133
|
+
!kafka[:sasl_oauth_token_provider].respond_to?(:token)
|
134
|
+
key.failure(:sasl_oauth_token_provider_respond_to_token)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|