karafka 1.4.13 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|