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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.console_irbrc +13 -0
  3. data/.github/ISSUE_TEMPLATE.md +2 -0
  4. data/.gitignore +1 -0
  5. data/CHANGELOG.md +59 -1
  6. data/CODE_OF_CONDUCT.md +46 -0
  7. data/CONTRIBUTING.md +67 -0
  8. data/Gemfile +2 -1
  9. data/Gemfile.lock +46 -147
  10. data/README.md +51 -952
  11. data/Rakefile +5 -14
  12. data/karafka.gemspec +19 -13
  13. data/lib/karafka.rb +7 -4
  14. data/lib/karafka/app.rb +10 -6
  15. data/lib/karafka/attributes_map.rb +67 -0
  16. data/lib/karafka/base_controller.rb +42 -52
  17. data/lib/karafka/base_responder.rb +30 -14
  18. data/lib/karafka/base_worker.rb +11 -26
  19. data/lib/karafka/cli.rb +2 -0
  20. data/lib/karafka/cli/base.rb +2 -0
  21. data/lib/karafka/cli/console.rb +7 -1
  22. data/lib/karafka/cli/flow.rb +13 -13
  23. data/lib/karafka/cli/info.rb +7 -4
  24. data/lib/karafka/cli/install.rb +4 -3
  25. data/lib/karafka/cli/server.rb +3 -1
  26. data/lib/karafka/cli/worker.rb +2 -0
  27. data/lib/karafka/connection/config_adapter.rb +103 -0
  28. data/lib/karafka/connection/listener.rb +16 -12
  29. data/lib/karafka/connection/messages_consumer.rb +86 -0
  30. data/lib/karafka/connection/messages_processor.rb +74 -0
  31. data/lib/karafka/errors.rb +15 -29
  32. data/lib/karafka/fetcher.rb +10 -8
  33. data/lib/karafka/helpers/class_matcher.rb +2 -0
  34. data/lib/karafka/helpers/config_retriever.rb +46 -0
  35. data/lib/karafka/helpers/multi_delegator.rb +2 -0
  36. data/lib/karafka/loader.rb +4 -2
  37. data/lib/karafka/logger.rb +37 -36
  38. data/lib/karafka/monitor.rb +3 -1
  39. data/lib/karafka/params/interchanger.rb +2 -0
  40. data/lib/karafka/params/params.rb +34 -41
  41. data/lib/karafka/params/params_batch.rb +46 -0
  42. data/lib/karafka/parsers/json.rb +4 -2
  43. data/lib/karafka/patches/dry_configurable.rb +2 -0
  44. data/lib/karafka/process.rb +4 -2
  45. data/lib/karafka/responders/builder.rb +2 -0
  46. data/lib/karafka/responders/topic.rb +14 -6
  47. data/lib/karafka/routing/builder.rb +22 -59
  48. data/lib/karafka/routing/consumer_group.rb +54 -0
  49. data/lib/karafka/routing/mapper.rb +2 -0
  50. data/lib/karafka/routing/proxy.rb +37 -0
  51. data/lib/karafka/routing/router.rb +18 -16
  52. data/lib/karafka/routing/topic.rb +78 -0
  53. data/lib/karafka/schemas/config.rb +36 -0
  54. data/lib/karafka/schemas/consumer_group.rb +56 -0
  55. data/lib/karafka/schemas/responder_usage.rb +38 -0
  56. data/lib/karafka/server.rb +5 -3
  57. data/lib/karafka/setup/config.rb +79 -32
  58. data/lib/karafka/setup/configurators/base.rb +2 -0
  59. data/lib/karafka/setup/configurators/celluloid.rb +2 -0
  60. data/lib/karafka/setup/configurators/sidekiq.rb +2 -0
  61. data/lib/karafka/setup/configurators/water_drop.rb +15 -3
  62. data/lib/karafka/status.rb +2 -0
  63. data/lib/karafka/templates/app.rb.example +15 -5
  64. data/lib/karafka/templates/application_worker.rb.example +0 -6
  65. data/lib/karafka/version.rb +2 -1
  66. data/lib/karafka/workers/builder.rb +2 -0
  67. metadata +109 -60
  68. data/lib/karafka/cli/routes.rb +0 -36
  69. data/lib/karafka/connection/consumer.rb +0 -33
  70. data/lib/karafka/connection/message.rb +0 -17
  71. data/lib/karafka/connection/topic_consumer.rb +0 -94
  72. data/lib/karafka/responders/usage_validator.rb +0 -60
  73. data/lib/karafka/routing/route.rb +0 -113
  74. data/lib/karafka/setup/config_schema.rb +0 -44
  75. data/lib/karafka/setup/configurators/worker_glass.rb +0 -13
  76. data/lib/karafka/templates/config.ru.example +0 -13
@@ -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
- ::JSON.parse(content)
12
- rescue ::JSON::ParserError => e
13
+ ::Yajl::Parser.parse(content)
14
+ rescue ::Yajl::ParseError => e
13
15
  raise ::Karafka::Errors::ParserError, e
14
16
  end
15
17
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Karafka
2
4
  # Namespace for patches of external gems/libraries
3
5
  module Patches
@@ -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
- ).freeze
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
  # Responders namespace encapsulates all the internal responder implementation parts
3
5
  module Responders
@@ -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
- private
34
+ # @return [Boolean] was usage of this topic registered or not
35
+ def registered?
36
+ @options[:registered] == true
37
+ end
34
38
 
35
- # Checks topic name with the same regexp as routing
36
- # @raise [Karafka::Errors::InvalidTopicName] raised when topic name is invalid
37
- def validate!
38
- raise Errors::InvalidTopicName, name if Routing::Route::NAME_FORMAT !~ name
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
- # Routes builder used as a DSL layer for drawing and describing routes
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
- # draw do
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
- # Stores current route locally after it was built and validated
65
- def store!
66
- @current_route.build
67
- @current_route.validate!
68
-
69
- self << @current_route
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
- # Checks that among all routes a given attribute value is unique
76
- # @param attribute [Symbol] what routes attribute we want to check for uniqueness
77
- # @param error [Class] error class that should be raised when something is wrong
78
- def validate!(attribute, error)
79
- map = each_with_object({}) do |route, amounts|
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Karafka
2
4
  module Routing
3
5
  # Default routes mapper that does not remap things
@@ -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
- class Router
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
- route.controller.new.tap do |ctrl|
18
- Karafka::Routing::Route::ATTRIBUTES.each do |attr|
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 route
30
- App.routes.find { |route| route.topic == @topic } ||
31
- raise(Errors::NonMatchingRouteError, @topic)
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