karafka 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +0 -0
  4. data/.coditsu/ci.yml +3 -0
  5. data/.console_irbrc +1 -3
  6. data/.github/FUNDING.yml +3 -0
  7. data/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  8. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  9. data/.gitignore +1 -0
  10. data/.ruby-version +1 -1
  11. data/.travis.yml +35 -16
  12. data/CHANGELOG.md +151 -2
  13. data/CONTRIBUTING.md +6 -7
  14. data/Gemfile +3 -3
  15. data/Gemfile.lock +96 -70
  16. data/README.md +29 -23
  17. data/bin/karafka +1 -1
  18. data/certs/mensfeld.pem +25 -0
  19. data/config/errors.yml +38 -5
  20. data/karafka.gemspec +19 -10
  21. data/lib/karafka.rb +15 -12
  22. data/lib/karafka/app.rb +19 -18
  23. data/lib/karafka/attributes_map.rb +15 -14
  24. data/lib/karafka/backends/inline.rb +1 -2
  25. data/lib/karafka/base_consumer.rb +57 -0
  26. data/lib/karafka/base_responder.rb +72 -31
  27. data/lib/karafka/cli.rb +1 -1
  28. data/lib/karafka/cli/console.rb +11 -9
  29. data/lib/karafka/cli/flow.rb +0 -1
  30. data/lib/karafka/cli/info.rb +3 -1
  31. data/lib/karafka/cli/install.rb +29 -8
  32. data/lib/karafka/cli/server.rb +11 -7
  33. data/lib/karafka/code_reloader.rb +67 -0
  34. data/lib/karafka/connection/{config_adapter.rb → api_adapter.rb} +67 -24
  35. data/lib/karafka/connection/batch_delegator.rb +51 -0
  36. data/lib/karafka/connection/builder.rb +16 -0
  37. data/lib/karafka/connection/client.rb +117 -0
  38. data/lib/karafka/connection/listener.rb +37 -17
  39. data/lib/karafka/connection/message_delegator.rb +36 -0
  40. data/lib/karafka/consumers/callbacks.rb +71 -0
  41. data/lib/karafka/consumers/includer.rb +63 -0
  42. data/lib/karafka/consumers/metadata.rb +10 -0
  43. data/lib/karafka/consumers/responders.rb +24 -0
  44. data/lib/karafka/{controllers → consumers}/single_params.rb +3 -3
  45. data/lib/karafka/contracts.rb +10 -0
  46. data/lib/karafka/contracts/config.rb +21 -0
  47. data/lib/karafka/contracts/consumer_group.rb +206 -0
  48. data/lib/karafka/contracts/consumer_group_topic.rb +19 -0
  49. data/lib/karafka/contracts/responder_usage.rb +54 -0
  50. data/lib/karafka/contracts/server_cli_options.rb +29 -0
  51. data/lib/karafka/errors.rb +23 -15
  52. data/lib/karafka/fetcher.rb +6 -12
  53. data/lib/karafka/helpers/class_matcher.rb +19 -9
  54. data/lib/karafka/helpers/config_retriever.rb +3 -3
  55. data/lib/karafka/helpers/inflector.rb +26 -0
  56. data/lib/karafka/helpers/multi_delegator.rb +0 -1
  57. data/lib/karafka/instrumentation/logger.rb +57 -0
  58. data/lib/karafka/instrumentation/monitor.rb +70 -0
  59. data/lib/karafka/instrumentation/proctitle_listener.rb +36 -0
  60. data/lib/karafka/instrumentation/stdout_listener.rb +138 -0
  61. data/lib/karafka/params/builders/metadata.rb +33 -0
  62. data/lib/karafka/params/builders/params.rb +36 -0
  63. data/lib/karafka/params/builders/params_batch.rb +25 -0
  64. data/lib/karafka/params/metadata.rb +35 -0
  65. data/lib/karafka/params/params.rb +35 -95
  66. data/lib/karafka/params/params_batch.rb +38 -18
  67. data/lib/karafka/patches/ruby_kafka.rb +25 -12
  68. data/lib/karafka/persistence/client.rb +29 -0
  69. data/lib/karafka/persistence/consumers.rb +45 -0
  70. data/lib/karafka/persistence/topics.rb +48 -0
  71. data/lib/karafka/process.rb +5 -8
  72. data/lib/karafka/responders/builder.rb +15 -14
  73. data/lib/karafka/responders/topic.rb +6 -8
  74. data/lib/karafka/routing/builder.rb +37 -9
  75. data/lib/karafka/routing/consumer_group.rb +1 -1
  76. data/lib/karafka/routing/consumer_mapper.rb +10 -9
  77. data/lib/karafka/routing/proxy.rb +10 -1
  78. data/lib/karafka/routing/router.rb +1 -1
  79. data/lib/karafka/routing/topic.rb +8 -12
  80. data/lib/karafka/routing/topic_mapper.rb +16 -18
  81. data/lib/karafka/serialization/json/deserializer.rb +27 -0
  82. data/lib/karafka/serialization/json/serializer.rb +31 -0
  83. data/lib/karafka/server.rb +45 -24
  84. data/lib/karafka/setup/config.rb +95 -37
  85. data/lib/karafka/setup/configurators/water_drop.rb +12 -5
  86. data/lib/karafka/setup/dsl.rb +21 -0
  87. data/lib/karafka/status.rb +7 -3
  88. data/lib/karafka/templates/{application_controller.rb.example → application_consumer.rb.erb} +2 -2
  89. data/lib/karafka/templates/{application_responder.rb.example → application_responder.rb.erb} +0 -0
  90. data/lib/karafka/templates/karafka.rb.erb +92 -0
  91. data/lib/karafka/version.rb +1 -1
  92. metadata +126 -57
  93. metadata.gz.sig +0 -0
  94. data/.github/ISSUE_TEMPLATE.md +0 -2
  95. data/lib/karafka/base_controller.rb +0 -60
  96. data/lib/karafka/connection/consumer.rb +0 -121
  97. data/lib/karafka/connection/processor.rb +0 -61
  98. data/lib/karafka/controllers/callbacks.rb +0 -54
  99. data/lib/karafka/controllers/includer.rb +0 -51
  100. data/lib/karafka/controllers/responders.rb +0 -19
  101. data/lib/karafka/loader.rb +0 -29
  102. data/lib/karafka/logger.rb +0 -53
  103. data/lib/karafka/monitor.rb +0 -98
  104. data/lib/karafka/parsers/json.rb +0 -38
  105. data/lib/karafka/patches/dry_configurable.rb +0 -31
  106. data/lib/karafka/persistence/consumer.rb +0 -25
  107. data/lib/karafka/persistence/controller.rb +0 -38
  108. data/lib/karafka/schemas/config.rb +0 -21
  109. data/lib/karafka/schemas/consumer_group.rb +0 -65
  110. data/lib/karafka/schemas/consumer_group_topic.rb +0 -18
  111. data/lib/karafka/schemas/responder_usage.rb +0 -39
  112. data/lib/karafka/schemas/server_cli_options.rb +0 -43
  113. data/lib/karafka/setup/configurators/base.rb +0 -35
  114. data/lib/karafka/templates/karafka.rb.example +0 -41
Binary file
@@ -1,2 +0,0 @@
1
- <!-- Love karafka? Please consider supporting our collective:
2
- 👉 https://opencollective.com/karafka/donate -->
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Karafka module namespace
4
- module Karafka
5
- # Base controller from which all Karafka controllers should inherit
6
- class BaseController
7
- extend ActiveSupport::DescendantsTracker
8
-
9
- class << self
10
- attr_reader :topic
11
-
12
- # Assigns a topic to a controller and build up proper controller functionalities, so it can
13
- # cooperate with the topic settings
14
- # @param topic [Karafka::Routing::Topic]
15
- # @return [Karafka::Routing::Topic] assigned topic
16
- def topic=(topic)
17
- @topic = topic
18
- Controllers::Includer.call(self)
19
- end
20
- end
21
-
22
- # @return [Karafka::Routing::Topic] topic to which a given controller is subscribed
23
- def topic
24
- self.class.topic
25
- end
26
-
27
- # Creates lazy loaded params batch object
28
- # @note Until first params usage, it won't parse data at all
29
- # @param messages [Array<Kafka::FetchedMessage>, Array<Hash>] messages with raw
30
- # content (from Kafka) or messages inside a hash (from backend, etc)
31
- # @return [Karafka::Params::ParamsBatch] lazy loaded params batch
32
- def params_batch=(messages)
33
- @params_batch = Karafka::Params::ParamsBatch.new(messages, topic.parser)
34
- end
35
-
36
- # Executes the default controller flow.
37
- def call
38
- process
39
- end
40
-
41
- private
42
-
43
- # We make it private as it should be accesible only from the inside of a controller
44
- attr_reader :params_batch
45
-
46
- # @return [Karafka::Connection::Consumer] messages consumer that can be used to
47
- # commit manually offset or pause / stop consumer based on the business logic
48
- def consumer
49
- Persistence::Consumer.read
50
- end
51
-
52
- # Method that will perform business logic and on data received from Kafka (it will consume
53
- # the data)
54
- # @note This method needs bo be implemented in a subclass. We stub it here as a failover if
55
- # someone forgets about it or makes on with typo
56
- def consume
57
- raise NotImplementedError, 'Implement this in a subclass'
58
- end
59
- end
60
- end
@@ -1,121 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- module Connection
5
- # Class used as a wrapper around Ruby-Kafka to simplify additional
6
- # features that we provide/might provide in future and to hide the internal implementation
7
- class Consumer
8
- # Creates a queue consumer that will pull the data from Kafka
9
- # @param consumer_group [Karafka::Routing::ConsumerGroup] consumer group for which
10
- # we create a client
11
- # @return [Karafka::Connection::Consumer] group consumer that can subscribe to
12
- # multiple topics
13
- def initialize(consumer_group)
14
- @consumer_group = consumer_group
15
- Persistence::Consumer.write(self)
16
- end
17
-
18
- # Opens connection, gets messages and calls a block for each of the incoming messages
19
- # @yieldparam [Array<Kafka::FetchedMessage>] kafka fetched messages
20
- # @note This will yield with raw messages - no preprocessing or reformatting.
21
- def fetch_loop
22
- send(
23
- consumer_group.batch_fetching ? :consume_each_batch : :consume_each_message
24
- ) { |messages| yield(messages) }
25
- rescue Kafka::ProcessingError => e
26
- # If there was an error during consumption, we have to log it, pause current partition
27
- # and process other things
28
- Karafka.monitor.notice_error(self.class, e.cause)
29
- pause(e.topic, e.partition)
30
- retry
31
- # This is on purpose - see the notes for this method
32
- # rubocop:disable RescueException
33
- rescue Exception => e
34
- # rubocop:enable RescueException
35
- Karafka.monitor.notice_error(self.class, e)
36
- retry
37
- end
38
-
39
- # Gracefuly stops topic consumption
40
- # @note Stopping running consumers without a really important reason is not recommended
41
- # as until all the consumers are stopped, the server will keep running serving only
42
- # part of the messages
43
- def stop
44
- @kafka_consumer&.stop
45
- @kafka_consumer = nil
46
- end
47
-
48
- # Pauses fetching and consumption of a given topic partition
49
- # @param topic [String] topic that we want to pause
50
- # @param partition [Integer] number partition that we want to pause
51
- def pause(topic, partition)
52
- settings = ConfigAdapter.pausing(consumer_group)
53
- timeout = settings[:timeout]
54
- raise(Errors::InvalidPauseTimeout, timeout) unless timeout.positive?
55
- kafka_consumer.pause(topic, partition, settings)
56
- end
57
-
58
- # Marks a given message as consumed and commit the offsets
59
- # @note In opposite to ruby-kafka, we commit the offset for each manual marking to be sure
60
- # that offset commit happen asap in case of a crash
61
- # @param [Karafka::Params::Params] params message that we want to mark as processed
62
- def mark_as_consumed(params)
63
- kafka_consumer.mark_message_as_processed(params)
64
- # Trigger an immediate, blocking offset commit in order to minimize the risk of crashing
65
- # before the automatic triggers have kicked in.
66
- kafka_consumer.commit_offsets
67
- end
68
-
69
- private
70
-
71
- attr_reader :consumer_group
72
-
73
- # Consumes messages from Kafka in batches
74
- # @yieldparam [Array<Kafka::FetchedMessage>] kafka fetched messages
75
- def consume_each_batch
76
- kafka_consumer.each_batch(
77
- ConfigAdapter.consuming(consumer_group)
78
- ) do |batch|
79
- yield(batch.messages)
80
- end
81
- end
82
-
83
- # Consumes messages from Kafka one by one
84
- # @yieldparam [Array<Kafka::FetchedMessage>] kafka fetched messages
85
- def consume_each_message
86
- kafka_consumer.each_message(
87
- ConfigAdapter.consuming(consumer_group)
88
- ) do |message|
89
- # always yield an array of messages, so we have consistent API (always a batch)
90
- yield([message])
91
- end
92
- end
93
-
94
- # @return [Kafka::Consumer] returns a ready to consume Kafka consumer
95
- # that is set up to consume from topics of a given consumer group
96
- def kafka_consumer
97
- @kafka_consumer ||= kafka.consumer(
98
- ConfigAdapter.consumer(consumer_group)
99
- ).tap do |consumer|
100
- consumer_group.topics.each do |topic|
101
- consumer.subscribe(*ConfigAdapter.subscription(topic))
102
- end
103
- end
104
- rescue Kafka::ConnectionError
105
- # If we would not wait it would totally spam log file with failed
106
- # attempts if Kafka is down
107
- sleep(consumer_group.reconnect_timeout)
108
- # We don't log and just reraise - this will be logged
109
- # down the road
110
- raise
111
- end
112
-
113
- # @return [Kafka] returns a Kafka
114
- # @note We don't cache it internally because we cache kafka_consumer that uses kafka
115
- # object instance
116
- def kafka
117
- Kafka.new(ConfigAdapter.client(consumer_group))
118
- end
119
- end
120
- end
121
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- module Connection
5
- # Class that consumes messages for which we listen
6
- module Processor
7
- class << self
8
- # Processes messages (does something with them)
9
- # It will either schedule or run a proper controller action for messages
10
- # @note This should be looped to obtain a constant listening
11
- # @note We catch all the errors here, to make sure that none failures
12
- # for a given consumption will affect other consumed messages
13
- # If we wouldn't catch it, it would propagate up until killing the thread
14
- # @param group_id [String] group_id of a group from which a given message came
15
- # @param kafka_messages [Array<Kafka::FetchedMessage>] raw messages fetched from kafka
16
- def process(group_id, kafka_messages)
17
- # @note We always get messages by topic and partition so we can take topic from the
18
- # first one and it will be valid for all the messages
19
- # We map from incoming topic name, as it might be namespaced, etc.
20
- # @see topic_mapper internal docs
21
- mapped_topic_name = Karafka::App.config.topic_mapper.incoming(kafka_messages[0].topic)
22
- topic = Routing::Router.find("#{group_id}_#{mapped_topic_name}")
23
- controller = Persistence::Controller.fetch(topic, kafka_messages[0].partition) do
24
- topic.controller.new
25
- end
26
-
27
- # Depending on a case (persisted or not) we might use new controller instance per each
28
- # batch, or use the same instance for all of them (for implementing buffering, etc)
29
- send(
30
- topic.batch_consuming ? :process_batch : :process_each,
31
- controller,
32
- kafka_messages
33
- )
34
- end
35
-
36
- private
37
-
38
- # Processes whole batch in one request (all at once)
39
- # @param controller [Karafka::BaseController] base controller descendant
40
- # @param kafka_messages [Array<Kafka::FetchedMessage>] raw messages from kafka
41
- def process_batch(controller, kafka_messages)
42
- controller.params_batch = kafka_messages
43
- Karafka.monitor.notice(self, kafka_messages)
44
- controller.call
45
- end
46
-
47
- # Processes messages one by one (like with std http requests)
48
- # @param controller [Karafka::BaseController] base controller descendant
49
- # @param kafka_messages [Array<Kafka::FetchedMessage>] raw messages from kafka
50
- def process_each(controller, kafka_messages)
51
- kafka_messages.each do |kafka_message|
52
- # @note This is a simple trick - we just process one after another, but in order
53
- # not to handle everywhere both cases (single vs batch), we just "fake" batching with
54
- # a single message for each
55
- process_batch(controller, [kafka_message])
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- module Controllers
5
- # Additional callbacks that can be used to trigger some actions on certain moments like
6
- # manual offset management, committing or anything else outside of a standard messages flow
7
- # They are not included by default, as we don't want to provide functionalities that are
8
- # not required by users by default
9
- # Please refer to the wiki callbacks page for more details on how to use them
10
- module Callbacks
11
- # Types of events on which we run callbacks
12
- TYPES = %i[
13
- after_fetched
14
- after_poll
15
- before_poll
16
- before_stop
17
- ].freeze
18
-
19
- # Class methods needed to make callbacks run
20
- module ClassMethods
21
- TYPES.each do |type|
22
- # A Creates a callback wrapper
23
- # @param method_name [Symbol, String] method name or nil if we plan to provide a block
24
- # @yield A block with a code that should be executed before scheduling
25
- define_method type do |method_name = nil, &block|
26
- set_callback type, :before, method_name ? method_name : block
27
- end
28
- end
29
- end
30
-
31
- # @param controller_class [Class] controller class that we extend with callbacks
32
- def self.included(controller_class)
33
- controller_class.class_eval do
34
- extend ClassMethods
35
- include ActiveSupport::Callbacks
36
-
37
- # The call method is wrapped with a set of callbacks
38
- # We won't run process if any of the callbacks throw abort
39
- # @see http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-get_callbacks
40
- TYPES.each { |type| define_callbacks type }
41
- end
42
- end
43
-
44
- # Executes the default controller flow, runs callbacks and if not halted will call process
45
- # method of a proper backend. This is here because it interacts with the default Karafka
46
- # call flow and needs to be overwritten in order to support callbacks
47
- def call
48
- run_callbacks :after_fetched do
49
- process
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- # Additional functionalities for controllers
5
- module Controllers
6
- # Module used to inject functionalities into a given controller class, based on the controller
7
- # topic and its settings
8
- # We don't need all the behaviors in all the cases, so it is totally not worth having
9
- # everything in all the cases all the time
10
- module Includer
11
- class << self
12
- # @param controller_class [Class] controller class, that will get some functionalities
13
- # based on the topic under which it operates
14
- def call(controller_class)
15
- topic = controller_class.topic
16
-
17
- bind_backend(controller_class, topic)
18
- bind_params(controller_class, topic)
19
- bind_responders(controller_class, topic)
20
- end
21
-
22
- private
23
-
24
- # Figures out backend for a given controller class, based on the topic backend and
25
- # includes it into the controller class
26
- # @param controller_class [Class] controller class
27
- # @param topic [Karafka::Routing::Topic] topic of a controller class
28
- def bind_backend(controller_class, topic)
29
- backend = Kernel.const_get("::Karafka::Backends::#{topic.backend.to_s.capitalize}")
30
- controller_class.include backend
31
- end
32
-
33
- # Adds a single #params support for non batch processed topics
34
- # @param controller_class [Class] controller class
35
- # @param topic [Karafka::Routing::Topic] topic of a controller class
36
- def bind_params(controller_class, topic)
37
- return if topic.batch_consuming
38
- controller_class.include SingleParams
39
- end
40
-
41
- # Adds responders support for topics and controllers with responders defined for them
42
- # @param controller_class [Class] controller class
43
- # @param topic [Karafka::Routing::Topic] topic of a controller class
44
- def bind_responders(controller_class, topic)
45
- return unless topic.responder
46
- controller_class.include Responders
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- module Controllers
5
- # Feature that allows us to use responders flow in controller
6
- module Responders
7
- # Responds with given data using given responder. This allows us to have a similar way of
8
- # defining flows like synchronous protocols
9
- # @param data Anything we want to pass to responder based on which we want to trigger further
10
- # Kafka responding
11
- def respond_with(*data)
12
- Karafka.monitor.notice(self.class, data: data)
13
- # @note we build a new instance of responder each time, as a long running (persisted)
14
- # controllers can respond multiple times during the lifecycle
15
- topic.responder.new(topic.parser).call(*data)
16
- end
17
- end
18
- end
19
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- # Loader for requiring all the files in a proper order
5
- module Loader
6
- # Order in which we want to load app files
7
- DIRS = %w[
8
- config/initializers
9
- lib
10
- app
11
- ].freeze
12
-
13
- # Will load files in a proper order (based on DIRS)
14
- # @param [String] root path from which we want to start
15
- def self.load(root)
16
- DIRS.each do |dir|
17
- path = File.join(root, dir)
18
- next unless File.exist?(path)
19
- load!(path)
20
- end
21
- end
22
-
23
- # Requires all the ruby files from one path in a proper order
24
- # @param path [String] path (dir) from which we want to load ruby files in a proper order
25
- def self.load!(path)
26
- require_all(path)
27
- end
28
- end
29
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Karafka
4
- # Default logger for Event Delegator
5
- # @note It uses ::Logger features - providing basic logging
6
- class Logger < ::Logger
7
- include Singleton
8
-
9
- # Map containing informations about log level for given environment
10
- ENV_MAP = {
11
- 'production' => ::Logger::ERROR,
12
- 'test' => ::Logger::ERROR,
13
- 'development' => ::Logger::INFO,
14
- 'debug' => ::Logger::DEBUG,
15
- default: ::Logger::INFO
16
- }.freeze
17
-
18
- # Creates a new instance of logger ensuring that it has a place to write to
19
- def initialize(*_args)
20
- ensure_dir_exists
21
- super(target)
22
- self.level = ENV_MAP[Karafka.env] || ENV_MAP[:default]
23
- end
24
-
25
- private
26
-
27
- # @return [Karafka::Helpers::MultiDelegator] multi delegator instance
28
- # to which we will be writtng logs
29
- # We use this approach to log stuff to file and to the STDOUT at the same time
30
- def target
31
- Karafka::Helpers::MultiDelegator
32
- .delegate(:write, :close)
33
- .to(STDOUT, file)
34
- end
35
-
36
- # Makes sure the log directory exists
37
- def ensure_dir_exists
38
- dir = File.dirname(log_path)
39
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
40
- end
41
-
42
- # @return [Pathname] Path to a file to which we should log
43
- def log_path
44
- @log_path ||= Karafka::App.root.join("log/#{Karafka.env}.log")
45
- end
46
-
47
- # @return [File] file to which we want to write our logs
48
- # @note File is being opened in append mode ('a')
49
- def file
50
- @file ||= File.open(log_path, 'a')
51
- end
52
- end
53
- end