karafka 2.2.14 → 2.3.0.alpha2
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/.github/workflows/ci.yml +38 -12
- data/.ruby-version +1 -1
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +16 -16
- data/README.md +0 -2
- data/SECURITY.md +23 -0
- data/bin/integrations +1 -1
- data/config/locales/errors.yml +7 -1
- data/config/locales/pro_errors.yml +22 -0
- data/docker-compose.yml +1 -1
- data/karafka.gemspec +2 -2
- data/lib/karafka/admin/acl.rb +287 -0
- data/lib/karafka/admin.rb +9 -13
- data/lib/karafka/app.rb +5 -3
- data/lib/karafka/base_consumer.rb +9 -1
- data/lib/karafka/cli/base.rb +1 -1
- data/lib/karafka/connection/client.rb +83 -76
- data/lib/karafka/connection/conductor.rb +28 -0
- data/lib/karafka/connection/listener.rb +159 -42
- data/lib/karafka/connection/listeners_batch.rb +5 -11
- data/lib/karafka/connection/manager.rb +72 -0
- data/lib/karafka/connection/messages_buffer.rb +12 -0
- data/lib/karafka/connection/proxy.rb +17 -0
- data/lib/karafka/connection/status.rb +75 -0
- data/lib/karafka/contracts/config.rb +14 -10
- data/lib/karafka/contracts/consumer_group.rb +9 -1
- data/lib/karafka/contracts/topic.rb +3 -1
- data/lib/karafka/errors.rb +17 -0
- data/lib/karafka/instrumentation/logger_listener.rb +3 -0
- data/lib/karafka/instrumentation/notifications.rb +13 -5
- data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +31 -28
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +20 -1
- data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +15 -12
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +39 -36
- data/lib/karafka/pro/base_consumer.rb +47 -0
- data/lib/karafka/pro/connection/manager.rb +269 -0
- data/lib/karafka/pro/connection/multiplexing/listener.rb +40 -0
- data/lib/karafka/pro/iterator/tpl_builder.rb +1 -1
- data/lib/karafka/pro/iterator.rb +1 -6
- data/lib/karafka/pro/loader.rb +14 -0
- data/lib/karafka/pro/processing/coordinator.rb +2 -1
- data/lib/karafka/pro/processing/executor.rb +37 -0
- data/lib/karafka/pro/processing/expansions_selector.rb +32 -0
- data/lib/karafka/pro/processing/jobs/periodic.rb +41 -0
- data/lib/karafka/pro/processing/jobs/periodic_non_blocking.rb +32 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +14 -3
- data/lib/karafka/pro/processing/offset_metadata/consumer.rb +44 -0
- data/lib/karafka/pro/processing/offset_metadata/fetcher.rb +131 -0
- data/lib/karafka/pro/processing/offset_metadata/listener.rb +46 -0
- data/lib/karafka/pro/processing/schedulers/base.rb +39 -23
- data/lib/karafka/pro/processing/schedulers/default.rb +12 -14
- data/lib/karafka/pro/processing/strategies/default.rb +154 -1
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +39 -0
- data/lib/karafka/pro/processing/strategies/vp/default.rb +65 -25
- data/lib/karafka/pro/processing/virtual_offset_manager.rb +41 -11
- data/lib/karafka/pro/routing/features/long_running_job/topic.rb +2 -0
- data/lib/karafka/pro/routing/features/multiplexing/config.rb +38 -0
- data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +114 -0
- data/lib/karafka/pro/routing/features/multiplexing/patches/contracts/consumer_group.rb +42 -0
- data/lib/karafka/pro/routing/features/multiplexing/proxy.rb +38 -0
- data/lib/karafka/pro/routing/features/multiplexing/subscription_group.rb +42 -0
- data/lib/karafka/pro/routing/features/multiplexing/subscription_groups_builder.rb +40 -0
- data/lib/karafka/pro/routing/features/multiplexing.rb +59 -0
- data/lib/karafka/pro/routing/features/non_blocking_job/topic.rb +32 -0
- data/lib/karafka/pro/routing/features/non_blocking_job.rb +37 -0
- data/lib/karafka/pro/routing/features/offset_metadata/config.rb +33 -0
- data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +42 -0
- data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +65 -0
- data/lib/karafka/pro/routing/features/offset_metadata.rb +40 -0
- data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +4 -0
- data/lib/karafka/pro/routing/features/patterns/detector.rb +18 -10
- data/lib/karafka/pro/routing/features/periodic_job/config.rb +37 -0
- data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +44 -0
- data/lib/karafka/pro/routing/features/periodic_job/topic.rb +94 -0
- data/lib/karafka/pro/routing/features/periodic_job.rb +27 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +1 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +7 -2
- data/lib/karafka/process.rb +5 -3
- data/lib/karafka/processing/coordinator.rb +5 -1
- data/lib/karafka/processing/executor.rb +16 -10
- data/lib/karafka/processing/executors_buffer.rb +19 -4
- data/lib/karafka/processing/schedulers/default.rb +3 -2
- data/lib/karafka/processing/strategies/default.rb +6 -0
- data/lib/karafka/processing/strategies/dlq.rb +36 -0
- data/lib/karafka/routing/builder.rb +12 -2
- data/lib/karafka/routing/consumer_group.rb +5 -5
- data/lib/karafka/routing/features/base.rb +44 -8
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +6 -1
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -2
- data/lib/karafka/routing/subscription_group.rb +2 -2
- data/lib/karafka/routing/subscription_groups_builder.rb +11 -2
- data/lib/karafka/routing/topic.rb +8 -10
- data/lib/karafka/runner.rb +13 -3
- data/lib/karafka/server.rb +5 -9
- data/lib/karafka/setup/config.rb +17 -0
- data/lib/karafka/status.rb +23 -14
- data/lib/karafka/templates/karafka.rb.erb +7 -0
- data/lib/karafka/time_trackers/partition_usage.rb +56 -0
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +42 -10
- metadata.gz.sig +0 -0
- data/lib/karafka/connection/consumer_group_coordinator.rb +0 -48
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This Karafka component is a Pro component under a commercial license.
|
|
4
|
+
# This Karafka component is NOT licensed under LGPL.
|
|
5
|
+
#
|
|
6
|
+
# All of the commercial components are present in the lib/karafka/pro directory of this
|
|
7
|
+
# repository and their usage requires commercial license agreement.
|
|
8
|
+
#
|
|
9
|
+
# Karafka has also commercial-friendly license, commercial support and commercial components.
|
|
10
|
+
#
|
|
11
|
+
# By sending a pull request to the pro components, you are agreeing to transfer the copyright of
|
|
12
|
+
# your code to Maciej Mensfeld.
|
|
13
|
+
|
|
14
|
+
module Karafka
|
|
15
|
+
module Pro
|
|
16
|
+
module Routing
|
|
17
|
+
module Features
|
|
18
|
+
# Feature allowing to run consumer operations even when no data is present on periodic
|
|
19
|
+
# interval.
|
|
20
|
+
# This allows for advanced window-based operations regardless of income of new data and
|
|
21
|
+
# other advanced cases where the consumer is needed even when no data is coming
|
|
22
|
+
class PeriodicJob < Base
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -32,6 +32,7 @@ module Karafka
|
|
|
32
32
|
required(:active) { |val| [true, false].include?(val) }
|
|
33
33
|
required(:partitioner) { |val| val.nil? || val.respond_to?(:call) }
|
|
34
34
|
required(:max_partitions) { |val| val.is_a?(Integer) && val >= 1 }
|
|
35
|
+
required(:offset_metadata_strategy) { |val| %i[exact current].include?(val) }
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# When virtual partitions are defined, partitioner needs to respond to `#call` and it
|
|
@@ -23,16 +23,21 @@ module Karafka
|
|
|
23
23
|
# create more work than workers. When less, can ensure we have spare resources to
|
|
24
24
|
# process other things in parallel.
|
|
25
25
|
# @param partitioner [nil, #call] nil or callable partitioner
|
|
26
|
+
# @param offset_metadata_strategy [Symbol] how we should match the metadata for the
|
|
27
|
+
# offset. `:exact` will match the offset matching metadata and `:current` will select
|
|
28
|
+
# the most recently reported metadata
|
|
26
29
|
# @return [VirtualPartitions] method that allows to set the virtual partitions details
|
|
27
30
|
# during the routing configuration and then allows to retrieve it
|
|
28
31
|
def virtual_partitions(
|
|
29
32
|
max_partitions: Karafka::App.config.concurrency,
|
|
30
|
-
partitioner: nil
|
|
33
|
+
partitioner: nil,
|
|
34
|
+
offset_metadata_strategy: :current
|
|
31
35
|
)
|
|
32
36
|
@virtual_partitions ||= Config.new(
|
|
33
37
|
active: !partitioner.nil?,
|
|
34
38
|
max_partitions: max_partitions,
|
|
35
|
-
partitioner: partitioner
|
|
39
|
+
partitioner: partitioner,
|
|
40
|
+
offset_metadata_strategy: offset_metadata_strategy
|
|
36
41
|
)
|
|
37
42
|
end
|
|
38
43
|
|
data/lib/karafka/process.rb
CHANGED
|
@@ -25,9 +25,11 @@ module Karafka
|
|
|
25
25
|
# Karafka.logger.info('Log something here')
|
|
26
26
|
# exit
|
|
27
27
|
# end
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
29
|
+
def on_#{signal.to_s.downcase}(&block)
|
|
30
|
+
@callbacks[:#{signal}] << block
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# Creates an instance of process and creates empty hash for callbacks
|
|
@@ -10,8 +10,12 @@ module Karafka
|
|
|
10
10
|
# listener thread, but we go with thread-safe by default for all not to worry about potential
|
|
11
11
|
# future mistakes.
|
|
12
12
|
class Coordinator
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
13
15
|
attr_reader :pause_tracker, :seek_offset, :topic, :partition
|
|
14
16
|
|
|
17
|
+
def_delegators :@pause_tracker, :attempt, :paused?
|
|
18
|
+
|
|
15
19
|
# @param topic [Karafka::Routing::Topic]
|
|
16
20
|
# @param partition [Integer]
|
|
17
21
|
# @param pause_tracker [Karafka::TimeTrackers::Pause] pause tracker for given topic partition
|
|
@@ -149,7 +153,7 @@ module Karafka
|
|
|
149
153
|
|
|
150
154
|
# @return [Boolean] are we in a pause that was initiated by the user
|
|
151
155
|
def manual_pause?
|
|
152
|
-
|
|
156
|
+
paused? && @manual_pause
|
|
153
157
|
end
|
|
154
158
|
|
|
155
159
|
# Marks seek as manual for coordination purposes
|
|
@@ -97,16 +97,6 @@ module Karafka
|
|
|
97
97
|
# This may include house-keeping or other state management changes that can occur but that
|
|
98
98
|
# not mean there are any new messages available for the end user to process
|
|
99
99
|
def idle
|
|
100
|
-
# Initializes the messages set in case idle operation would happen before any processing
|
|
101
|
-
# This prevents us from having no messages object at all as the messages object and
|
|
102
|
-
# its metadata may be used for statistics
|
|
103
|
-
consumer.messages ||= Messages::Builders::Messages.call(
|
|
104
|
-
[],
|
|
105
|
-
topic,
|
|
106
|
-
partition,
|
|
107
|
-
Time.now
|
|
108
|
-
)
|
|
109
|
-
|
|
110
100
|
consumer.on_idle
|
|
111
101
|
end
|
|
112
102
|
|
|
@@ -170,10 +160,26 @@ module Karafka
|
|
|
170
160
|
consumer.client = @client
|
|
171
161
|
consumer.producer = ::Karafka::App.producer
|
|
172
162
|
consumer.coordinator = @coordinator
|
|
163
|
+
# Since we have some message-less flows (idle, etc), we initialize consumer with empty
|
|
164
|
+
# messages set. In production we have persistent consumers, so this is not a performance
|
|
165
|
+
# overhead as this will happen only once per consumer lifetime
|
|
166
|
+
consumer.messages = empty_messages
|
|
173
167
|
|
|
174
168
|
consumer
|
|
175
169
|
end
|
|
176
170
|
end
|
|
171
|
+
|
|
172
|
+
# Initializes the messages set in case given operation would happen before any processing
|
|
173
|
+
# This prevents us from having no messages object at all as the messages object and
|
|
174
|
+
# its metadata may be used for statistics
|
|
175
|
+
def empty_messages
|
|
176
|
+
Messages::Builders::Messages.call(
|
|
177
|
+
[],
|
|
178
|
+
topic,
|
|
179
|
+
partition,
|
|
180
|
+
Time.now
|
|
181
|
+
)
|
|
182
|
+
end
|
|
177
183
|
end
|
|
178
184
|
end
|
|
179
185
|
end
|
|
@@ -13,6 +13,7 @@ module Karafka
|
|
|
13
13
|
@client = client
|
|
14
14
|
# We need two layers here to keep track of topics, partitions and processing groups
|
|
15
15
|
@buffer = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = {} } }
|
|
16
|
+
@executor_class = Karafka::App.config.internal.processing.executor_class
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Finds or creates an executor based on the provided details
|
|
@@ -21,15 +22,28 @@ module Karafka
|
|
|
21
22
|
# @param partition [Integer] partition number
|
|
22
23
|
# @param parallel_key [String] parallel group key
|
|
23
24
|
# @param coordinator [Karafka::Processing::Coordinator]
|
|
24
|
-
# @return [Executor] consumer executor
|
|
25
|
+
# @return [Executor, Pro::Processing::Executor] consumer executor
|
|
25
26
|
def find_or_create(topic, partition, parallel_key, coordinator)
|
|
26
|
-
@buffer[topic][partition][parallel_key] ||=
|
|
27
|
+
@buffer[topic][partition][parallel_key] ||= @executor_class.new(
|
|
27
28
|
@subscription_group.id,
|
|
28
29
|
@client,
|
|
29
30
|
coordinator
|
|
30
31
|
)
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
# Finds all existing executors for given topic partition or creates one for it
|
|
35
|
+
# @param topic [String] topic name
|
|
36
|
+
# @param partition [Integer] partition number
|
|
37
|
+
# @param coordinator [Karafka::Processing::Coordinator]
|
|
38
|
+
# @return [Array<Executor, Pro::Processing::Executor>]
|
|
39
|
+
def find_all_or_create(topic, partition, coordinator)
|
|
40
|
+
existing = find_all(topic, partition)
|
|
41
|
+
|
|
42
|
+
return existing unless existing.empty?
|
|
43
|
+
|
|
44
|
+
[find_or_create(topic, partition, 0, coordinator)]
|
|
45
|
+
end
|
|
46
|
+
|
|
33
47
|
# Revokes executors of a given topic partition, so they won't be used anymore for incoming
|
|
34
48
|
# messages
|
|
35
49
|
#
|
|
@@ -43,7 +57,8 @@ module Karafka
|
|
|
43
57
|
#
|
|
44
58
|
# @param topic [String] topic name
|
|
45
59
|
# @param partition [Integer] partition number
|
|
46
|
-
# @return [Array<Executor>] executors in use for this
|
|
60
|
+
# @return [Array<Executor, Pro::Processing::Executor>] executors in use for this
|
|
61
|
+
# topic + partition
|
|
47
62
|
def find_all(topic, partition)
|
|
48
63
|
@buffer[topic][partition].values
|
|
49
64
|
end
|
|
@@ -52,7 +67,7 @@ module Karafka
|
|
|
52
67
|
# info
|
|
53
68
|
# @yieldparam [Routing::Topic] karafka routing topic object
|
|
54
69
|
# @yieldparam [Integer] partition number
|
|
55
|
-
# @yieldparam [Executor] given executor
|
|
70
|
+
# @yieldparam [Executor, Pro::Processing::Executor] given executor
|
|
56
71
|
def each
|
|
57
72
|
@buffer.each_value do |partitions|
|
|
58
73
|
partitions.each_value do |executors|
|
|
@@ -13,16 +13,17 @@ module Karafka
|
|
|
13
13
|
|
|
14
14
|
# Schedules jobs in the fifo order
|
|
15
15
|
#
|
|
16
|
-
# @param jobs_array [Array<Karafka::Processing::Jobs::
|
|
16
|
+
# @param jobs_array [Array<Karafka::Processing::Jobs::Consume>] jobs we want to schedule
|
|
17
17
|
def on_schedule_consumption(jobs_array)
|
|
18
18
|
jobs_array.each do |job|
|
|
19
19
|
@queue << job
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
#
|
|
23
|
+
# Revocation, shutdown and idle jobs can also run in fifo by default
|
|
24
24
|
alias on_schedule_revocation on_schedule_consumption
|
|
25
25
|
alias on_schedule_shutdown on_schedule_consumption
|
|
26
|
+
alias on_schedule_idle on_schedule_consumption
|
|
26
27
|
|
|
27
28
|
# This scheduler does not have anything to manage as it is a pass through and has no state
|
|
28
29
|
def on_manage
|
|
@@ -41,6 +41,9 @@ module Karafka
|
|
|
41
41
|
# already processed but rather at the next one. This applies to both sync and async
|
|
42
42
|
# versions of this method.
|
|
43
43
|
def mark_as_consumed(message)
|
|
44
|
+
# seek offset can be nil only in case `#seek` was invoked with offset reset request
|
|
45
|
+
# In case like this we ignore marking
|
|
46
|
+
return true if coordinator.seek_offset.nil?
|
|
44
47
|
# Ignore earlier offsets than the one we already committed
|
|
45
48
|
return true if coordinator.seek_offset > message.offset
|
|
46
49
|
return false if revoked?
|
|
@@ -57,6 +60,9 @@ module Karafka
|
|
|
57
60
|
# @return [Boolean] true if we were able to mark the offset, false otherwise.
|
|
58
61
|
# False indicates that we were not able and that we have lost the partition.
|
|
59
62
|
def mark_as_consumed!(message)
|
|
63
|
+
# seek offset can be nil only in case `#seek` was invoked with offset reset request
|
|
64
|
+
# In case like this we ignore marking
|
|
65
|
+
return true if coordinator.seek_offset.nil?
|
|
60
66
|
# Ignore earlier offsets than the one we already committed
|
|
61
67
|
return true if coordinator.seek_offset > message.offset
|
|
62
68
|
return false if revoked?
|
|
@@ -14,6 +14,42 @@ module Karafka
|
|
|
14
14
|
dead_letter_queue
|
|
15
15
|
].freeze
|
|
16
16
|
|
|
17
|
+
# Override of the standard `#mark_as_consumed` in order to handle the pause tracker
|
|
18
|
+
# reset in case DLQ is marked as fully independent. When DLQ is marked independent,
|
|
19
|
+
# any offset marking causes the pause count tracker to reset. This is useful when
|
|
20
|
+
# the error is not due to the collective batch operations state but due to intermediate
|
|
21
|
+
# "crawling" errors that move with it
|
|
22
|
+
#
|
|
23
|
+
# @see `Strategies::Default#mark_as_consumed` for more details
|
|
24
|
+
# @param message [Messages::Message]
|
|
25
|
+
def mark_as_consumed(message)
|
|
26
|
+
# If we are not retrying pause count is already 0, no need to try to reset the state
|
|
27
|
+
return super unless retrying?
|
|
28
|
+
# If we do not use independent marking on DLQ, we just mark as consumed
|
|
29
|
+
return super unless topic.dead_letter_queue.independent?
|
|
30
|
+
# If we were not able to mark no need to reset
|
|
31
|
+
return false unless super
|
|
32
|
+
|
|
33
|
+
coordinator.pause_tracker.reset
|
|
34
|
+
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Override of the standard `#mark_as_consumed!`. Resets the pause tracker count in case
|
|
39
|
+
# DLQ was configured with the `independent` flag.
|
|
40
|
+
#
|
|
41
|
+
# @see `Strategies::Default#mark_as_consumed!` for more details
|
|
42
|
+
# @param message [Messages::Message]
|
|
43
|
+
def mark_as_consumed!(message)
|
|
44
|
+
return super unless retrying?
|
|
45
|
+
return super unless topic.dead_letter_queue.independent?
|
|
46
|
+
return false unless super
|
|
47
|
+
|
|
48
|
+
coordinator.pause_tracker.reset
|
|
49
|
+
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
17
53
|
# When manual offset management is on, we do not mark anything as consumed automatically
|
|
18
54
|
# and we rely on the user to figure things out
|
|
19
55
|
def handle_after_consume
|
|
@@ -109,10 +109,20 @@ module Karafka
|
|
|
109
109
|
# subscription group customization
|
|
110
110
|
# @param subscription_group_name [String, Symbol] subscription group id. When not provided,
|
|
111
111
|
# a random uuid will be used
|
|
112
|
+
# @param args [Array] any extra arguments accepted by the subscription group builder
|
|
112
113
|
# @param block [Proc] further topics definitions
|
|
113
|
-
def subscription_group(
|
|
114
|
+
def subscription_group(
|
|
115
|
+
subscription_group_name = SubscriptionGroup.id,
|
|
116
|
+
**args,
|
|
117
|
+
&block
|
|
118
|
+
)
|
|
114
119
|
consumer_group('app') do
|
|
115
|
-
target.public_send(
|
|
120
|
+
target.public_send(
|
|
121
|
+
:subscription_group=,
|
|
122
|
+
subscription_group_name.to_s,
|
|
123
|
+
**args,
|
|
124
|
+
&block
|
|
125
|
+
)
|
|
116
126
|
end
|
|
117
127
|
end
|
|
118
128
|
|
|
@@ -14,7 +14,7 @@ module Karafka
|
|
|
14
14
|
# It allows us to store the "current" subscription group defined in the routing
|
|
15
15
|
# This subscription group id is then injected into topics, so we can compute the subscription
|
|
16
16
|
# groups
|
|
17
|
-
attr_accessor :
|
|
17
|
+
attr_accessor :current_subscription_group_details
|
|
18
18
|
|
|
19
19
|
# @param name [String, Symbol] raw name of this consumer group. Raw means, that it does not
|
|
20
20
|
# yet have an application client_id namespace, this will be added here by default.
|
|
@@ -26,7 +26,7 @@ module Karafka
|
|
|
26
26
|
@topics = Topics.new([])
|
|
27
27
|
# Initialize the subscription group so there's always a value for it, since even if not
|
|
28
28
|
# defined directly, a subscription group will be created
|
|
29
|
-
@
|
|
29
|
+
@current_subscription_group_details = { name: SubscriptionGroup.id }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# @return [Boolean] true if this consumer group should be active in our current process
|
|
@@ -48,7 +48,7 @@ module Karafka
|
|
|
48
48
|
built_topic = @topics.last
|
|
49
49
|
# We overwrite it conditionally in case it was not set by the user inline in the topic
|
|
50
50
|
# block definition
|
|
51
|
-
built_topic.
|
|
51
|
+
built_topic.subscription_group_details ||= current_subscription_group_details
|
|
52
52
|
built_topic
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -59,13 +59,13 @@ module Karafka
|
|
|
59
59
|
def subscription_group=(name = SubscriptionGroup.id, &block)
|
|
60
60
|
# We cast it here, so the routing supports symbol based but that's anyhow later on
|
|
61
61
|
# validated as a string
|
|
62
|
-
@
|
|
62
|
+
@current_subscription_group_details = { name: name.to_s }
|
|
63
63
|
|
|
64
64
|
Proxy.new(self, &block)
|
|
65
65
|
|
|
66
66
|
# We need to reset the current subscription group after it is used, so it won't leak
|
|
67
67
|
# outside to other topics that would be defined without a defined subscription group
|
|
68
|
-
@
|
|
68
|
+
@current_subscription_group_details = { name: SubscriptionGroup.id }
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# @return [Array<Routing::SubscriptionGroup>] all the subscription groups build based on
|
|
@@ -13,17 +13,46 @@ module Karafka
|
|
|
13
13
|
class << self
|
|
14
14
|
# Extends topic and builder with given feature API
|
|
15
15
|
def activate
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
if const_defined?('Topic', false)
|
|
17
|
+
Topic.prepend(self::Topic)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if const_defined?('Topics', false)
|
|
21
|
+
Topics.prepend(self::Topics)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if const_defined?('ConsumerGroup', false)
|
|
25
|
+
ConsumerGroup.prepend(self::ConsumerGroup)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if const_defined?('Proxy', false)
|
|
29
|
+
Proxy.prepend(self::Proxy)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if const_defined?('Builder', false)
|
|
33
|
+
Builder.prepend(self::Builder)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if const_defined?('Contracts', false)
|
|
37
|
+
Builder.prepend(Base::Expander.new(self))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if const_defined?('SubscriptionGroup', false)
|
|
41
|
+
SubscriptionGroup.prepend(self::SubscriptionGroup)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if const_defined?('SubscriptionGroupsBuilder', false)
|
|
45
|
+
SubscriptionGroupsBuilder.prepend(self::SubscriptionGroupsBuilder)
|
|
46
|
+
end
|
|
22
47
|
end
|
|
23
48
|
|
|
24
|
-
# Loads all the features and activates them
|
|
49
|
+
# Loads all the features and activates them once
|
|
25
50
|
def load_all
|
|
51
|
+
return if @loaded
|
|
52
|
+
|
|
26
53
|
features.each(&:activate)
|
|
54
|
+
|
|
55
|
+
@loaded = true
|
|
27
56
|
end
|
|
28
57
|
|
|
29
58
|
# @param config [Karafka::Core::Configurable::Node] app config that we can alter with
|
|
@@ -41,11 +70,18 @@ module Karafka
|
|
|
41
70
|
|
|
42
71
|
private
|
|
43
72
|
|
|
44
|
-
# @return [Array<Class>] all available routing features
|
|
73
|
+
# @return [Array<Class>] all available routing features that are direct descendants of
|
|
74
|
+
# the features base.Approach with using `#superclass` prevents us from accidentally
|
|
75
|
+
# loading Pro components
|
|
45
76
|
def features
|
|
46
77
|
ObjectSpace
|
|
47
78
|
.each_object(Class)
|
|
48
79
|
.select { |klass| klass < self }
|
|
80
|
+
# Ensures, that Pro components are only loaded when we operate in Pro mode. Since
|
|
81
|
+
# outside of specs Zeitwerk does not require them at all, they will not be loaded
|
|
82
|
+
# anyhow, but for specs this needs to be done as RSpec requires all files to be
|
|
83
|
+
# present
|
|
84
|
+
.reject { |klass| Karafka.pro? ? false : klass.superclass != self }
|
|
49
85
|
.sort_by(&:to_s)
|
|
50
86
|
end
|
|
51
87
|
|
|
@@ -11,8 +11,13 @@ module Karafka
|
|
|
11
11
|
:max_retries,
|
|
12
12
|
# To what topic the skipped messages should be moved
|
|
13
13
|
:topic,
|
|
14
|
+
# Should retries be handled collectively on a batch or independently per message
|
|
15
|
+
:independent,
|
|
14
16
|
keyword_init: true
|
|
15
|
-
)
|
|
17
|
+
) do
|
|
18
|
+
alias_method :active?, :active
|
|
19
|
+
alias_method :independent?, :independent
|
|
20
|
+
end
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -14,12 +14,19 @@ module Karafka
|
|
|
14
14
|
# @param max_retries [Integer] after how many retries should we move data to dlq
|
|
15
15
|
# @param topic [String, false] where the messages should be moved if failing or false
|
|
16
16
|
# if we do not want to move it anywhere and just skip
|
|
17
|
+
# @param independent [Boolean] needs to be true in order for each marking as consumed
|
|
18
|
+
# in a retry flow to reset the errors counter
|
|
17
19
|
# @return [Config] defined config
|
|
18
|
-
def dead_letter_queue(
|
|
20
|
+
def dead_letter_queue(
|
|
21
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
|
22
|
+
topic: nil,
|
|
23
|
+
independent: false
|
|
24
|
+
)
|
|
19
25
|
@dead_letter_queue ||= Config.new(
|
|
20
26
|
active: !topic.nil?,
|
|
21
27
|
max_retries: max_retries,
|
|
22
|
-
topic: topic
|
|
28
|
+
topic: topic,
|
|
29
|
+
independent: independent
|
|
23
30
|
)
|
|
24
31
|
end
|
|
25
32
|
|
|
@@ -37,7 +37,8 @@ module Karafka
|
|
|
37
37
|
# @param topics [Karafka::Routing::Topics] all the topics that share the same key settings
|
|
38
38
|
# @return [SubscriptionGroup] built subscription group
|
|
39
39
|
def initialize(position, topics)
|
|
40
|
-
@
|
|
40
|
+
@details = topics.first.subscription_group_details
|
|
41
|
+
@name = @details.fetch(:name)
|
|
41
42
|
@consumer_group = topics.first.consumer_group
|
|
42
43
|
# We include the consumer group id here because we want to have unique ids of subscription
|
|
43
44
|
# groups across the system. Otherwise user could set the same name for multiple
|
|
@@ -47,7 +48,6 @@ module Karafka
|
|
|
47
48
|
@position = position
|
|
48
49
|
@topics = topics
|
|
49
50
|
@kafka = build_kafka
|
|
50
|
-
freeze
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# @return [String] consumer group id
|
|
@@ -19,7 +19,7 @@ module Karafka
|
|
|
19
19
|
max_messages
|
|
20
20
|
max_wait_time
|
|
21
21
|
initial_offset
|
|
22
|
-
|
|
22
|
+
subscription_group_details
|
|
23
23
|
].freeze
|
|
24
24
|
|
|
25
25
|
private_constant :DISTRIBUTION_KEYS
|
|
@@ -37,7 +37,7 @@ module Karafka
|
|
|
37
37
|
.group_by(&:first)
|
|
38
38
|
.values
|
|
39
39
|
.map { |value| value.map(&:last) }
|
|
40
|
-
.
|
|
40
|
+
.flat_map { |value| expand(value) }
|
|
41
41
|
.map { |grouped_topics| SubscriptionGroup.new(@position += 1, grouped_topics) }
|
|
42
42
|
.tap do |subscription_groups|
|
|
43
43
|
subscription_groups.each do |subscription_group|
|
|
@@ -60,6 +60,15 @@ module Karafka
|
|
|
60
60
|
|
|
61
61
|
accu.hash
|
|
62
62
|
end
|
|
63
|
+
|
|
64
|
+
# Hook for optional expansion of groups based on subscription group features
|
|
65
|
+
#
|
|
66
|
+
# @param topics_array [Array<Routing::Topic>] group of topics that have the same settings
|
|
67
|
+
# and can use the same connection
|
|
68
|
+
# @return [Array<Array<Routing::Topics>>] expanded groups
|
|
69
|
+
def expand(topics_array)
|
|
70
|
+
[Routing::Topics.new(topics_array)]
|
|
71
|
+
end
|
|
63
72
|
end
|
|
64
73
|
end
|
|
65
74
|
end
|
|
@@ -9,7 +9,7 @@ module Karafka
|
|
|
9
9
|
attr_reader :id, :name, :consumer_group
|
|
10
10
|
attr_writer :consumer
|
|
11
11
|
|
|
12
|
-
attr_accessor :
|
|
12
|
+
attr_accessor :subscription_group_details
|
|
13
13
|
|
|
14
14
|
# Full subscription group reference can be built only when we have knowledge about the
|
|
15
15
|
# whole routing tree, this is why it is going to be set later on
|
|
@@ -46,15 +46,13 @@ module Karafka
|
|
|
46
46
|
INHERITABLE_ATTRIBUTES.each do |attribute|
|
|
47
47
|
attr_writer attribute
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
50
|
+
def #{attribute}
|
|
51
|
+
return @#{attribute} unless @#{attribute}.nil?
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
instance_variable_set(:"@#{attribute}", value)
|
|
57
|
-
end
|
|
53
|
+
@#{attribute} = Karafka::App.config.send(:#{attribute})
|
|
54
|
+
end
|
|
55
|
+
RUBY
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
# @return [String] name of subscription that will go to librdkafka
|
|
@@ -117,7 +115,7 @@ module Karafka
|
|
|
117
115
|
active: active?,
|
|
118
116
|
consumer: consumer,
|
|
119
117
|
consumer_group_id: consumer_group.id,
|
|
120
|
-
|
|
118
|
+
subscription_group_details: subscription_group_details
|
|
121
119
|
).freeze
|
|
122
120
|
end
|
|
123
121
|
end
|
data/lib/karafka/runner.rb
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
module Karafka
|
|
4
4
|
# Class used to run the Karafka listeners in separate threads
|
|
5
5
|
class Runner
|
|
6
|
+
def initialize
|
|
7
|
+
@manager = App.config.internal.connection.manager
|
|
8
|
+
@conductor = App.config.internal.connection.conductor
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
# Starts listening on all the listeners asynchronously and handles the jobs queue closing
|
|
7
12
|
# after listeners are done with their work.
|
|
8
13
|
def call
|
|
@@ -13,16 +18,21 @@ module Karafka
|
|
|
13
18
|
workers = Processing::WorkersBatch.new(jobs_queue)
|
|
14
19
|
listeners = Connection::ListenersBatch.new(jobs_queue)
|
|
15
20
|
|
|
21
|
+
# Register all the listeners so they can be started and managed
|
|
22
|
+
@manager.register(listeners)
|
|
23
|
+
|
|
16
24
|
workers.each(&:async_call)
|
|
17
|
-
listeners.each(&:async_call)
|
|
18
25
|
|
|
19
26
|
# We aggregate threads here for a supervised shutdown process
|
|
20
27
|
Karafka::Server.workers = workers
|
|
21
28
|
Karafka::Server.listeners = listeners
|
|
22
29
|
Karafka::Server.jobs_queue = jobs_queue
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
until @manager.done?
|
|
32
|
+
@conductor.wait
|
|
33
|
+
|
|
34
|
+
@manager.control
|
|
35
|
+
end
|
|
26
36
|
|
|
27
37
|
# We close the jobs queue only when no listener threads are working.
|
|
28
38
|
# This ensures, that everything was closed prior to us not accepting anymore jobs and that
|
data/lib/karafka/server.rb
CHANGED
|
@@ -88,7 +88,10 @@ module Karafka
|
|
|
88
88
|
# their work and if so, we can just return and normal shutdown process will take place
|
|
89
89
|
# We divide it by 1000 because we use time in ms.
|
|
90
90
|
((timeout / 1_000) * SUPERVISION_CHECK_FACTOR).to_i.times do
|
|
91
|
-
|
|
91
|
+
all_listeners_stopped = listeners.all?(&:stopped?)
|
|
92
|
+
all_workers_stopped = workers.none?(&:alive?)
|
|
93
|
+
|
|
94
|
+
return if all_listeners_stopped && all_workers_stopped
|
|
92
95
|
|
|
93
96
|
sleep SUPERVISION_SLEEP
|
|
94
97
|
end
|
|
@@ -104,7 +107,7 @@ module Karafka
|
|
|
104
107
|
|
|
105
108
|
# We're done waiting, lets kill them!
|
|
106
109
|
workers.each(&:terminate)
|
|
107
|
-
listeners.each(&:terminate)
|
|
110
|
+
listeners.active.each(&:terminate)
|
|
108
111
|
# We always need to shutdown clients to make sure we do not force the GC to close consumer.
|
|
109
112
|
# This can cause memory leaks and crashes.
|
|
110
113
|
listeners.each(&:shutdown)
|
|
@@ -137,13 +140,6 @@ module Karafka
|
|
|
137
140
|
# We don't have to safe-guard it with check states as the state transitions work only
|
|
138
141
|
# in one direction
|
|
139
142
|
Karafka::App.quiet!
|
|
140
|
-
|
|
141
|
-
# We need one more thread to monitor the process and move to quieted once everything
|
|
142
|
-
# is quiet and no processing is happening anymore
|
|
143
|
-
Thread.new do
|
|
144
|
-
sleep(0.1) until listeners.coordinators.all?(&:finished?)
|
|
145
|
-
Karafka::App.quieted!
|
|
146
|
-
end
|
|
147
143
|
end
|
|
148
144
|
|
|
149
145
|
private
|