-
-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provide context based producer variants (#483)
- Loading branch information
Showing
17 changed files
with
457 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# frozen_string_literal: true | ||
|
||
module WaterDrop | ||
module Contracts | ||
# Variant validator to ensure basic sanity of the variant alteration data | ||
class Variant < ::Karafka::Core::Contractable::Contract | ||
# Taken from librdkafka config | ||
# Those values can be changed on a per topic basis. We do not support experimental or | ||
# deprecated values. We also do not support settings that would break rdkafka-ruby | ||
# | ||
# @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties | ||
TOPIC_CONFIG_KEYS = %i[ | ||
acks | ||
compression.codec | ||
compression.level | ||
compression.type | ||
delivery.timeout.ms | ||
message.timeout.ms | ||
partitioner | ||
request.required.acks | ||
request.timeout.ms | ||
].freeze | ||
|
||
# Boolean values | ||
BOOLEANS = [true, false].freeze | ||
|
||
private_constant :TOPIC_CONFIG_KEYS, :BOOLEANS | ||
|
||
configure do |config| | ||
config.error_messages = YAML.safe_load( | ||
File.read( | ||
File.join(WaterDrop.gem_root, 'config', 'locales', 'errors.yml') | ||
) | ||
).fetch('en').fetch('validations').fetch('variant') | ||
end | ||
|
||
required(:default) { |val| BOOLEANS.include?(val) } | ||
required(:max_wait_timeout) { |val| val.is_a?(Numeric) && val >= 0 } | ||
|
||
# Checks if all keys are symbols | ||
virtual do |config, errors| | ||
next true unless errors.empty? | ||
|
||
errors = [] | ||
|
||
config | ||
.fetch(:topic_config) | ||
.keys | ||
.reject { |key| key.is_a?(Symbol) } | ||
.each { |key| errors << [[:kafka, key], :kafka_key_must_be_a_symbol] } | ||
|
||
errors | ||
end | ||
|
||
# Checks if we have any keys that are not allowed | ||
virtual do |config, errors| | ||
next true unless errors.empty? | ||
|
||
errors = [] | ||
|
||
config | ||
.fetch(:topic_config) | ||
.keys | ||
.reject { |key| TOPIC_CONFIG_KEYS.include?(key) } | ||
.each { |key| errors << [[:kafka, key], :kafka_key_not_per_topic] } | ||
|
||
errors | ||
end | ||
|
||
# Ensure, that acks is not changed when in transactional mode | ||
# acks needs to be set to 'all' and should not be changed when working with transactional | ||
# producer as it causes librdkafka to crash | ||
virtual do |config, errors| | ||
next true unless errors.empty? | ||
# Relevant only for the transactional producer | ||
next true unless config.fetch(:transactional) | ||
|
||
errors = [] | ||
|
||
config | ||
.fetch(:topic_config) | ||
.keys | ||
.select { |key| key.to_s.include?('acks') } | ||
.each { |key| errors << [[:kafka, key], :kafka_key_acks_not_changeable] } | ||
|
||
errors | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# frozen_string_literal: true | ||
|
||
module WaterDrop | ||
class Producer | ||
# Object that acts as a proxy allowing for alteration of certain low-level per-topic | ||
# configuration and some other settings that users may find useful to alter, without having | ||
# to create new producers with their underlying librdkafka instances. | ||
# | ||
# Since each librdkafka instance creates at least one TCP connection per broker, creating | ||
# separate objects just to alter thing like `acks` may not be efficient and may lead to | ||
# extensive usage of TCP connections, especially in bigger clusters. | ||
# | ||
# This variant object allows for "wrapping" of the producer with alteration of those settings | ||
# in such a way, that two or more alterations can co-exist and share the same producer, | ||
# effectively sharing the librdkafka client. | ||
# | ||
# Since this is an enhanced `SimpleDelegator` all `WaterDrop::Producer` APIs are preserved and | ||
# a variant alteration can be used as a regular producer. The only important thing is to | ||
# remember to only close it once. | ||
# | ||
# @note Not all settings are alterable. We only allow to alter things that are safe to be | ||
# altered as they have no impact on the producer. If there is a setting you consider | ||
# important and want to make it alterable, please open a GH issue for evaluation. | ||
# | ||
# @note Please be aware, that variant changes also affect buffers. If you overwrite the | ||
# `max_wait_timeout`, since buffers are shared (as they exist on producer level), flushing | ||
# may be impacted. | ||
# | ||
# @note `topic_config` is validated when created for the first time during message production. | ||
# This means, that configuration error may be raised only during dispatch. There is no | ||
# way out of this, since we need `librdkafka` instance to create the references. | ||
class Variant < SimpleDelegator | ||
# Empty hash we use as defaults for topic config. | ||
# When rdkafka-ruby detects empty hash, it will use the librdkafka defaults | ||
EMPTY_HASH = {}.freeze | ||
|
||
private_constant :EMPTY_HASH | ||
|
||
attr_reader :max_wait_timeout, :topic_config, :producer | ||
|
||
# @param producer [WaterDrop::Producer] producer for which we want to have a variant | ||
# @param max_wait_timeout [Integer, nil] alteration to max wait timeout or nil to use | ||
# default | ||
# @param topic_config [Hash] extra topic configuration that can be altered. | ||
# @param default [Boolean] is this a default variant or an altered one | ||
# @see https://karafka.io/docs/Librdkafka-Configuration/#topic-configuration-properties | ||
def initialize( | ||
producer, | ||
max_wait_timeout: producer.config.max_wait_timeout, | ||
topic_config: EMPTY_HASH, | ||
default: false | ||
) | ||
@producer = producer | ||
@max_wait_timeout = max_wait_timeout | ||
@topic_config = topic_config | ||
@default = default | ||
super(producer) | ||
|
||
Contracts::Variant.new.validate!(to_h, Errors::VariantInvalidError) | ||
end | ||
|
||
# @return [Boolean] is this a default variant for this producer | ||
def default? | ||
@default | ||
end | ||
|
||
# We need to wrap any methods from our API that could use a variant alteration with the | ||
# per thread variant injection. Since method_missing can be slow and problematic, it is just | ||
# easier to use our public API components methods to ensure the variant is being injected. | ||
[ | ||
Async, | ||
Buffer, | ||
Sync, | ||
Transactions | ||
].each do |scope| | ||
scope.instance_methods(false).each do |method_name| | ||
class_eval <<-RUBY, __FILE__, __LINE__ + 1 | ||
def #{method_name}(*args, &block) | ||
Thread.current[@producer.id] = self | ||
@producer.#{method_name}(*args, &block) | ||
ensure | ||
Thread.current[@producer.id] = nil | ||
end | ||
RUBY | ||
end | ||
end | ||
|
||
private | ||
|
||
# @return [Hash] hash representation for contract validation to ensure basic sanity of the | ||
# settings. | ||
def to_h | ||
{ | ||
default: default?, | ||
max_wait_timeout: max_wait_timeout, | ||
topic_config: topic_config, | ||
# We pass this to validation, to make sure no-one alters the `acks` value when operating | ||
# in the transactional mode as it causes librdkafka to crash ruby | ||
# @see https://github.com/confluentinc/librdkafka/issues/4710 | ||
transactional: @producer.transactional? | ||
} | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.