karafka 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +68 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +202 -0
  7. data/Gemfile +8 -0
  8. data/Gemfile.lock +216 -0
  9. data/MIT-LICENCE +18 -0
  10. data/README.md +831 -0
  11. data/Rakefile +17 -0
  12. data/bin/karafka +7 -0
  13. data/karafka.gemspec +34 -0
  14. data/lib/karafka.rb +73 -0
  15. data/lib/karafka/app.rb +45 -0
  16. data/lib/karafka/base_controller.rb +162 -0
  17. data/lib/karafka/base_responder.rb +118 -0
  18. data/lib/karafka/base_worker.rb +41 -0
  19. data/lib/karafka/capistrano.rb +2 -0
  20. data/lib/karafka/capistrano/karafka.cap +84 -0
  21. data/lib/karafka/cli.rb +52 -0
  22. data/lib/karafka/cli/base.rb +74 -0
  23. data/lib/karafka/cli/console.rb +23 -0
  24. data/lib/karafka/cli/flow.rb +46 -0
  25. data/lib/karafka/cli/info.rb +26 -0
  26. data/lib/karafka/cli/install.rb +45 -0
  27. data/lib/karafka/cli/routes.rb +39 -0
  28. data/lib/karafka/cli/server.rb +59 -0
  29. data/lib/karafka/cli/worker.rb +26 -0
  30. data/lib/karafka/connection/consumer.rb +29 -0
  31. data/lib/karafka/connection/listener.rb +54 -0
  32. data/lib/karafka/connection/message.rb +17 -0
  33. data/lib/karafka/connection/topic_consumer.rb +48 -0
  34. data/lib/karafka/errors.rb +50 -0
  35. data/lib/karafka/fetcher.rb +40 -0
  36. data/lib/karafka/helpers/class_matcher.rb +77 -0
  37. data/lib/karafka/helpers/multi_delegator.rb +31 -0
  38. data/lib/karafka/loader.rb +77 -0
  39. data/lib/karafka/logger.rb +52 -0
  40. data/lib/karafka/monitor.rb +82 -0
  41. data/lib/karafka/params/interchanger.rb +33 -0
  42. data/lib/karafka/params/params.rb +102 -0
  43. data/lib/karafka/patches/dry/configurable/config.rb +37 -0
  44. data/lib/karafka/process.rb +61 -0
  45. data/lib/karafka/responders/builder.rb +33 -0
  46. data/lib/karafka/responders/topic.rb +43 -0
  47. data/lib/karafka/responders/usage_validator.rb +59 -0
  48. data/lib/karafka/routing/builder.rb +89 -0
  49. data/lib/karafka/routing/route.rb +80 -0
  50. data/lib/karafka/routing/router.rb +38 -0
  51. data/lib/karafka/server.rb +53 -0
  52. data/lib/karafka/setup/config.rb +57 -0
  53. data/lib/karafka/setup/configurators/base.rb +33 -0
  54. data/lib/karafka/setup/configurators/celluloid.rb +20 -0
  55. data/lib/karafka/setup/configurators/sidekiq.rb +34 -0
  56. data/lib/karafka/setup/configurators/water_drop.rb +19 -0
  57. data/lib/karafka/setup/configurators/worker_glass.rb +13 -0
  58. data/lib/karafka/status.rb +23 -0
  59. data/lib/karafka/templates/app.rb.example +26 -0
  60. data/lib/karafka/templates/application_controller.rb.example +5 -0
  61. data/lib/karafka/templates/application_responder.rb.example +9 -0
  62. data/lib/karafka/templates/application_worker.rb.example +12 -0
  63. data/lib/karafka/templates/config.ru.example +13 -0
  64. data/lib/karafka/templates/sidekiq.yml.example +26 -0
  65. data/lib/karafka/version.rb +6 -0
  66. data/lib/karafka/workers/builder.rb +49 -0
  67. data/log/.gitkeep +0 -0
  68. metadata +267 -0
@@ -0,0 +1,59 @@
1
+ module Karafka
2
+ module Responders
3
+ # Usage validator checks if all the requirements related to responders topics were met
4
+ class UsageValidator
5
+ # @param registered_topics [Hash] Hash with registered topics objects from
6
+ # a given responder class under it's name key
7
+ # @param used_topics [Array<String>] Array with names of topics that we used in this
8
+ # responding process
9
+ # @return [Karafka::Responders::UsageValidator] responding flow usage validator
10
+ def initialize(registered_topics, used_topics)
11
+ @registered_topics = registered_topics
12
+ @used_topics = used_topics
13
+ end
14
+
15
+ # Validates the whole flow
16
+ # @raise [Karafka::Errors::UnregisteredTopic] raised when we used a topic that we didn't
17
+ # register using #topic method
18
+ # @raise [Karafka::Errors::TopicMultipleUsage] raised when we used a non multipleusage topic
19
+ # multiple times
20
+ # @raise [Karafka::Errors::UnusedResponderRequiredTopic] raised when we didn't use a topic
21
+ # that was defined as required to be used
22
+ def validate!
23
+ @used_topics.each do |used_topic|
24
+ validate_usage_of!(used_topic)
25
+ end
26
+
27
+ @registered_topics.each do |_name, registered_topic|
28
+ validate_requirements_of!(registered_topic)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Checks if a given used topic were used in a proper way
35
+ # @raise [Karafka::Errors::UnregisteredTopic] raised when we used a topic that we didn't
36
+ # register using #topic method
37
+ # @raise [Karafka::Errors::TopicMultipleUsage] raised when we used a non multipleusage topic
38
+ # multiple times
39
+ # @param used_topic [String] topic to which we've sent a message
40
+ def validate_usage_of!(used_topic)
41
+ raise(Errors::UnregisteredTopic, used_topic) unless @registered_topics[used_topic]
42
+ return if @registered_topics[used_topic].multiple_usage?
43
+ return if @used_topics.count(used_topic) < 2
44
+ raise(Errors::TopicMultipleUsage, used_topic)
45
+ end
46
+
47
+ # Checks if we met all the requirements for all the registered topics
48
+ # @raise [Karafka::Errors::UnusedResponderRequiredTopic] raised when we didn't use a topic
49
+ # that was defined as required to be used
50
+ # @param registered_topic [::Karafka::Responders::Topic] registered topic object
51
+ def validate_requirements_of!(registered_topic)
52
+ return unless registered_topic.required?
53
+ return if @used_topics.include?(registered_topic.name)
54
+
55
+ raise(Errors::UnusedResponderRequiredTopic, registered_topic.name)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,89 @@
1
+ module Karafka
2
+ module Routing
3
+ # Routes builder used as a DSL layer for drawing and describing routes
4
+ # @example Build a simple (most common) route
5
+ # draw do
6
+ # topic :new_videos do
7
+ # controller NewVideosController
8
+ # end
9
+ # end
10
+ class Builder < Array
11
+ include Singleton
12
+
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
+ ).freeze
22
+
23
+ # All those options should be set on the route level
24
+ ROUTE_OPTIONS.each do |option|
25
+ define_method option do |value|
26
+ @current_route.public_send :"#{option}=", value
27
+ end
28
+ end
29
+
30
+ # Creates a new route for a given topic and evalues provided block in builder context
31
+ # @param topic [String, Symbol] Kafka topic name
32
+ # @param block [Proc] block that will be evaluated in current context
33
+ # @note Creating new topic means creating a new route
34
+ # @example Define controller for a topic
35
+ # topic :xyz do
36
+ # controller XyzController
37
+ # end
38
+ def topic(topic, &block)
39
+ @current_route = Route.new
40
+ @current_route.topic = topic
41
+
42
+ instance_eval(&block)
43
+
44
+ store!
45
+ end
46
+
47
+ # Used to draw routes for Karafka
48
+ # @note After it is done drawing it will store and validate all the routes to make sure that
49
+ # they are correct and that there are no topic/group duplications (this is forbidden)
50
+ # @yield Evaluates provided block in a builder context so we can describe routes
51
+ # @example
52
+ # draw do
53
+ # topic :xyz do
54
+ # end
55
+ # end
56
+ def draw(&block)
57
+ instance_eval(&block)
58
+ end
59
+
60
+ private
61
+
62
+ # Stores current route locally after it was built and validated
63
+ def store!
64
+ @current_route.build
65
+ @current_route.validate!
66
+
67
+ self << @current_route
68
+
69
+ validate! :topic, Errors::DuplicatedTopicError
70
+ validate! :group, Errors::DuplicatedGroupError
71
+ end
72
+
73
+ # Checks that among all routes a given attribute value is unique
74
+ # @param attribute [Symbol] what routes attribute we want to check for uniqueness
75
+ # @param error [Class] error class that should be raised when something is wrong
76
+ def validate!(attribute, error)
77
+ map = each_with_object({}) do |route, amounts|
78
+ key = route.public_send(attribute)
79
+ amounts[key] = amounts[key].to_i + 1
80
+ amounts
81
+ end
82
+
83
+ wrong = map.find { |_, amount| amount > 1 }
84
+
85
+ raise error, wrong if wrong
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,80 @@
1
+ module Karafka
2
+ module Routing
3
+ # Class representing a single route (from topic to worker) with all additional features
4
+ # and elements. Single route contains descriptions of:
5
+ # - topic - Kafka topic name (required)
6
+ # - controller - Class of a controller that will handle messages from a given topic (required)
7
+ # - group - Kafka group that we want to use (optional)
8
+ # - worker - Which worker should handle the backend task (optional)
9
+ # - parser - What parsed do we want to use to unparse the data (optional)
10
+ # - interchanger - What interchanger to encode/decode data do we want to use (optional)
11
+ class Route
12
+ # Only ASCII alphanumeric characters and underscore and dash are allowed in topics and groups
13
+ NAME_FORMAT = /\A(\w|\-)+\z/
14
+
15
+ # Options that we can set per each route
16
+ attr_writer :group, :topic, :worker, :parser, :interchanger, :responder
17
+
18
+ # This we can get "directly" because it does not have any details, etc
19
+ attr_accessor :controller
20
+
21
+ # Initializes default values for all the options that support defaults if their values are
22
+ # not yet specified. This is need to be done (cannot be lazy loaded on first use) because
23
+ # everywhere except Karafka server command, those would not be initialized on time - for
24
+ # example for Sidekiq
25
+ def build
26
+ group
27
+ worker
28
+ parser
29
+ interchanger
30
+ responder
31
+ self
32
+ end
33
+
34
+ # @return [String] Kafka group name
35
+ # @note If group is not provided in a route, will build one based on the app name
36
+ # and the route topic (that is required)
37
+ def group
38
+ (@group ||= "#{Karafka::App.config.name.underscore}_#{topic}").to_s
39
+ end
40
+
41
+ # @return [String] route topic - this is the core esence of Kafka
42
+ def topic
43
+ @topic.to_s
44
+ end
45
+
46
+ # @return [Class] Class (not an instance) of a worker that should be used to schedule the
47
+ # background job
48
+ # @note If not provided - will be built based on the provided controller
49
+ def worker
50
+ @worker ||= Karafka::Workers::Builder.new(controller).build
51
+ end
52
+
53
+ # @return [Class, nil] Class (not an instance) of a responder that should respond from
54
+ # controller back to Kafka (usefull for piping dataflows)
55
+ def responder
56
+ @responder ||= Karafka::Responders::Builder.new(controller).build
57
+ end
58
+
59
+ # @return [Class] Parser class (not instance) that we want to use to unparse Kafka messages
60
+ # @note If not provided - will use JSON as default
61
+ def parser
62
+ @parser ||= JSON
63
+ end
64
+
65
+ # @return [Class] Interchanger class (not an instance) that we want to use to interchange
66
+ # params between Karafka server and Karafka background job
67
+ def interchanger
68
+ @interchanger ||= Karafka::Params::Interchanger
69
+ end
70
+
71
+ # Checks if topic and group have proper format (acceptable by Kafka)
72
+ # @raise [Karafka::Errors::InvalidTopicName] raised when topic name is invalid
73
+ # @raise [Karafka::Errors::InvalidGroupName] raised when group name is invalid
74
+ def validate!
75
+ raise Errors::InvalidTopicName, topic if NAME_FORMAT !~ topic
76
+ raise Errors::InvalidGroupName, group if NAME_FORMAT !~ group
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,38 @@
1
+ module Karafka
2
+ # Namespace for all elements related to requests routing
3
+ module Routing
4
+ # Karafka framework Router for routing incoming messages to proper controllers
5
+ # @note Since Kafka does not provide namespaces or modules for topics, they all have "flat"
6
+ # 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
+
14
+ # Builds a controller instance that should handle message from a given topic
15
+ # @return [Karafka::BaseController] base controller descendant instance object
16
+ def build
17
+ controller = route.controller.new
18
+ controller.topic = route.topic
19
+ controller.parser = route.parser
20
+ controller.worker = route.worker
21
+ controller.interchanger = route.interchanger
22
+ controller.responder = route.responder
23
+
24
+ controller
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Karafka::Routing::Route] proper route details
30
+ # @raise [Karafka::Topic::NonMatchingTopicError] raised if topic name does not match
31
+ # any route defined by user using routes.draw
32
+ def route
33
+ App.routes.find { |route| route.topic == @topic } ||
34
+ raise(Errors::NonMatchingRouteError, @topic)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,53 @@
1
+ module Karafka
2
+ # Karafka consuming server class
3
+ class Server
4
+ class << self
5
+ # We need to store reference to all the consumers in the main server thread,
6
+ # So we can have access to them later on and be able to stop them on exit
7
+ attr_reader :consumers
8
+
9
+ # Method which runs app
10
+ def run
11
+ @consumers = Concurrent::Array.new
12
+ bind_on_sigint
13
+ bind_on_sigquit
14
+ start_supervised
15
+ end
16
+
17
+ private
18
+
19
+ # @return [Karafka::Process] process wrapper instance used to catch system signal calls
20
+ def process
21
+ Karafka::Process.instance
22
+ end
23
+
24
+ # What should happen when we decide to quit with sigint
25
+ def bind_on_sigint
26
+ process.on_sigint do
27
+ Karafka::App.stop!
28
+ consumers.map(&:stop) if Karafka::App.running?
29
+ exit
30
+ end
31
+ end
32
+
33
+ # What should happen when we decide to quit with sigquit
34
+ def bind_on_sigquit
35
+ process.on_sigquit do
36
+ Karafka::App.stop!
37
+ consumers.map(&:stop) if Karafka::App.running?
38
+ exit
39
+ end
40
+ end
41
+
42
+ # Starts Karafka with a supervision
43
+ # @note We don't need to sleep because Karafka::Fetcher is locking and waiting to
44
+ # finish loop (and it won't happen until we explicitily want to stop)
45
+ def start_supervised
46
+ process.supervise do
47
+ Karafka::App.run!
48
+ Karafka::Fetcher.new.fetch_loop
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ module Karafka
2
+ # Module containing all Karafka setup related elements like configuration settings,
3
+ # config validations and configurators for external gems integration
4
+ module Setup
5
+ # Configurator for setting up all the framework details that are required to make it work
6
+ # @note If you want to do some configurations after all of this is done, please add to
7
+ # karafka/config a proper file (needs to inherit from Karafka::Setup::Configurators::Base
8
+ # and implement setup method) after that everything will happen automatically
9
+ # @note This config object allows to create a 1 level nestings (nodes) only. This should be
10
+ # enough and will still keep the code simple
11
+ # @see Karafka::Setup::Configurators::Base for more details about configurators api
12
+ class Config
13
+ extend Dry::Configurable
14
+
15
+ # Available settings
16
+ # option name [String] current app name - used to provide default Kafka groups namespaces
17
+ setting :name
18
+ # option logger [Instance] logger that we want to use
19
+ setting :logger, ::Karafka::Logger.instance
20
+ # option monitor [Instance] monitor that we will to use (defaults to Karafka::Monitor)
21
+ setting :monitor, ::Karafka::Monitor.instance
22
+ # option redis [Hash] redis options hash (url and optional parameters)
23
+ # Note that redis could be rewriten using nested options, but it is a sidekiq specific
24
+ # stuff and we don't want to touch it
25
+ setting :redis
26
+ # option kafka [Hash] - optional - kafka configuration options (hosts)
27
+ setting :kafka do
28
+ setting :hosts
29
+ end
30
+
31
+ # This is configured automatically, don't overwrite it!
32
+ # Each route requires separate thread, so number of threads should be equal to number
33
+ # of routes
34
+ setting :concurrency, -> { ::Karafka::App.routes.count }
35
+
36
+ class << self
37
+ # Configurating method
38
+ # @yield Runs a block of code providing a config singleton instance to it
39
+ # @yieldparam [Karafka::Setup::Config] Karafka config instance
40
+ def setup
41
+ configure do |config|
42
+ yield(config)
43
+ end
44
+ end
45
+
46
+ # Everything that should be initialized after the setup
47
+ # Components are in karafka/config directory and are all loaded one by one
48
+ # If you want to configure a next component, please add a proper file to config dir
49
+ def setup_components
50
+ Configurators::Base.descendants.each do |klass|
51
+ klass.new(config).setup
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ module Karafka
2
+ module Setup
3
+ # Configurators module is used to enclose all the external dependencies configurations
4
+ class Configurators
5
+ # Karafka has come components that it relies on (like Celluloid or Sidekiq)
6
+ # We need to configure all of them only when the framework was set up.
7
+ # Any class that descends from this one will be automatically invoked upon setup (after it)
8
+ # @example Configure an Example class
9
+ # class ExampleConfigurator < Base
10
+ # def setup
11
+ # ExampleClass.logger = Karafka.logger
12
+ # ExampleClass.redis = config.redis
13
+ # end
14
+ # end
15
+ class Base
16
+ extend ActiveSupport::DescendantsTracker
17
+
18
+ attr_reader :config
19
+
20
+ # @param config [Karafka::Config] config instance
21
+ # @return [Karafka::Config::Base] configurator for a given component
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ # This method needs to be implemented in a subclass
27
+ def setup
28
+ raise NotImplementedError
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ module Karafka
2
+ module Setup
3
+ class Configurators
4
+ # Class responsible for setting up Celluloid settings
5
+ class Celluloid < Base
6
+ # How many seconds should we wait for actors (listeners) before forcefully shutting them
7
+ SHUTDOWN_TIME = 30
8
+
9
+ # Sets up a Karafka logger as celluloid logger
10
+ def setup
11
+ ::Celluloid.logger = ::Karafka.logger
12
+ # This is just a precaution - it should automatically close the current
13
+ # connection and shutdown actor - but in case it didn't (hanged, etc)
14
+ # we will kill it after waiting for some time
15
+ ::Celluloid.shutdown_timeout = SHUTDOWN_TIME
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ module Karafka
2
+ module Setup
3
+ class Configurators
4
+ # Class to configure all the Sidekiq settings based on Karafka settings
5
+ class Sidekiq < Base
6
+ # Sets up sidekiq client and server
7
+ def setup
8
+ setup_sidekiq_client
9
+ setup_sidekiq_server
10
+ end
11
+
12
+ private
13
+
14
+ # Configure sidekiq client
15
+ def setup_sidekiq_client
16
+ ::Sidekiq.configure_client do |sidekiq_config|
17
+ sidekiq_config.redis = config.redis.to_h.merge(
18
+ size: config.concurrency
19
+ )
20
+ end
21
+ end
22
+
23
+ # Configure sidekiq setorrver
24
+ def setup_sidekiq_server
25
+ ::Sidekiq.configure_server do |sidekiq_config|
26
+ # We don't set size for the server - this will be set automatically based
27
+ # on the Sidekiq concurrency level (Sidekiq not Karafkas)
28
+ sidekiq_config.redis = config.redis.to_h
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end