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.
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