karafka 0.5.0.3 → 0.6.0.rc1
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 +4 -4
- data/.console_irbrc +13 -0
- data/.github/ISSUE_TEMPLATE.md +2 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +59 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +67 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +46 -147
- data/README.md +51 -952
- data/Rakefile +5 -14
- data/karafka.gemspec +19 -13
- data/lib/karafka.rb +7 -4
- data/lib/karafka/app.rb +10 -6
- data/lib/karafka/attributes_map.rb +67 -0
- data/lib/karafka/base_controller.rb +42 -52
- data/lib/karafka/base_responder.rb +30 -14
- data/lib/karafka/base_worker.rb +11 -26
- data/lib/karafka/cli.rb +2 -0
- data/lib/karafka/cli/base.rb +2 -0
- data/lib/karafka/cli/console.rb +7 -1
- data/lib/karafka/cli/flow.rb +13 -13
- data/lib/karafka/cli/info.rb +7 -4
- data/lib/karafka/cli/install.rb +4 -3
- data/lib/karafka/cli/server.rb +3 -1
- data/lib/karafka/cli/worker.rb +2 -0
- data/lib/karafka/connection/config_adapter.rb +103 -0
- data/lib/karafka/connection/listener.rb +16 -12
- data/lib/karafka/connection/messages_consumer.rb +86 -0
- data/lib/karafka/connection/messages_processor.rb +74 -0
- data/lib/karafka/errors.rb +15 -29
- data/lib/karafka/fetcher.rb +10 -8
- data/lib/karafka/helpers/class_matcher.rb +2 -0
- data/lib/karafka/helpers/config_retriever.rb +46 -0
- data/lib/karafka/helpers/multi_delegator.rb +2 -0
- data/lib/karafka/loader.rb +4 -2
- data/lib/karafka/logger.rb +37 -36
- data/lib/karafka/monitor.rb +3 -1
- data/lib/karafka/params/interchanger.rb +2 -0
- data/lib/karafka/params/params.rb +34 -41
- data/lib/karafka/params/params_batch.rb +46 -0
- data/lib/karafka/parsers/json.rb +4 -2
- data/lib/karafka/patches/dry_configurable.rb +2 -0
- data/lib/karafka/process.rb +4 -2
- data/lib/karafka/responders/builder.rb +2 -0
- data/lib/karafka/responders/topic.rb +14 -6
- data/lib/karafka/routing/builder.rb +22 -59
- data/lib/karafka/routing/consumer_group.rb +54 -0
- data/lib/karafka/routing/mapper.rb +2 -0
- data/lib/karafka/routing/proxy.rb +37 -0
- data/lib/karafka/routing/router.rb +18 -16
- data/lib/karafka/routing/topic.rb +78 -0
- data/lib/karafka/schemas/config.rb +36 -0
- data/lib/karafka/schemas/consumer_group.rb +56 -0
- data/lib/karafka/schemas/responder_usage.rb +38 -0
- data/lib/karafka/server.rb +5 -3
- data/lib/karafka/setup/config.rb +79 -32
- data/lib/karafka/setup/configurators/base.rb +2 -0
- data/lib/karafka/setup/configurators/celluloid.rb +2 -0
- data/lib/karafka/setup/configurators/sidekiq.rb +2 -0
- data/lib/karafka/setup/configurators/water_drop.rb +15 -3
- data/lib/karafka/status.rb +2 -0
- data/lib/karafka/templates/app.rb.example +15 -5
- data/lib/karafka/templates/application_worker.rb.example +0 -6
- data/lib/karafka/version.rb +2 -1
- data/lib/karafka/workers/builder.rb +2 -0
- metadata +109 -60
- data/lib/karafka/cli/routes.rb +0 -36
- data/lib/karafka/connection/consumer.rb +0 -33
- data/lib/karafka/connection/message.rb +0 -17
- data/lib/karafka/connection/topic_consumer.rb +0 -94
- data/lib/karafka/responders/usage_validator.rb +0 -60
- data/lib/karafka/routing/route.rb +0 -113
- data/lib/karafka/setup/config_schema.rb +0 -44
- data/lib/karafka/setup/configurators/worker_glass.rb +0 -13
- data/lib/karafka/templates/config.ru.example +0 -13
data/lib/karafka/parsers/json.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Karafka
|
2
4
|
# Module for all supported by default parsers for incoming/outgoing data
|
3
5
|
module Parsers
|
@@ -8,8 +10,8 @@ module Karafka
|
|
8
10
|
# @example
|
9
11
|
# Json.parse("{\"a\":1}") #=> { 'a' => 1 }
|
10
12
|
def self.parse(content)
|
11
|
-
::
|
12
|
-
rescue ::
|
13
|
+
::Yajl::Parser.parse(content)
|
14
|
+
rescue ::Yajl::ParseError => e
|
13
15
|
raise ::Karafka::Errors::ParserError, e
|
14
16
|
end
|
15
17
|
|
data/lib/karafka/process.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Karafka
|
2
4
|
# Class used to catch signals from ruby Signal class in order to manage Karafka shutdown
|
3
5
|
# @note There might be only one process - this class is a singleton
|
@@ -5,9 +7,9 @@ module Karafka
|
|
5
7
|
include Singleton
|
6
8
|
|
7
9
|
# Signal types that we handle
|
8
|
-
HANDLED_SIGNALS = %i
|
10
|
+
HANDLED_SIGNALS = %i[
|
9
11
|
SIGINT SIGQUIT SIGTERM
|
10
|
-
|
12
|
+
].freeze
|
11
13
|
|
12
14
|
HANDLED_SIGNALS.each do |signal|
|
13
15
|
# Assigns a callback that will happen when certain signal will be send
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Karafka
|
2
4
|
module Responders
|
3
5
|
# Topic describes a single topic on which we want to respond with responding requirements
|
@@ -17,7 +19,6 @@ module Karafka
|
|
17
19
|
def initialize(name, options)
|
18
20
|
@name = name.to_s
|
19
21
|
@options = options
|
20
|
-
validate!
|
21
22
|
end
|
22
23
|
|
23
24
|
# @return [Boolean] is this a required topic (if not, it is optional)
|
@@ -30,12 +31,19 @@ module Karafka
|
|
30
31
|
@options[:multiple_usage] || false
|
31
32
|
end
|
32
33
|
|
33
|
-
|
34
|
+
# @return [Boolean] was usage of this topic registered or not
|
35
|
+
def registered?
|
36
|
+
@options[:registered] == true
|
37
|
+
end
|
34
38
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
# @return [Hash] hash with this topic attributes and options
|
40
|
+
def to_h
|
41
|
+
{
|
42
|
+
name: name,
|
43
|
+
multiple_usage: multiple_usage?,
|
44
|
+
required: required?,
|
45
|
+
registered: registered?
|
46
|
+
}
|
39
47
|
end
|
40
48
|
end
|
41
49
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Karafka
|
2
4
|
module Routing
|
3
|
-
#
|
5
|
+
# Builder used as a DSL layer for building consumers and telling them which topics to consume
|
4
6
|
# @example Build a simple (most common) route
|
5
|
-
#
|
7
|
+
# consumers do
|
6
8
|
# topic :new_videos do
|
7
9
|
# controller NewVideosController
|
8
10
|
# end
|
@@ -10,42 +12,6 @@ module Karafka
|
|
10
12
|
class Builder < Array
|
11
13
|
include Singleton
|
12
14
|
|
13
|
-
# Options that are being set on the route level
|
14
|
-
ROUTE_OPTIONS = %i(
|
15
|
-
group
|
16
|
-
worker
|
17
|
-
controller
|
18
|
-
parser
|
19
|
-
interchanger
|
20
|
-
responder
|
21
|
-
inline_mode
|
22
|
-
batch_mode
|
23
|
-
).freeze
|
24
|
-
|
25
|
-
# All those options should be set on the route level
|
26
|
-
ROUTE_OPTIONS.each do |option|
|
27
|
-
define_method option do |value|
|
28
|
-
@current_route.public_send :"#{option}=", value
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Creates a new route for a given topic and evalues provided block in builder context
|
33
|
-
# @param topic [String, Symbol] Kafka topic name
|
34
|
-
# @param block [Proc] block that will be evaluated in current context
|
35
|
-
# @note Creating new topic means creating a new route
|
36
|
-
# @example Define controller for a topic
|
37
|
-
# topic :xyz do
|
38
|
-
# controller XyzController
|
39
|
-
# end
|
40
|
-
def topic(topic, &block)
|
41
|
-
@current_route = Route.new
|
42
|
-
@current_route.topic = topic
|
43
|
-
|
44
|
-
instance_eval(&block)
|
45
|
-
|
46
|
-
store!
|
47
|
-
end
|
48
|
-
|
49
15
|
# Used to draw routes for Karafka
|
50
16
|
# @note After it is done drawing it will store and validate all the routes to make sure that
|
51
17
|
# they are correct and that there are no topic/group duplications (this is forbidden)
|
@@ -57,34 +23,31 @@ module Karafka
|
|
57
23
|
# end
|
58
24
|
def draw(&block)
|
59
25
|
instance_eval(&block)
|
26
|
+
|
27
|
+
each do |consumer_group|
|
28
|
+
hashed_group = consumer_group.to_h
|
29
|
+
validation_result = Karafka::Schemas::ConsumerGroup.call(hashed_group)
|
30
|
+
next if validation_result.success?
|
31
|
+
raise Errors::InvalidConfiguration, [validation_result.errors, hashed_group]
|
32
|
+
end
|
60
33
|
end
|
61
34
|
|
62
35
|
private
|
63
36
|
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
self <<
|
70
|
-
|
71
|
-
validate! :topic, Errors::DuplicatedTopicError
|
72
|
-
validate! :group, Errors::DuplicatedGroupError
|
37
|
+
# Builds and saves given consumer group
|
38
|
+
# @param group_id [String, Symbol] name for consumer group
|
39
|
+
# @yield Evaluates a given block in a consumer group context
|
40
|
+
def consumer_group(group_id, &block)
|
41
|
+
consumer_group = ConsumerGroup.new(group_id.to_s)
|
42
|
+
self << Proxy.new(consumer_group, &block).target
|
73
43
|
end
|
74
44
|
|
75
|
-
#
|
76
|
-
# @
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
key = route.public_send(attribute)
|
81
|
-
amounts[key] = amounts[key].to_i + 1
|
82
|
-
amounts
|
45
|
+
# @param topic_name [String, Symbol] name of a topic from which we want to consumer
|
46
|
+
# @yield Evaluates a given block in a topic context
|
47
|
+
def topic(topic_name, &block)
|
48
|
+
consumer_group(topic_name) do
|
49
|
+
topic(topic_name, &block).tap(&:build)
|
83
50
|
end
|
84
|
-
|
85
|
-
wrong = map.find { |_, amount| amount > 1 }
|
86
|
-
|
87
|
-
raise error, wrong if wrong
|
88
51
|
end
|
89
52
|
end
|
90
53
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Routing
|
5
|
+
# Object used to describe a single consumer group that is going to subscribe to
|
6
|
+
# given topics
|
7
|
+
# It is a part of Karafka's DSL
|
8
|
+
class ConsumerGroup
|
9
|
+
extend Helpers::ConfigRetriever
|
10
|
+
|
11
|
+
attr_reader :topics
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
# @param id [String, Symbol] raw id of this consumer group. Raw means, that it does not
|
15
|
+
# yet have an application client_id namespace, this will be added here by default.
|
16
|
+
# We add it to make a multi-system development easier for people that don't use
|
17
|
+
# kafka and don't understand the concept of consumer groups.
|
18
|
+
def initialize(id)
|
19
|
+
@id = "#{Karafka::App.config.client_id.to_s.underscore}_#{id}"
|
20
|
+
@topics = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Builds a topic representation inside of a current consumer group route
|
24
|
+
# @param name [String, Symbol] name of topic to which we want to subscribe
|
25
|
+
# @yield Evaluates a given block in a topic context
|
26
|
+
# @return [Karafka::Routing::Topic] newly built topic instance
|
27
|
+
def topic=(name, &block)
|
28
|
+
topic = Topic.new(name, self)
|
29
|
+
@topics << Proxy.new(topic, &block).target.tap(&:build)
|
30
|
+
@topics.last
|
31
|
+
end
|
32
|
+
|
33
|
+
Karafka::AttributesMap.consumer_group.each do |attribute|
|
34
|
+
config_retriever_for(attribute)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Hashed version of consumer group that can be used for validation purposes
|
38
|
+
# @return [Hash] hash with consumer group attributes including serialized to hash
|
39
|
+
# topics inside of it.
|
40
|
+
def to_h
|
41
|
+
result = {
|
42
|
+
topics: topics.map(&:to_h),
|
43
|
+
id: id
|
44
|
+
}
|
45
|
+
|
46
|
+
Karafka::AttributesMap.consumer_group.each do |attribute|
|
47
|
+
result[attribute] = public_send(attribute)
|
48
|
+
end
|
49
|
+
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Routing
|
5
|
+
# Proxy is used as a translation layer in between the DSL and raw topic and consumer group
|
6
|
+
# objects.
|
7
|
+
class Proxy
|
8
|
+
attr_reader :target
|
9
|
+
|
10
|
+
# We should proxy only non ? and = methods as we want to have a regular dsl
|
11
|
+
IGNORED_POSTFIXES = %w[
|
12
|
+
?
|
13
|
+
=
|
14
|
+
!
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
# @param target [Object] target object to which we proxy any DSL call
|
18
|
+
# @yield Evaluates block in the proxy context
|
19
|
+
def initialize(target, &block)
|
20
|
+
@target = target
|
21
|
+
instance_eval(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Translates the no "=" DSL of routing into elements assignments on target
|
25
|
+
def method_missing(method_name, *arguments, &block)
|
26
|
+
return super unless respond_to_missing?(method_name)
|
27
|
+
@target.public_send(:"#{method_name}=", *arguments, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Tells whether or not a given element exists on the target
|
31
|
+
def respond_to_missing?(method_name, include_private = false)
|
32
|
+
return false if IGNORED_POSTFIXES.any? { |postfix| method_name.to_s.end_with?(postfix) }
|
33
|
+
@target.respond_to?(:"#{method_name}=", include_private) || super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,24 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Karafka
|
2
4
|
# Namespace for all elements related to requests routing
|
3
5
|
module Routing
|
4
6
|
# Karafka framework Router for routing incoming messages to proper controllers
|
5
7
|
# @note Since Kafka does not provide namespaces or modules for topics, they all have "flat"
|
6
8
|
# structure so all the routes are being stored in a single level array
|
7
|
-
|
8
|
-
# @param topic [String] topic based on which we find a proper route
|
9
|
-
# @return [Karafka::Router] router instance
|
10
|
-
def initialize(topic)
|
11
|
-
@topic = topic
|
12
|
-
end
|
13
|
-
|
9
|
+
module Router
|
14
10
|
# Builds a controller instance that should handle message from a given topic
|
11
|
+
# @param topic_id [String] topic based on which we find a proper route
|
15
12
|
# @return [Karafka::BaseController] base controller descendant instance object
|
16
|
-
def build
|
17
|
-
|
18
|
-
|
19
|
-
ctrl.public_send(:"#{attr}=", route.public_send(attr))
|
20
|
-
end
|
21
|
-
end
|
13
|
+
def build(topic_id)
|
14
|
+
topic = find(topic_id)
|
15
|
+
topic.controller.new.tap { |ctrl| ctrl.topic = topic }
|
22
16
|
end
|
23
17
|
|
24
18
|
private
|
@@ -26,10 +20,18 @@ module Karafka
|
|
26
20
|
# @return [Karafka::Routing::Route] proper route details
|
27
21
|
# @raise [Karafka::Topic::NonMatchingTopicError] raised if topic name does not match
|
28
22
|
# any route defined by user using routes.draw
|
29
|
-
def
|
30
|
-
App.
|
31
|
-
|
23
|
+
def find(topic_id)
|
24
|
+
App.consumer_groups.each do |consumer_group|
|
25
|
+
consumer_group.topics.each do |topic|
|
26
|
+
return topic if topic.id == topic_id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
raise(Errors::NonMatchingRouteError, topic_id)
|
32
31
|
end
|
32
|
+
|
33
|
+
module_function :build
|
34
|
+
module_function :find
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Routing
|
5
|
+
# Topic stores all the details on how we should interact with Kafka given topic
|
6
|
+
# It belongs to a consumer group as from 0.6 all the topics can work in the same consumer group
|
7
|
+
# It is a part of Karafka's DSL
|
8
|
+
class Topic
|
9
|
+
extend Helpers::ConfigRetriever
|
10
|
+
|
11
|
+
attr_reader :id, :consumer_group
|
12
|
+
attr_accessor :controller
|
13
|
+
|
14
|
+
# @param [String, Symbol] name of a topic on which we want to listen
|
15
|
+
# @param consumer_group [Karafka::Routing::ConsumerGroup] owning consumer group of this topic
|
16
|
+
def initialize(name, consumer_group)
|
17
|
+
@name = name.to_s
|
18
|
+
@consumer_group = consumer_group
|
19
|
+
@attributes = {}
|
20
|
+
# @note We use identifier related to the consumer group that owns a topic, because from
|
21
|
+
# Karafka 0.6 we can handle multiple Kafka instances with the same process and we can
|
22
|
+
# have same topic name across mutliple Kafkas
|
23
|
+
@id = "#{consumer_group.id}_#{@name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Initializes default values for all the options that support defaults if their values are
|
27
|
+
# not yet specified. This is need to be done (cannot be lazy loaded on first use) because
|
28
|
+
# everywhere except Karafka server command, those would not be initialized on time - for
|
29
|
+
# example for Sidekiq
|
30
|
+
def build
|
31
|
+
Karafka::AttributesMap.topic.each { |attr| send(attr) }
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Class] Class (not an instance) of a worker that should be used to schedule the
|
36
|
+
# background job
|
37
|
+
# @note If not provided - will be built based on the provided controller
|
38
|
+
def worker
|
39
|
+
@worker ||= inline_processing ? nil : Karafka::Workers::Builder.new(controller).build
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Class, nil] Class (not an instance) of a responder that should respond from
|
43
|
+
# controller back to Kafka (usefull for piping dataflows)
|
44
|
+
def responder
|
45
|
+
@responder ||= Karafka::Responders::Builder.new(controller).build
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Class] Parser class (not instance) that we want to use to unparse Kafka messages
|
49
|
+
# @note If not provided - will use Json as default
|
50
|
+
def parser
|
51
|
+
@parser ||= Karafka::Parsers::Json
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Class] Interchanger class (not an instance) that we want to use to interchange
|
55
|
+
# params between Karafka server and Karafka background job
|
56
|
+
def interchanger
|
57
|
+
@interchanger ||= Karafka::Params::Interchanger
|
58
|
+
end
|
59
|
+
|
60
|
+
Karafka::AttributesMap.topic.each do |attribute|
|
61
|
+
config_retriever_for(attribute)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Hash] hash with all the topic attributes
|
65
|
+
# @note This is being used when we validate the consumer_group and its topics
|
66
|
+
def to_h
|
67
|
+
map = Karafka::AttributesMap.topic.map do |attribute|
|
68
|
+
[attribute, public_send(attribute)]
|
69
|
+
end
|
70
|
+
|
71
|
+
Hash[map].merge!(
|
72
|
+
id: id,
|
73
|
+
controller: controller
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
# Namespace for all the validation schemas that we use to check input
|
5
|
+
module Schemas
|
6
|
+
# Regexp for validating format of groups and topics
|
7
|
+
TOPIC_REGEXP = /\A(\w|\-|\.)+\z/
|
8
|
+
|
9
|
+
# Schema with validation rules for Karafka configuration details
|
10
|
+
# @note There are many more configuration options inside of the
|
11
|
+
# Karafka::Setup::Config model, but we don't validate them here as they are
|
12
|
+
# validated per each route (topic + consumer_group) because they can be overwritten,
|
13
|
+
# so we validate all of that once all the routes are defined and ready
|
14
|
+
Config = Dry::Validation.Schema do
|
15
|
+
required(:client_id).filled(:str?, format?: Karafka::Schemas::TOPIC_REGEXP)
|
16
|
+
|
17
|
+
required(:redis).maybe do
|
18
|
+
schema do
|
19
|
+
required(:url).filled(:str?)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
optional(:inline_processing).filled(:bool?)
|
24
|
+
|
25
|
+
# If inline_processing is true, redis should be filled
|
26
|
+
rule(redis_presence: %i[redis inline_processing]) do |redis, inline_processing|
|
27
|
+
inline_processing.false?.then(redis.filled?)
|
28
|
+
end
|
29
|
+
|
30
|
+
optional(:connection_pool).schema do
|
31
|
+
required(:size).filled
|
32
|
+
optional(:timeout).filled(:int?)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|