karafka 2.0.0.alpha6 → 2.0.0.beta3
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 +0 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -2
- data/Gemfile.lock +9 -9
- data/bin/integrations +36 -14
- data/bin/scenario +29 -0
- data/config/errors.yml +1 -0
- data/docker-compose.yml +3 -0
- data/karafka.gemspec +1 -1
- data/lib/active_job/karafka.rb +2 -2
- data/lib/karafka/active_job/routing/extensions.rb +31 -0
- data/lib/karafka/base_consumer.rb +74 -6
- data/lib/karafka/connection/client.rb +39 -16
- data/lib/karafka/connection/listener.rb +103 -34
- data/lib/karafka/connection/listeners_batch.rb +24 -0
- data/lib/karafka/connection/messages_buffer.rb +48 -61
- data/lib/karafka/connection/pauses_manager.rb +2 -2
- data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
- data/lib/karafka/contracts/config.rb +10 -1
- data/lib/karafka/helpers/async.rb +33 -0
- data/lib/karafka/instrumentation/logger_listener.rb +37 -10
- data/lib/karafka/instrumentation/monitor.rb +4 -0
- data/lib/karafka/licenser.rb +26 -7
- data/lib/karafka/messages/batch_metadata.rb +26 -3
- data/lib/karafka/messages/builders/batch_metadata.rb +17 -29
- data/lib/karafka/messages/builders/message.rb +1 -0
- data/lib/karafka/messages/builders/messages.rb +4 -12
- data/lib/karafka/pro/active_job/consumer.rb +48 -0
- data/lib/karafka/pro/active_job/dispatcher.rb +3 -3
- data/lib/karafka/pro/active_job/job_options_contract.rb +2 -2
- data/lib/karafka/pro/base_consumer_extensions.rb +66 -0
- data/lib/karafka/pro/loader.rb +27 -4
- data/lib/karafka/pro/performance_tracker.rb +80 -0
- data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +31 -0
- data/lib/karafka/pro/routing/extensions.rb +32 -0
- data/lib/karafka/pro/scheduler.rb +54 -0
- data/lib/karafka/processing/executor.rb +26 -11
- data/lib/karafka/processing/executors_buffer.rb +15 -7
- data/lib/karafka/processing/jobs/base.rb +28 -0
- data/lib/karafka/processing/jobs/consume.rb +11 -4
- data/lib/karafka/processing/jobs_builder.rb +28 -0
- data/lib/karafka/processing/jobs_queue.rb +28 -16
- data/lib/karafka/processing/worker.rb +39 -10
- data/lib/karafka/processing/workers_batch.rb +5 -0
- data/lib/karafka/routing/consumer_group.rb +1 -1
- data/lib/karafka/routing/subscription_group.rb +2 -2
- data/lib/karafka/routing/subscription_groups_builder.rb +3 -2
- data/lib/karafka/routing/topics.rb +38 -0
- data/lib/karafka/runner.rb +19 -27
- data/lib/karafka/scheduler.rb +20 -0
- data/lib/karafka/server.rb +24 -23
- data/lib/karafka/setup/config.rb +6 -1
- data/lib/karafka/status.rb +1 -3
- data/lib/karafka/time_trackers/pause.rb +10 -2
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +19 -4
- metadata.gz.sig +0 -0
- data/lib/karafka/active_job/routing_extensions.rb +0 -18
@@ -4,10 +4,10 @@ module Karafka
|
|
4
4
|
# Namespace that encapsulates all the logic related to processing data.
|
5
5
|
module Processing
|
6
6
|
# Executors:
|
7
|
-
# - run consumers code
|
8
|
-
#
|
9
|
-
# - they re-create consumer instances in case of partitions that were revoked
|
10
|
-
#
|
7
|
+
# - run consumers code (for `#call`) or run given preparation / teardown operations when needed
|
8
|
+
# from separate threads.
|
9
|
+
# - they re-create consumer instances in case of partitions that were revoked and assigned
|
10
|
+
# back.
|
11
11
|
#
|
12
12
|
# @note Executors are not removed after partition is revoked. They are not that big and will
|
13
13
|
# be re-used in case of a re-claim
|
@@ -18,24 +18,34 @@ module Karafka
|
|
18
18
|
# @return [String] subscription group id to which a given executor belongs
|
19
19
|
attr_reader :group_id
|
20
20
|
|
21
|
+
# @return [Karafka::Messages::Messages] messages batch
|
22
|
+
attr_reader :messages
|
23
|
+
|
24
|
+
# Topic accessibility may be needed for the jobs builder to be able to build a proper job
|
25
|
+
# based on the topic settings defined by the end user
|
26
|
+
#
|
27
|
+
# @return [Karafka::Routing::Topic] topic of this executor
|
28
|
+
attr_reader :topic
|
29
|
+
|
21
30
|
# @param group_id [String] id of the subscription group to which the executor belongs
|
22
31
|
# @param client [Karafka::Connection::Client] kafka client
|
23
32
|
# @param topic [Karafka::Routing::Topic] topic for which this executor will run
|
24
|
-
# @param
|
25
|
-
def initialize(group_id, client, topic,
|
33
|
+
# @param pause_tracker [Karafka::TimeTrackers::Pause] fetch pause tracker for pausing
|
34
|
+
def initialize(group_id, client, topic, pause_tracker)
|
26
35
|
@id = SecureRandom.uuid
|
27
36
|
@group_id = group_id
|
28
37
|
@client = client
|
29
38
|
@topic = topic
|
30
|
-
@
|
39
|
+
@pause_tracker = pause_tracker
|
31
40
|
end
|
32
41
|
|
33
|
-
#
|
42
|
+
# Builds the consumer instance, builds messages batch and sets all that is needed to run the
|
43
|
+
# user consumption logic
|
34
44
|
#
|
35
|
-
# @param messages [Array<
|
45
|
+
# @param messages [Array<Karafka::Messages::Message>]
|
36
46
|
# @param received_at [Time] the moment we've received the batch (actually the moment we've)
|
37
47
|
# enqueued it, but good enough
|
38
|
-
def
|
48
|
+
def prepare(messages, received_at)
|
39
49
|
# Recreate consumer with each batch if persistence is not enabled
|
40
50
|
# We reload the consumers with each batch instead of relying on some external signals
|
41
51
|
# when needed for consistency. That way devs may have it on or off and not in this
|
@@ -49,6 +59,11 @@ module Karafka
|
|
49
59
|
received_at
|
50
60
|
)
|
51
61
|
|
62
|
+
consumer.on_prepare
|
63
|
+
end
|
64
|
+
|
65
|
+
# Runs consumer data processing against given batch and handles failures and errors.
|
66
|
+
def consume
|
52
67
|
# We run the consumer client logic...
|
53
68
|
consumer.on_consume
|
54
69
|
end
|
@@ -86,7 +101,7 @@ module Karafka
|
|
86
101
|
consumer = @topic.consumer.new
|
87
102
|
consumer.topic = @topic
|
88
103
|
consumer.client = @client
|
89
|
-
consumer.
|
104
|
+
consumer.pause_tracker = @pause_tracker
|
90
105
|
consumer.producer = ::Karafka::App.producer
|
91
106
|
consumer
|
92
107
|
end
|
@@ -23,21 +23,29 @@ module Karafka
|
|
23
23
|
partition,
|
24
24
|
pause
|
25
25
|
)
|
26
|
-
|
26
|
+
ktopic = @subscription_group.topics.find(topic)
|
27
27
|
|
28
|
-
|
28
|
+
ktopic || raise(Errors::TopicNotFoundError, topic)
|
29
29
|
|
30
|
-
@buffer[
|
30
|
+
@buffer[ktopic][partition] ||= Executor.new(
|
31
31
|
@subscription_group.id,
|
32
32
|
@client,
|
33
|
-
|
33
|
+
ktopic,
|
34
34
|
pause
|
35
35
|
)
|
36
36
|
end
|
37
37
|
|
38
|
-
#
|
39
|
-
|
40
|
-
|
38
|
+
# Iterates over all available executors and yields them together with topic and partition
|
39
|
+
# info
|
40
|
+
# @yieldparam [Routing::Topic] karafka routing topic object
|
41
|
+
# @yieldparam [Integer] partition number
|
42
|
+
# @yieldparam [Executor] given executor
|
43
|
+
def each
|
44
|
+
@buffer.each do |ktopic, partitions|
|
45
|
+
partitions.each do |partition, executor|
|
46
|
+
yield(ktopic, partition, executor)
|
47
|
+
end
|
48
|
+
end
|
41
49
|
end
|
42
50
|
|
43
51
|
# Clears the executors buffer. Useful for critical errors recovery.
|
@@ -5,6 +5,8 @@ module Karafka
|
|
5
5
|
# Namespace for all the jobs that are suppose to run in workers.
|
6
6
|
module Jobs
|
7
7
|
# Base class for all the jobs types that are suppose to run in workers threads.
|
8
|
+
# Each job can have 3 main entry-points: `#prepare`, `#call` and `#teardown`
|
9
|
+
# Only `#call` is required.
|
8
10
|
class Base
|
9
11
|
extend Forwardable
|
10
12
|
|
@@ -12,6 +14,32 @@ module Karafka
|
|
12
14
|
def_delegators :executor, :id, :group_id
|
13
15
|
|
14
16
|
attr_reader :executor
|
17
|
+
|
18
|
+
# Creates a new job instance
|
19
|
+
def initialize
|
20
|
+
# All jobs are blocking by default and they can release the lock when blocking operations
|
21
|
+
# are done (if needed)
|
22
|
+
@non_blocking = false
|
23
|
+
end
|
24
|
+
|
25
|
+
# When redefined can run any code that should run before executing the proper code
|
26
|
+
def prepare; end
|
27
|
+
|
28
|
+
# When redefined can run any code that should run after executing the proper code
|
29
|
+
def teardown; end
|
30
|
+
|
31
|
+
# @return [Boolean] is this a non-blocking job
|
32
|
+
#
|
33
|
+
# @note Blocking job is a job, that will cause the job queue to wait until it is finished
|
34
|
+
# before removing the lock on new jobs being added
|
35
|
+
#
|
36
|
+
# @note All the jobs are blocking by default
|
37
|
+
#
|
38
|
+
# @note Job **needs** to mark itself as non-blocking only **after** it is done with all
|
39
|
+
# the blocking things (pausing partition, etc).
|
40
|
+
def non_blocking?
|
41
|
+
@non_blocking
|
42
|
+
end
|
15
43
|
end
|
16
44
|
end
|
17
45
|
end
|
@@ -6,10 +6,12 @@ module Karafka
|
|
6
6
|
# The main job type. It runs the executor that triggers given topic partition messages
|
7
7
|
# processing in an underlying consumer instance.
|
8
8
|
class Consume < Base
|
9
|
+
# @return [Array<Rdkafka::Consumer::Message>] array with messages
|
10
|
+
attr_reader :messages
|
11
|
+
|
9
12
|
# @param executor [Karafka::Processing::Executor] executor that is suppose to run a given
|
10
13
|
# job
|
11
|
-
# @param messages [
|
12
|
-
# which we are suppose to work
|
14
|
+
# @param messages [Karafka::Messages::Messages] karafka messages batch
|
13
15
|
# @return [Consume]
|
14
16
|
def initialize(executor, messages)
|
15
17
|
@executor = executor
|
@@ -18,9 +20,14 @@ module Karafka
|
|
18
20
|
super()
|
19
21
|
end
|
20
22
|
|
21
|
-
# Runs the
|
23
|
+
# Runs the preparations on the executor
|
24
|
+
def prepare
|
25
|
+
executor.prepare(@messages, @created_at)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Runs the given executor
|
22
29
|
def call
|
23
|
-
executor.consume
|
30
|
+
executor.consume
|
24
31
|
end
|
25
32
|
end
|
26
33
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Processing
|
5
|
+
# Class responsible for deciding what type of job should we build to run a given command and
|
6
|
+
# for building a proper job for it.
|
7
|
+
class JobsBuilder
|
8
|
+
# @param executor [Karafka::Processing::Executor]
|
9
|
+
# @param messages [Karafka::Messages::Messages] messages batch to be consumed
|
10
|
+
# @return [Karafka::Processing::Jobs::Consume] consumption job
|
11
|
+
def consume(executor, messages)
|
12
|
+
Jobs::Consume.new(executor, messages)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param executor [Karafka::Processing::Executor]
|
16
|
+
# @return [Karafka::Processing::Jobs::Revoked] revocation job
|
17
|
+
def revoked(executor)
|
18
|
+
Jobs::Revoked.new(executor)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param executor [Karafka::Processing::Executor]
|
22
|
+
# @return [Karafka::Processing::Jobs::Shutdown] shutdown job
|
23
|
+
def shutdown(executor)
|
24
|
+
Jobs::Shutdown.new(executor)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -12,7 +12,7 @@ module Karafka
|
|
12
12
|
class JobsQueue
|
13
13
|
# @return [Karafka::Processing::JobsQueue]
|
14
14
|
def initialize
|
15
|
-
@queue =
|
15
|
+
@queue = Queue.new
|
16
16
|
# Those queues will act as a semaphores internally. Since we need an indicator for waiting
|
17
17
|
# we could use Thread.pass but this is expensive. Instead we can just lock until any
|
18
18
|
# of the workers finishes their work and we can re-check. This means that in the worse
|
@@ -21,7 +21,7 @@ module Karafka
|
|
21
21
|
# We cannot use a single semaphore as it could potentially block in listeners that should
|
22
22
|
# process with their data and also could unlock when a given group needs to remain locked
|
23
23
|
@semaphores = Hash.new { |h, k| h[k] = Queue.new }
|
24
|
-
@in_processing = Hash.new { |h, k| h[k] =
|
24
|
+
@in_processing = Hash.new { |h, k| h[k] = [] }
|
25
25
|
@mutex = Mutex.new
|
26
26
|
end
|
27
27
|
|
@@ -44,9 +44,9 @@ module Karafka
|
|
44
44
|
@mutex.synchronize do
|
45
45
|
group = @in_processing[job.group_id]
|
46
46
|
|
47
|
-
raise(Errors::JobsQueueSynchronizationError, job.group_id) if group.
|
47
|
+
raise(Errors::JobsQueueSynchronizationError, job.group_id) if group.include?(job)
|
48
48
|
|
49
|
-
group
|
49
|
+
group << job
|
50
50
|
end
|
51
51
|
|
52
52
|
@queue << job
|
@@ -60,14 +60,21 @@ module Karafka
|
|
60
60
|
@queue.pop
|
61
61
|
end
|
62
62
|
|
63
|
+
# Causes the wait lock to re-check the lock conditions and potential unlock.
|
64
|
+
# @param group_id [String] id of the group we want to unlock for one tick
|
65
|
+
# @note This does not release the wait lock. It just causes a conditions recheck
|
66
|
+
def tick(group_id)
|
67
|
+
@semaphores[group_id] << true
|
68
|
+
end
|
69
|
+
|
63
70
|
# Marks a given job from a given group as completed. When there are no more jobs from a given
|
64
71
|
# group to be executed, we won't wait.
|
65
72
|
#
|
66
73
|
# @param [Jobs::Base] job that was completed
|
67
74
|
def complete(job)
|
68
75
|
@mutex.synchronize do
|
69
|
-
@in_processing[job.group_id].delete(job
|
70
|
-
|
76
|
+
@in_processing[job.group_id].delete(job)
|
77
|
+
tick(job.group_id)
|
71
78
|
end
|
72
79
|
end
|
73
80
|
|
@@ -79,7 +86,7 @@ module Karafka
|
|
79
86
|
@mutex.synchronize do
|
80
87
|
@in_processing[group_id].clear
|
81
88
|
# We unlock it just in case it was blocked when clearing started
|
82
|
-
|
89
|
+
tick(group_id)
|
83
90
|
end
|
84
91
|
end
|
85
92
|
|
@@ -93,8 +100,17 @@ module Karafka
|
|
93
100
|
end
|
94
101
|
end
|
95
102
|
|
96
|
-
#
|
97
|
-
#
|
103
|
+
# @param group_id [String]
|
104
|
+
#
|
105
|
+
# @return [Boolean] tell us if we have anything in the processing (or for processing) from
|
106
|
+
# a given group.
|
107
|
+
def empty?(group_id)
|
108
|
+
@in_processing[group_id].empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Blocks when there are things in the queue in a given group and waits until all the blocking
|
112
|
+
# jobs from a given group are completed
|
113
|
+
#
|
98
114
|
# @param group_id [String] id of the group in which jobs we're interested.
|
99
115
|
# @note This method is blocking.
|
100
116
|
def wait(group_id)
|
@@ -107,14 +123,10 @@ module Karafka
|
|
107
123
|
|
108
124
|
# @param group_id [String] id of the group in which jobs we're interested.
|
109
125
|
# @return [Boolean] should we keep waiting or not
|
126
|
+
# @note We do not wait for non-blocking jobs. Their flow should allow for `poll` running
|
127
|
+
# as they may exceed `max.poll.interval`
|
110
128
|
def wait?(group_id)
|
111
|
-
|
112
|
-
# finish. Otherwise we may risk closing the client and committing offsets afterwards
|
113
|
-
return false if Karafka::App.stopping? && @in_processing[group_id].empty?
|
114
|
-
return false if @queue.closed?
|
115
|
-
return false if @in_processing[group_id].empty?
|
116
|
-
|
117
|
-
true
|
129
|
+
!@in_processing[group_id].all?(&:non_blocking?)
|
118
130
|
end
|
119
131
|
end
|
120
132
|
end
|
@@ -4,25 +4,39 @@ module Karafka
|
|
4
4
|
module Processing
|
5
5
|
# Workers are used to run jobs in separate threads.
|
6
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.
|
7
19
|
class Worker
|
8
|
-
|
20
|
+
include Helpers::Async
|
9
21
|
|
10
|
-
|
22
|
+
# @return [String] id of this worker
|
23
|
+
attr_reader :id
|
11
24
|
|
12
25
|
# @param jobs_queue [JobsQueue]
|
13
26
|
# @return [Worker]
|
14
27
|
def initialize(jobs_queue)
|
28
|
+
@id = SecureRandom.uuid
|
15
29
|
@jobs_queue = jobs_queue
|
16
|
-
@thread = Thread.new do
|
17
|
-
# If anything goes wrong in this worker thread, it means something went really wrong and
|
18
|
-
# we should terminate.
|
19
|
-
Thread.current.abort_on_exception = true
|
20
|
-
loop { break unless process }
|
21
|
-
end
|
22
30
|
end
|
23
31
|
|
24
32
|
private
|
25
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
|
+
|
26
40
|
# Fetches a single job, processes it and marks as completed.
|
27
41
|
#
|
28
42
|
# @note We do not have error handling here, as no errors should propagate this far. If they
|
@@ -33,8 +47,23 @@ module Karafka
|
|
33
47
|
job = @jobs_queue.pop
|
34
48
|
|
35
49
|
if job
|
36
|
-
job
|
37
|
-
|
50
|
+
Karafka.monitor.instrument('worker.process', caller: self, job: job)
|
51
|
+
|
52
|
+
Karafka.monitor.instrument('worker.processed', caller: self, job: job) do
|
53
|
+
job.prepare
|
54
|
+
|
55
|
+
# If a job is marked as non blocking, we can run a tick in the job queue and if there
|
56
|
+
# are no other blocking factors, the job queue will be unlocked.
|
57
|
+
# If this does not run, all the things will be blocking and job queue won't allow to
|
58
|
+
# pass it until done.
|
59
|
+
@jobs_queue.tick(job.group_id) if job.non_blocking?
|
60
|
+
|
61
|
+
job.call
|
62
|
+
|
63
|
+
job.teardown
|
64
|
+
|
65
|
+
true
|
66
|
+
end
|
38
67
|
else
|
39
68
|
false
|
40
69
|
end
|
@@ -10,7 +10,7 @@ module Karafka
|
|
10
10
|
class SubscriptionGroup
|
11
11
|
attr_reader :id, :topics
|
12
12
|
|
13
|
-
# @param topics [
|
13
|
+
# @param topics [Karafka::Routing::Topics] all the topics that share the same key settings
|
14
14
|
# @return [SubscriptionGroup] built subscription group
|
15
15
|
def initialize(topics)
|
16
16
|
@id = SecureRandom.uuid
|
@@ -44,7 +44,7 @@ module Karafka
|
|
44
44
|
kafka[:'auto.offset.reset'] ||= @topics.first.initial_offset
|
45
45
|
# Karafka manages the offsets based on the processing state, thus we do not rely on the
|
46
46
|
# rdkafka offset auto-storing
|
47
|
-
kafka[:'enable.auto.offset.store'] =
|
47
|
+
kafka[:'enable.auto.offset.store'] = false
|
48
48
|
kafka.freeze
|
49
49
|
kafka
|
50
50
|
end
|
@@ -23,8 +23,8 @@ module Karafka
|
|
23
23
|
|
24
24
|
private_constant :DISTRIBUTION_KEYS
|
25
25
|
|
26
|
-
# @param topics [
|
27
|
-
# groups
|
26
|
+
# @param topics [Karafka::Routing::Topics] all the topics based on which we want to build
|
27
|
+
# subscription groups
|
28
28
|
# @return [Array<SubscriptionGroup>] all subscription groups we need in separate threads
|
29
29
|
def call(topics)
|
30
30
|
topics
|
@@ -32,6 +32,7 @@ module Karafka
|
|
32
32
|
.group_by(&:first)
|
33
33
|
.values
|
34
34
|
.map { |value| value.map(&:last) }
|
35
|
+
.map { |topics_array| Routing::Topics.new(topics_array) }
|
35
36
|
.map { |grouped_topics| SubscriptionGroup.new(grouped_topics) }
|
36
37
|
end
|
37
38
|
|
@@ -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
|
data/lib/karafka/runner.rb
CHANGED
@@ -3,32 +3,37 @@
|
|
3
3
|
module Karafka
|
4
4
|
# Class used to run the Karafka listeners in separate threads
|
5
5
|
class Runner
|
6
|
-
# Starts listening on all the listeners asynchronously
|
7
|
-
#
|
6
|
+
# Starts listening on all the listeners asynchronously and handles the jobs queue closing
|
7
|
+
# after listeners are done with their work.
|
8
8
|
def call
|
9
9
|
# Despite possibility of having several independent listeners, we aim to have one queue for
|
10
10
|
# jobs across and one workers poll for that
|
11
11
|
jobs_queue = Processing::JobsQueue.new
|
12
12
|
|
13
13
|
workers = Processing::WorkersBatch.new(jobs_queue)
|
14
|
-
|
14
|
+
listeners = Connection::ListenersBatch.new(jobs_queue)
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
# each listener running in separate threads, so the exceptions should never leak
|
19
|
-
# and if that happens, it means that something really bad happened and we should stop
|
20
|
-
# the whole process
|
21
|
-
Thread
|
22
|
-
.new { listener.call }
|
23
|
-
.tap { |thread| thread.abort_on_exception = true }
|
24
|
-
end
|
16
|
+
workers.each(&:async_call)
|
17
|
+
listeners.each(&:async_call)
|
25
18
|
|
26
19
|
# We aggregate threads here for a supervised shutdown process
|
27
|
-
Karafka::Server.
|
20
|
+
Karafka::Server.workers = workers
|
21
|
+
Karafka::Server.listeners = listeners
|
28
22
|
|
29
23
|
# All the listener threads need to finish
|
30
|
-
|
24
|
+
listeners.each(&:join)
|
25
|
+
|
26
|
+
# We close the jobs queue only when no listener threads are working.
|
27
|
+
# This ensures, that everything was closed prior to us not accepting anymore jobs and that
|
28
|
+
# no more jobs will be enqueued. Since each listener waits for jobs to finish, once those
|
29
|
+
# are done, we can close.
|
30
|
+
jobs_queue.close
|
31
|
+
|
31
32
|
# All the workers need to stop processing anything before we can stop the runner completely
|
33
|
+
# This ensures that even async long-running jobs have time to finish before we are done
|
34
|
+
# with everything. One thing worth keeping in mind though: It is the end user responsibility
|
35
|
+
# to handle the shutdown detection in their long-running processes. Otherwise if timeout
|
36
|
+
# is exceeded, there will be a forced shutdown.
|
32
37
|
workers.each(&:join)
|
33
38
|
# If anything crashes here, we need to raise the error and crush the runner because it means
|
34
39
|
# that something terrible happened
|
@@ -42,18 +47,5 @@ module Karafka
|
|
42
47
|
Karafka::App.stop!
|
43
48
|
raise e
|
44
49
|
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
# @param jobs_queue [Processing::JobsQueue] the main processing queue
|
49
|
-
# @return [Array<Karafka::Connection::Listener>] listeners that will consume messages for each
|
50
|
-
# of the subscription groups
|
51
|
-
def listeners(jobs_queue)
|
52
|
-
App
|
53
|
-
.subscription_groups
|
54
|
-
.map do |subscription_group|
|
55
|
-
Karafka::Connection::Listener.new(subscription_group, jobs_queue)
|
56
|
-
end
|
57
|
-
end
|
58
50
|
end
|
59
51
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
# FIFO scheduler for messages coming from various topics and partitions
|
5
|
+
class Scheduler
|
6
|
+
# Schedules jobs in the fifo order
|
7
|
+
#
|
8
|
+
# @param queue [Karafka::Processing::JobsQueue] queue where we want to put the jobs
|
9
|
+
# @param jobs_array [Array<Karafka::Processing::Jobs::Base>] jobs we want to schedule
|
10
|
+
def schedule_consumption(queue, jobs_array)
|
11
|
+
jobs_array.each do |job|
|
12
|
+
queue << job
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Both revocation and shutdown jobs can also run in fifo by default
|
17
|
+
alias schedule_revocation schedule_consumption
|
18
|
+
alias schedule_shutdown schedule_consumption
|
19
|
+
end
|
20
|
+
end
|
data/lib/karafka/server.rb
CHANGED
@@ -15,7 +15,7 @@ module Karafka
|
|
15
15
|
|
16
16
|
class << self
|
17
17
|
# Set of consuming threads. Each consumer thread contains a single consumer
|
18
|
-
attr_accessor :
|
18
|
+
attr_accessor :listeners
|
19
19
|
|
20
20
|
# Set of workers
|
21
21
|
attr_accessor :workers
|
@@ -25,9 +25,12 @@ module Karafka
|
|
25
25
|
|
26
26
|
# Method which runs app
|
27
27
|
def run
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
# Since we do a lot of threading and queuing, we don't want to stop from the trap context
|
29
|
+
# as some things may not work there as expected, that is why we spawn a separate thread to
|
30
|
+
# handle the stopping process
|
31
|
+
process.on_sigint { Thread.new { stop } }
|
32
|
+
process.on_sigquit { Thread.new { stop } }
|
33
|
+
process.on_sigterm { Thread.new { stop } }
|
31
34
|
|
32
35
|
# Start is blocking until stop is called and when we stop, it will wait until
|
33
36
|
# all of the things are ready to stop
|
@@ -35,6 +38,8 @@ module Karafka
|
|
35
38
|
|
36
39
|
# We always need to wait for Karafka to stop here since we should wait for the stop running
|
37
40
|
# in a separate thread (or trap context) to indicate everything is closed
|
41
|
+
# Since `#start` is blocking, we were get here only after the runner is done. This will
|
42
|
+
# not add any performance degradation because of that.
|
38
43
|
Thread.pass until Karafka::App.stopped?
|
39
44
|
# Try its best to shutdown underlying components before re-raising
|
40
45
|
# rubocop:disable Lint/RescueException
|
@@ -70,16 +75,16 @@ module Karafka
|
|
70
75
|
def stop
|
71
76
|
Karafka::App.stop!
|
72
77
|
|
73
|
-
timeout =
|
78
|
+
timeout = Karafka::App.config.shutdown_timeout
|
74
79
|
|
75
80
|
# We check from time to time (for the timeout period) if all the threads finished
|
76
81
|
# their work and if so, we can just return and normal shutdown process will take place
|
77
82
|
# We divide it by 1000 because we use time in ms.
|
78
83
|
((timeout / 1_000) * SUPERVISION_CHECK_FACTOR).to_i.times do
|
79
|
-
if
|
84
|
+
if listeners.count(&:alive?).zero? &&
|
80
85
|
workers.count(&:alive?).zero?
|
81
86
|
|
82
|
-
|
87
|
+
Karafka::App.producer.close
|
83
88
|
|
84
89
|
return
|
85
90
|
end
|
@@ -89,22 +94,18 @@ module Karafka
|
|
89
94
|
|
90
95
|
raise Errors::ForcefulShutdownError
|
91
96
|
rescue Errors::ForcefulShutdownError => e
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
Karafka::App.producer.close
|
105
|
-
end
|
106
|
-
|
107
|
-
thread.join
|
97
|
+
Karafka.monitor.instrument(
|
98
|
+
'error.occurred',
|
99
|
+
caller: self,
|
100
|
+
error: e,
|
101
|
+
type: 'app.stopping.error'
|
102
|
+
)
|
103
|
+
|
104
|
+
# We're done waiting, lets kill them!
|
105
|
+
workers.each(&:terminate)
|
106
|
+
listeners.each(&:terminate)
|
107
|
+
|
108
|
+
Karafka::App.producer.close
|
108
109
|
|
109
110
|
# exit! is not within the instrumentation as it would not trigger due to exit
|
110
111
|
Kernel.exit! FORCEFUL_EXIT_CODE
|