karafka 1.4.13 → 2.0.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +3 -3
- data/.github/workflows/ci.yml +85 -30
- data/.ruby-version +1 -1
- data/CHANGELOG.md +268 -7
- data/CONTRIBUTING.md +10 -19
- data/Gemfile +6 -0
- data/Gemfile.lock +44 -87
- data/LICENSE +17 -0
- data/LICENSE-COMM +89 -0
- data/LICENSE-LGPL +165 -0
- data/README.md +44 -48
- data/bin/benchmarks +85 -0
- data/bin/create_token +22 -0
- data/bin/integrations +237 -0
- data/bin/karafka +4 -0
- data/bin/scenario +29 -0
- data/bin/stress_many +13 -0
- data/bin/stress_one +13 -0
- data/bin/wait_for_kafka +20 -0
- data/certs/karafka-pro.pem +11 -0
- data/config/errors.yml +55 -40
- data/docker-compose.yml +39 -3
- data/karafka.gemspec +11 -17
- data/lib/active_job/karafka.rb +21 -0
- data/lib/active_job/queue_adapters/karafka_adapter.rb +26 -0
- data/lib/karafka/active_job/consumer.rb +26 -0
- data/lib/karafka/active_job/dispatcher.rb +38 -0
- data/lib/karafka/active_job/job_extensions.rb +34 -0
- data/lib/karafka/active_job/job_options_contract.rb +21 -0
- data/lib/karafka/active_job/routing/extensions.rb +31 -0
- data/lib/karafka/app.rb +15 -20
- data/lib/karafka/base_consumer.rb +181 -31
- data/lib/karafka/cli/base.rb +4 -4
- data/lib/karafka/cli/info.rb +43 -9
- data/lib/karafka/cli/install.rb +19 -10
- data/lib/karafka/cli/server.rb +17 -42
- data/lib/karafka/cli.rb +4 -11
- data/lib/karafka/connection/client.rb +385 -90
- data/lib/karafka/connection/listener.rb +246 -38
- data/lib/karafka/connection/listeners_batch.rb +24 -0
- data/lib/karafka/connection/messages_buffer.rb +84 -0
- data/lib/karafka/connection/pauses_manager.rb +46 -0
- data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
- data/lib/karafka/connection/rebalance_manager.rb +78 -0
- data/lib/karafka/contracts/base.rb +17 -0
- data/lib/karafka/contracts/config.rb +88 -11
- data/lib/karafka/contracts/consumer_group.rb +21 -189
- data/lib/karafka/contracts/consumer_group_topic.rb +34 -11
- data/lib/karafka/contracts/server_cli_options.rb +19 -18
- data/lib/karafka/contracts.rb +1 -1
- data/lib/karafka/env.rb +46 -0
- data/lib/karafka/errors.rb +21 -21
- data/lib/karafka/helpers/async.rb +33 -0
- data/lib/karafka/helpers/colorize.rb +20 -0
- data/lib/karafka/helpers/multi_delegator.rb +2 -2
- data/lib/karafka/instrumentation/callbacks/error.rb +40 -0
- data/lib/karafka/instrumentation/callbacks/statistics.rb +41 -0
- data/lib/karafka/instrumentation/logger_listener.rb +164 -0
- data/lib/karafka/instrumentation/monitor.rb +13 -61
- data/lib/karafka/instrumentation/notifications.rb +52 -0
- data/lib/karafka/instrumentation/proctitle_listener.rb +3 -3
- data/lib/karafka/instrumentation/vendors/datadog/dashboard.json +1 -0
- data/lib/karafka/instrumentation/vendors/datadog/listener.rb +232 -0
- data/lib/karafka/instrumentation.rb +21 -0
- data/lib/karafka/licenser.rb +75 -0
- data/lib/karafka/messages/batch_metadata.rb +45 -0
- data/lib/karafka/messages/builders/batch_metadata.rb +40 -0
- data/lib/karafka/messages/builders/message.rb +39 -0
- data/lib/karafka/messages/builders/messages.rb +32 -0
- data/lib/karafka/{params/params.rb → messages/message.rb} +7 -12
- data/lib/karafka/messages/messages.rb +64 -0
- data/lib/karafka/{params → messages}/metadata.rb +4 -6
- data/lib/karafka/messages/seek.rb +9 -0
- data/lib/karafka/patches/rdkafka/consumer.rb +22 -0
- data/lib/karafka/pro/active_job/consumer.rb +46 -0
- data/lib/karafka/pro/active_job/dispatcher.rb +61 -0
- data/lib/karafka/pro/active_job/job_options_contract.rb +32 -0
- data/lib/karafka/pro/base_consumer.rb +82 -0
- data/lib/karafka/pro/contracts/base.rb +21 -0
- data/lib/karafka/pro/contracts/consumer_group.rb +34 -0
- data/lib/karafka/pro/contracts/consumer_group_topic.rb +33 -0
- data/lib/karafka/pro/loader.rb +76 -0
- data/lib/karafka/pro/performance_tracker.rb +80 -0
- data/lib/karafka/pro/processing/coordinator.rb +72 -0
- data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +32 -0
- data/lib/karafka/pro/processing/partitioner.rb +60 -0
- data/lib/karafka/pro/processing/scheduler.rb +56 -0
- data/lib/karafka/pro/routing/builder_extensions.rb +30 -0
- data/lib/karafka/pro/routing/topic_extensions.rb +38 -0
- data/lib/karafka/pro.rb +13 -0
- data/lib/karafka/process.rb +1 -0
- data/lib/karafka/processing/coordinator.rb +88 -0
- data/lib/karafka/processing/coordinators_buffer.rb +54 -0
- data/lib/karafka/processing/executor.rb +118 -0
- data/lib/karafka/processing/executors_buffer.rb +88 -0
- data/lib/karafka/processing/jobs/base.rb +51 -0
- data/lib/karafka/processing/jobs/consume.rb +42 -0
- data/lib/karafka/processing/jobs/revoked.rb +22 -0
- data/lib/karafka/processing/jobs/shutdown.rb +23 -0
- data/lib/karafka/processing/jobs_builder.rb +29 -0
- data/lib/karafka/processing/jobs_queue.rb +144 -0
- data/lib/karafka/processing/partitioner.rb +22 -0
- data/lib/karafka/processing/result.rb +29 -0
- data/lib/karafka/processing/scheduler.rb +22 -0
- data/lib/karafka/processing/worker.rb +88 -0
- data/lib/karafka/processing/workers_batch.rb +27 -0
- data/lib/karafka/railtie.rb +113 -0
- data/lib/karafka/routing/builder.rb +15 -24
- data/lib/karafka/routing/consumer_group.rb +11 -19
- data/lib/karafka/routing/consumer_mapper.rb +1 -2
- data/lib/karafka/routing/router.rb +1 -1
- data/lib/karafka/routing/subscription_group.rb +53 -0
- data/lib/karafka/routing/subscription_groups_builder.rb +53 -0
- data/lib/karafka/routing/topic.rb +61 -24
- data/lib/karafka/routing/topics.rb +38 -0
- data/lib/karafka/runner.rb +51 -0
- data/lib/karafka/serialization/json/deserializer.rb +6 -15
- data/lib/karafka/server.rb +67 -26
- data/lib/karafka/setup/config.rb +147 -175
- data/lib/karafka/status.rb +14 -5
- data/lib/karafka/templates/example_consumer.rb.erb +16 -0
- data/lib/karafka/templates/karafka.rb.erb +15 -51
- data/lib/karafka/time_trackers/base.rb +19 -0
- data/lib/karafka/time_trackers/pause.rb +92 -0
- data/lib/karafka/time_trackers/poll.rb +65 -0
- data/lib/karafka/version.rb +1 -1
- data/lib/karafka.rb +38 -17
- data.tar.gz.sig +0 -0
- metadata +118 -120
- metadata.gz.sig +0 -0
- data/MIT-LICENCE +0 -18
- data/lib/karafka/assignment_strategies/round_robin.rb +0 -13
- data/lib/karafka/attributes_map.rb +0 -63
- data/lib/karafka/backends/inline.rb +0 -16
- data/lib/karafka/base_responder.rb +0 -226
- data/lib/karafka/cli/flow.rb +0 -48
- data/lib/karafka/cli/missingno.rb +0 -19
- data/lib/karafka/code_reloader.rb +0 -67
- data/lib/karafka/connection/api_adapter.rb +0 -158
- data/lib/karafka/connection/batch_delegator.rb +0 -55
- data/lib/karafka/connection/builder.rb +0 -23
- data/lib/karafka/connection/message_delegator.rb +0 -36
- data/lib/karafka/consumers/batch_metadata.rb +0 -10
- data/lib/karafka/consumers/callbacks.rb +0 -71
- data/lib/karafka/consumers/includer.rb +0 -64
- data/lib/karafka/consumers/responders.rb +0 -24
- data/lib/karafka/consumers/single_params.rb +0 -15
- data/lib/karafka/contracts/responder_usage.rb +0 -54
- data/lib/karafka/fetcher.rb +0 -42
- data/lib/karafka/helpers/class_matcher.rb +0 -88
- data/lib/karafka/helpers/config_retriever.rb +0 -46
- data/lib/karafka/helpers/inflector.rb +0 -26
- data/lib/karafka/instrumentation/stdout_listener.rb +0 -140
- data/lib/karafka/params/batch_metadata.rb +0 -26
- data/lib/karafka/params/builders/batch_metadata.rb +0 -30
- data/lib/karafka/params/builders/params.rb +0 -38
- data/lib/karafka/params/builders/params_batch.rb +0 -25
- data/lib/karafka/params/params_batch.rb +0 -60
- data/lib/karafka/patches/ruby_kafka.rb +0 -47
- data/lib/karafka/persistence/client.rb +0 -29
- data/lib/karafka/persistence/consumers.rb +0 -45
- data/lib/karafka/persistence/topics.rb +0 -48
- data/lib/karafka/responders/builder.rb +0 -36
- data/lib/karafka/responders/topic.rb +0 -55
- data/lib/karafka/routing/topic_mapper.rb +0 -53
- data/lib/karafka/serialization/json/serializer.rb +0 -31
- data/lib/karafka/setup/configurators/water_drop.rb +0 -36
- data/lib/karafka/templates/application_responder.rb.erb +0 -11
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
# Workers are used to run jobs in separate threads.
|
|
6
|
+
# Workers are the main processing units of the Karafka framework.
|
|
7
|
+
#
|
|
8
|
+
# Each job runs in three stages:
|
|
9
|
+
# - prepare - here we can run any code that we would need to run blocking before we allow
|
|
10
|
+
# the job to run fully async (non blocking). This will always run in a blocking
|
|
11
|
+
# way and can be used to make sure all the resources and external dependencies
|
|
12
|
+
# are satisfied before going async.
|
|
13
|
+
#
|
|
14
|
+
# - call - actual processing logic that can run sync or async
|
|
15
|
+
#
|
|
16
|
+
# - teardown - it should include any code that we want to run after we executed the user
|
|
17
|
+
# code. This can be used to unlock certain resources or do other things that are
|
|
18
|
+
# not user code but need to run after user code base is executed.
|
|
19
|
+
class Worker
|
|
20
|
+
include Helpers::Async
|
|
21
|
+
|
|
22
|
+
# @return [String] id of this worker
|
|
23
|
+
attr_reader :id
|
|
24
|
+
|
|
25
|
+
# @param jobs_queue [JobsQueue]
|
|
26
|
+
# @return [Worker]
|
|
27
|
+
def initialize(jobs_queue)
|
|
28
|
+
@id = SecureRandom.uuid
|
|
29
|
+
@jobs_queue = jobs_queue
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Runs processing of jobs in a loop
|
|
35
|
+
# Stops when queue is closed.
|
|
36
|
+
def call
|
|
37
|
+
loop { break unless process }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Fetches a single job, processes it and marks as completed.
|
|
41
|
+
#
|
|
42
|
+
# @note We do not have error handling here, as no errors should propagate this far. If they
|
|
43
|
+
# do, it is a critical error and should bubble up.
|
|
44
|
+
#
|
|
45
|
+
# @note Upon closing the jobs queue, worker will close it's thread
|
|
46
|
+
def process
|
|
47
|
+
job = @jobs_queue.pop
|
|
48
|
+
|
|
49
|
+
if job
|
|
50
|
+
instrument_details = { caller: self, job: job, jobs_queue: @jobs_queue }
|
|
51
|
+
|
|
52
|
+
Karafka.monitor.instrument('worker.process', instrument_details)
|
|
53
|
+
|
|
54
|
+
Karafka.monitor.instrument('worker.processed', instrument_details) do
|
|
55
|
+
job.before_call
|
|
56
|
+
|
|
57
|
+
# If a job is marked as non blocking, we can run a tick in the job queue and if there
|
|
58
|
+
# are no other blocking factors, the job queue will be unlocked.
|
|
59
|
+
# If this does not run, all the things will be blocking and job queue won't allow to
|
|
60
|
+
# pass it until done.
|
|
61
|
+
@jobs_queue.tick(job.group_id) if job.non_blocking?
|
|
62
|
+
|
|
63
|
+
job.call
|
|
64
|
+
|
|
65
|
+
job.after_call
|
|
66
|
+
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
# We signal critical exceptions, notify and do not allow worker to fail
|
|
73
|
+
# rubocop:disable Lint/RescueException
|
|
74
|
+
rescue Exception => e
|
|
75
|
+
# rubocop:enable Lint/RescueException
|
|
76
|
+
Karafka.monitor.instrument(
|
|
77
|
+
'error.occurred',
|
|
78
|
+
caller: self,
|
|
79
|
+
error: e,
|
|
80
|
+
type: 'worker.process.error'
|
|
81
|
+
)
|
|
82
|
+
ensure
|
|
83
|
+
# job can be nil when the queue is being closed
|
|
84
|
+
@jobs_queue.complete(job) if job
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Processing
|
|
5
|
+
# Abstraction layer around workers batch.
|
|
6
|
+
class WorkersBatch
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
# @param jobs_queue [JobsQueue]
|
|
10
|
+
# @return [WorkersBatch]
|
|
11
|
+
def initialize(jobs_queue)
|
|
12
|
+
@batch = Array.new(App.config.concurrency) { Processing::Worker.new(jobs_queue) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Iterates over available workers and yields each worker
|
|
16
|
+
# @param block [Proc] block we want to run
|
|
17
|
+
def each(&block)
|
|
18
|
+
@batch.each(&block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Integer] number of workers in the batch
|
|
22
|
+
def size
|
|
23
|
+
@batch.size
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file contains Railtie for auto-configuration
|
|
4
|
+
|
|
5
|
+
rails = false
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require 'rails'
|
|
9
|
+
|
|
10
|
+
rails = true
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# Without defining this in any way, Zeitwerk ain't happy so we do it that way
|
|
13
|
+
module Karafka
|
|
14
|
+
class Railtie
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if rails
|
|
20
|
+
# Load Karafka
|
|
21
|
+
require 'karafka'
|
|
22
|
+
|
|
23
|
+
# Load ActiveJob adapter
|
|
24
|
+
require 'active_job/karafka'
|
|
25
|
+
|
|
26
|
+
# Setup env if configured (may be configured later by .net, etc)
|
|
27
|
+
ENV['KARAFKA_ENV'] ||= ENV['RAILS_ENV'] if ENV.key?('RAILS_ENV')
|
|
28
|
+
|
|
29
|
+
module Karafka
|
|
30
|
+
# Railtie for setting up Rails integration
|
|
31
|
+
class Railtie < Rails::Railtie
|
|
32
|
+
railtie_name :karafka
|
|
33
|
+
|
|
34
|
+
initializer 'karafka.active_job_integration' do
|
|
35
|
+
ActiveSupport.on_load(:active_job) do
|
|
36
|
+
# Extend ActiveJob with some Karafka specific ActiveJob magic
|
|
37
|
+
extend ::Karafka::ActiveJob::JobExtensions
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# This lines will make Karafka print to stdout like puma or unicorn when we run karafka
|
|
42
|
+
# server + will support code reloading with each fetched loop. We do it only for karafka
|
|
43
|
+
# based commands as Rails processes and console will have it enabled already
|
|
44
|
+
initializer 'karafka.configure_rails_logger' do
|
|
45
|
+
# Make Karafka use Rails logger
|
|
46
|
+
::Karafka::App.config.logger = Rails.logger
|
|
47
|
+
|
|
48
|
+
next unless Rails.env.development?
|
|
49
|
+
next unless ENV.key?('KARAFKA_CLI')
|
|
50
|
+
|
|
51
|
+
Rails.logger.extend(
|
|
52
|
+
ActiveSupport::Logger.broadcast(
|
|
53
|
+
ActiveSupport::Logger.new($stdout)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
initializer 'karafka.configure_rails_auto_load_paths' do |app|
|
|
59
|
+
# Consumers should autoload by default in the Rails app so they are visible
|
|
60
|
+
app.config.autoload_paths += %w[app/consumers]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
initializer 'karafka.configure_rails_code_reloader' do
|
|
64
|
+
# There are components that won't work with older Rails version, so we check it and
|
|
65
|
+
# provide a failover
|
|
66
|
+
rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
|
|
67
|
+
|
|
68
|
+
next unless Rails.env.development?
|
|
69
|
+
next unless ENV.key?('KARAFKA_CLI')
|
|
70
|
+
next unless rails6plus
|
|
71
|
+
|
|
72
|
+
# We can have many listeners, but it does not matter in which we will reload the code
|
|
73
|
+
# as long as all the consumers will be re-created as Rails reload is thread-safe
|
|
74
|
+
::Karafka::App.monitor.subscribe('connection.listener.fetch_loop') do
|
|
75
|
+
# Reload code each time there is a change in the code
|
|
76
|
+
next unless Rails.application.reloaders.any?(&:updated?)
|
|
77
|
+
|
|
78
|
+
Rails.application.reloader.reload!
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
initializer 'karafka.require_karafka_boot_file' do |app|
|
|
83
|
+
rails6plus = Rails.gem_version >= Gem::Version.new('6.0.0')
|
|
84
|
+
|
|
85
|
+
# If the boot file location is set to "false", we should not raise an exception and we
|
|
86
|
+
# should just not load karafka stuff. Setting this explicitly to false indicates, that
|
|
87
|
+
# karafka is part of the supply chain but it is not a first class citizen of a given
|
|
88
|
+
# system (may be just a dependency of a dependency), thus railtie should not kick in to
|
|
89
|
+
# load the non-existing boot file
|
|
90
|
+
next if Karafka.boot_file.to_s == 'false'
|
|
91
|
+
|
|
92
|
+
karafka_boot_file = Rails.root.join(Karafka.boot_file.to_s).to_s
|
|
93
|
+
|
|
94
|
+
# Provide more comprehensive error for when no boot file
|
|
95
|
+
unless File.exist?(karafka_boot_file)
|
|
96
|
+
raise(Karafka::Errors::MissingBootFileError, karafka_boot_file)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if rails6plus
|
|
100
|
+
app.reloader.to_prepare do
|
|
101
|
+
# Load Karafka boot file, so it can be used in Rails server context
|
|
102
|
+
require karafka_boot_file
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
# Load Karafka main setup for older Rails versions
|
|
106
|
+
app.config.after_initialize do
|
|
107
|
+
require karafka_boot_file
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -10,14 +10,9 @@ module Karafka
|
|
|
10
10
|
# end
|
|
11
11
|
# end
|
|
12
12
|
class Builder < Concurrent::Array
|
|
13
|
-
# Consumer group consistency checking contract
|
|
14
|
-
CONTRACT = Karafka::Contracts::ConsumerGroup.new.freeze
|
|
15
|
-
|
|
16
|
-
private_constant :CONTRACT
|
|
17
|
-
|
|
18
13
|
def initialize
|
|
19
|
-
super
|
|
20
14
|
@draws = Concurrent::Array.new
|
|
15
|
+
super
|
|
21
16
|
end
|
|
22
17
|
|
|
23
18
|
# Used to draw routes for Karafka
|
|
@@ -38,11 +33,7 @@ module Karafka
|
|
|
38
33
|
instance_eval(&block)
|
|
39
34
|
|
|
40
35
|
each do |consumer_group|
|
|
41
|
-
|
|
42
|
-
validation_result = CONTRACT.call(hashed_group)
|
|
43
|
-
next if validation_result.success?
|
|
44
|
-
|
|
45
|
-
raise Errors::InvalidConfigurationError, validation_result.errors.to_h
|
|
36
|
+
Contracts::ConsumerGroup.new.validate!(consumer_group.to_h)
|
|
46
37
|
end
|
|
47
38
|
end
|
|
48
39
|
|
|
@@ -59,30 +50,30 @@ module Karafka
|
|
|
59
50
|
super
|
|
60
51
|
end
|
|
61
52
|
|
|
62
|
-
# Redraws all the routes for the in-process code reloading.
|
|
63
|
-
# @note This won't allow registration of new topics without process restart but will trigger
|
|
64
|
-
# cache invalidation so all the classes, etc are re-fetched after code reload
|
|
65
|
-
def reload
|
|
66
|
-
draws = @draws.dup
|
|
67
|
-
clear
|
|
68
|
-
draws.each { |block| draw(&block) }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
53
|
private
|
|
72
54
|
|
|
73
55
|
# Builds and saves given consumer group
|
|
74
56
|
# @param group_id [String, Symbol] name for consumer group
|
|
75
57
|
# @param block [Proc] proc that should be executed in the proxy context
|
|
76
58
|
def consumer_group(group_id, &block)
|
|
77
|
-
consumer_group =
|
|
78
|
-
|
|
59
|
+
consumer_group = find { |cg| cg.name == group_id.to_s }
|
|
60
|
+
|
|
61
|
+
if consumer_group
|
|
62
|
+
Proxy.new(consumer_group, &block).target
|
|
63
|
+
else
|
|
64
|
+
consumer_group = ConsumerGroup.new(group_id.to_s)
|
|
65
|
+
self << Proxy.new(consumer_group, &block).target
|
|
66
|
+
end
|
|
79
67
|
end
|
|
80
68
|
|
|
69
|
+
# In case we use simple style of routing, all topics will be assigned to the same consumer
|
|
70
|
+
# group that will be based on the client_id
|
|
71
|
+
#
|
|
81
72
|
# @param topic_name [String, Symbol] name of a topic from which we want to consumer
|
|
82
73
|
# @param block [Proc] proc we want to evaluate in the topic context
|
|
83
74
|
def topic(topic_name, &block)
|
|
84
|
-
consumer_group(
|
|
85
|
-
topic(topic_name, &block)
|
|
75
|
+
consumer_group('app') do
|
|
76
|
+
topic(topic_name, &block)
|
|
86
77
|
end
|
|
87
78
|
end
|
|
88
79
|
end
|
|
@@ -5,14 +5,10 @@ module Karafka
|
|
|
5
5
|
# Object used to describe a single consumer group that is going to subscribe to
|
|
6
6
|
# given topics
|
|
7
7
|
# It is a part of Karafka's DSL
|
|
8
|
+
# @note A single consumer group represents Kafka consumer group, but it may not match 1:1 with
|
|
9
|
+
# subscription groups. There can be more subscription groups than consumer groups
|
|
8
10
|
class ConsumerGroup
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
attr_reader(
|
|
12
|
-
:topics,
|
|
13
|
-
:id,
|
|
14
|
-
:name
|
|
15
|
-
)
|
|
11
|
+
attr_reader :id, :topics, :name
|
|
16
12
|
|
|
17
13
|
# @param name [String, Symbol] raw name of this consumer group. Raw means, that it does not
|
|
18
14
|
# yet have an application client_id namespace, this will be added here by default.
|
|
@@ -21,7 +17,7 @@ module Karafka
|
|
|
21
17
|
def initialize(name)
|
|
22
18
|
@name = name
|
|
23
19
|
@id = Karafka::App.config.consumer_mapper.call(name)
|
|
24
|
-
@topics = []
|
|
20
|
+
@topics = Topics.new([])
|
|
25
21
|
end
|
|
26
22
|
|
|
27
23
|
# @return [Boolean] true if this consumer group should be active in our current process
|
|
@@ -35,28 +31,24 @@ module Karafka
|
|
|
35
31
|
# @return [Karafka::Routing::Topic] newly built topic instance
|
|
36
32
|
def topic=(name, &block)
|
|
37
33
|
topic = Topic.new(name, self)
|
|
38
|
-
@topics << Proxy.new(topic, &block).target
|
|
34
|
+
@topics << Proxy.new(topic, &block).target
|
|
39
35
|
@topics.last
|
|
40
36
|
end
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
# @return [Array<Routing::SubscriptionGroup>] all the subscription groups build based on
|
|
39
|
+
# the consumer group topics
|
|
40
|
+
def subscription_groups
|
|
41
|
+
App.config.internal.routing.subscription_groups_builder.call(topics)
|
|
44
42
|
end
|
|
45
43
|
|
|
46
44
|
# Hashed version of consumer group that can be used for validation purposes
|
|
47
45
|
# @return [Hash] hash with consumer group attributes including serialized to hash
|
|
48
46
|
# topics inside of it.
|
|
49
47
|
def to_h
|
|
50
|
-
|
|
48
|
+
{
|
|
51
49
|
topics: topics.map(&:to_h),
|
|
52
50
|
id: id
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
Karafka::AttributesMap.consumer_group.each do |attribute|
|
|
56
|
-
result[attribute] = public_send(attribute)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
result
|
|
51
|
+
}.freeze
|
|
60
52
|
end
|
|
61
53
|
end
|
|
62
54
|
end
|
|
@@ -26,8 +26,7 @@ module Karafka
|
|
|
26
26
|
# @param raw_consumer_group_name [String, Symbol] string or symbolized consumer group name
|
|
27
27
|
# @return [String] remapped final consumer group name
|
|
28
28
|
def call(raw_consumer_group_name)
|
|
29
|
-
|
|
30
|
-
"#{client_name}_#{raw_consumer_group_name}"
|
|
29
|
+
"#{Karafka::App.config.client_id}_#{raw_consumer_group_name}"
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
end
|
|
@@ -10,7 +10,7 @@ module Karafka
|
|
|
10
10
|
# Find a proper topic based on full topic id
|
|
11
11
|
# @param topic_id [String] proper topic id (already mapped, etc) for which we want to find
|
|
12
12
|
# routing topic
|
|
13
|
-
# @return [Karafka::Routing::
|
|
13
|
+
# @return [Karafka::Routing::Topic] proper route details
|
|
14
14
|
# @raise [Karafka::Topic::NonMatchingTopicError] raised if topic name does not match
|
|
15
15
|
# any route defined by user using routes.draw
|
|
16
16
|
def find(topic_id)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Routing
|
|
5
|
+
# Object representing a set of single consumer group topics that can be subscribed together
|
|
6
|
+
# with one connection.
|
|
7
|
+
#
|
|
8
|
+
# @note One subscription group will always belong to one consumer group, but one consumer
|
|
9
|
+
# group can have multiple subscription groups.
|
|
10
|
+
class SubscriptionGroup
|
|
11
|
+
attr_reader :id, :topics
|
|
12
|
+
|
|
13
|
+
# @param topics [Karafka::Routing::Topics] all the topics that share the same key settings
|
|
14
|
+
# @return [SubscriptionGroup] built subscription group
|
|
15
|
+
def initialize(topics)
|
|
16
|
+
@id = SecureRandom.uuid
|
|
17
|
+
@topics = topics
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String] consumer group id
|
|
22
|
+
def consumer_group_id
|
|
23
|
+
kafka[:'group.id']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Integer] max messages fetched in a single go
|
|
27
|
+
def max_messages
|
|
28
|
+
@topics.first.max_messages
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Integer] max milliseconds we can wait for incoming messages
|
|
32
|
+
def max_wait_time
|
|
33
|
+
@topics.first.max_wait_time
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Hash] kafka settings are a bit special. They are exactly the same for all of the
|
|
37
|
+
# topics but they lack the group.id (unless explicitly) provided. To make it compatible
|
|
38
|
+
# with our routing engine, we inject it before it will go to the consumer
|
|
39
|
+
def kafka
|
|
40
|
+
kafka = @topics.first.kafka.dup
|
|
41
|
+
|
|
42
|
+
kafka[:'client.id'] ||= Karafka::App.config.client_id
|
|
43
|
+
kafka[:'group.id'] ||= @topics.first.consumer_group.id
|
|
44
|
+
kafka[:'auto.offset.reset'] ||= @topics.first.initial_offset
|
|
45
|
+
# Karafka manages the offsets based on the processing state, thus we do not rely on the
|
|
46
|
+
# rdkafka offset auto-storing
|
|
47
|
+
kafka[:'enable.auto.offset.store'] = false
|
|
48
|
+
kafka.freeze
|
|
49
|
+
kafka
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Routing
|
|
5
|
+
# rdkafka allows us to group topics subscriptions when they have same settings.
|
|
6
|
+
# This builder groups topics from a single consumer group into subscription groups that can be
|
|
7
|
+
# subscribed with one rdkafka connection.
|
|
8
|
+
# This way we save resources as having several rdkafka consumers under the hood is not the
|
|
9
|
+
# cheapest thing in a bigger system.
|
|
10
|
+
#
|
|
11
|
+
# In general, if we can, we try to subscribe to as many topics with one rdkafka connection as
|
|
12
|
+
# possible, but if not possible, we divide.
|
|
13
|
+
class SubscriptionGroupsBuilder
|
|
14
|
+
# Keys used to build up a hash for subscription groups distribution.
|
|
15
|
+
# In order to be able to use the same rdkafka connection for several topics, those keys need
|
|
16
|
+
# to have same values.
|
|
17
|
+
DISTRIBUTION_KEYS = %i[
|
|
18
|
+
kafka
|
|
19
|
+
max_messages
|
|
20
|
+
max_wait_time
|
|
21
|
+
initial_offset
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
private_constant :DISTRIBUTION_KEYS
|
|
25
|
+
|
|
26
|
+
# @param topics [Karafka::Routing::Topics] all the topics based on which we want to build
|
|
27
|
+
# subscription groups
|
|
28
|
+
# @return [Array<SubscriptionGroup>] all subscription groups we need in separate threads
|
|
29
|
+
def call(topics)
|
|
30
|
+
topics
|
|
31
|
+
.map { |topic| [checksum(topic), topic] }
|
|
32
|
+
.group_by(&:first)
|
|
33
|
+
.values
|
|
34
|
+
.map { |value| value.map(&:last) }
|
|
35
|
+
.map { |topics_array| Routing::Topics.new(topics_array) }
|
|
36
|
+
.map { |grouped_topics| SubscriptionGroup.new(grouped_topics) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @param topic [Karafka::Routing::Topic] topic for which we compute the grouping checksum
|
|
42
|
+
# @return [Integer] checksum that we can use to check if topics have the same set of
|
|
43
|
+
# settings based on which we group
|
|
44
|
+
def checksum(topic)
|
|
45
|
+
accu = {}
|
|
46
|
+
|
|
47
|
+
DISTRIBUTION_KEYS.each { |key| accu[key] = topic.public_send(key) }
|
|
48
|
+
|
|
49
|
+
accu.hash
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module Karafka
|
|
4
4
|
module Routing
|
|
5
|
-
# Topic stores all the details on how we should interact with Kafka given topic
|
|
5
|
+
# Topic stores all the details on how we should interact with Kafka given topic.
|
|
6
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
|
|
7
|
+
# It is a part of Karafka's DSL.
|
|
8
8
|
class Topic
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
attr_reader :id, :name, :consumer_group
|
|
10
|
+
attr_writer :consumer
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# Attributes we can inherit from the root unless they were defined on this level
|
|
13
|
+
INHERITABLE_ATTRIBUTES = %i[
|
|
14
|
+
kafka
|
|
15
|
+
deserializer
|
|
16
|
+
manual_offset_management
|
|
17
|
+
max_messages
|
|
18
|
+
max_wait_time
|
|
19
|
+
initial_offset
|
|
20
|
+
].freeze
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
private_constant :INHERITABLE_ATTRIBUTES
|
|
16
23
|
|
|
17
24
|
# @param [String, Symbol] name of a topic on which we want to listen
|
|
18
25
|
# @param consumer_group [Karafka::Routing::ConsumerGroup] owning consumer group of this topic
|
|
@@ -22,40 +29,70 @@ module Karafka
|
|
|
22
29
|
@attributes = {}
|
|
23
30
|
# @note We use identifier related to the consumer group that owns a topic, because from
|
|
24
31
|
# Karafka 0.6 we can handle multiple Kafka instances with the same process and we can
|
|
25
|
-
# have same topic name across multiple
|
|
32
|
+
# have same topic name across multiple consumer groups
|
|
26
33
|
@id = "#{consumer_group.id}_#{@name}"
|
|
27
34
|
end
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
INHERITABLE_ATTRIBUTES.each do |attribute|
|
|
37
|
+
attr_writer attribute
|
|
38
|
+
|
|
39
|
+
define_method attribute do
|
|
40
|
+
current_value = instance_variable_get(:"@#{attribute}")
|
|
41
|
+
|
|
42
|
+
return current_value unless current_value.nil?
|
|
43
|
+
|
|
44
|
+
value = Karafka::App.config.send(attribute)
|
|
45
|
+
|
|
46
|
+
instance_variable_set(:"@#{attribute}", value)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Class] consumer class that we should use
|
|
51
|
+
def consumer
|
|
52
|
+
if Karafka::App.config.consumer_persistence
|
|
53
|
+
# When persistence of consumers is on, no need to reload them
|
|
54
|
+
@consumer
|
|
55
|
+
else
|
|
56
|
+
# In order to support code reload without having to change the topic api, we re-fetch the
|
|
57
|
+
# class of a consumer based on its class name. This will support all the cases where the
|
|
58
|
+
# consumer class is defined with a name. It won't support code reload for anonymous
|
|
59
|
+
# consumer classes, but this is an edge case
|
|
60
|
+
begin
|
|
61
|
+
::Object.const_get(@consumer.to_s)
|
|
62
|
+
rescue NameError
|
|
63
|
+
# It will only fail if the in case of anonymous classes
|
|
64
|
+
@consumer
|
|
65
|
+
end
|
|
66
|
+
end
|
|
36
67
|
end
|
|
37
68
|
|
|
38
|
-
# @return [Class
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
# @return [Class] consumer class that we should use
|
|
70
|
+
# @note This is just an alias to the `#consumer` method. We however want to use it internally
|
|
71
|
+
# instead of referencing the `#consumer`. We use this to indicate that this method returns
|
|
72
|
+
# class and not an instance. In the routing we want to keep the `#consumer Consumer`
|
|
73
|
+
# routing syntax, but for references outside, we should use this one.
|
|
74
|
+
def consumer_class
|
|
75
|
+
consumer
|
|
42
76
|
end
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
78
|
+
# @return [Boolean] true if this topic offset is handled by the end user
|
|
79
|
+
def manual_offset_management?
|
|
80
|
+
manual_offset_management
|
|
46
81
|
end
|
|
47
82
|
|
|
48
83
|
# @return [Hash] hash with all the topic attributes
|
|
49
84
|
# @note This is being used when we validate the consumer_group and its topics
|
|
50
85
|
def to_h
|
|
51
|
-
map =
|
|
86
|
+
map = INHERITABLE_ATTRIBUTES.map do |attribute|
|
|
52
87
|
[attribute, public_send(attribute)]
|
|
53
88
|
end
|
|
54
89
|
|
|
55
90
|
Hash[map].merge!(
|
|
56
91
|
id: id,
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
name: name,
|
|
93
|
+
consumer: consumer,
|
|
94
|
+
consumer_group_id: consumer_group.id
|
|
95
|
+
).freeze
|
|
59
96
|
end
|
|
60
97
|
end
|
|
61
98
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Karafka
|
|
6
|
+
module Routing
|
|
7
|
+
# Abstraction layer on top of groups of topics
|
|
8
|
+
class Topics
|
|
9
|
+
include Enumerable
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
def_delegators :@accumulator, :[], :size, :empty?, :last, :<<
|
|
13
|
+
|
|
14
|
+
# @param topics_array [Array<Karafka::Routing::Topic>] array with topics
|
|
15
|
+
def initialize(topics_array)
|
|
16
|
+
@accumulator = topics_array.dup
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Yields each topic
|
|
20
|
+
#
|
|
21
|
+
# @param [Proc] block we want to yield with on each topic
|
|
22
|
+
def each(&block)
|
|
23
|
+
@accumulator.each(&block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Finds topic by its name
|
|
27
|
+
#
|
|
28
|
+
# @param topic_name [String] topic name
|
|
29
|
+
# @return [Karafka::Routing::Topic]
|
|
30
|
+
# @raise [Karafka::Errors::TopicNotFoundError] this should never happen. If you see it,
|
|
31
|
+
# please create an issue.
|
|
32
|
+
def find(topic_name)
|
|
33
|
+
@accumulator.find { |topic| topic.name == topic_name } ||
|
|
34
|
+
raise(Karafka::Errors::TopicNotFoundError, topic_name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|