karafka 0.5.0

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