karafka 0.5.0.3 → 0.6.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|