karafka 2.3.3 → 2.4.0.beta2
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 +12 -38
- data/CHANGELOG.md +59 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +29 -27
- data/bin/integrations +1 -1
- data/config/locales/errors.yml +21 -2
- data/config/locales/pro_errors.yml +16 -1
- data/karafka.gemspec +4 -2
- data/lib/active_job/queue_adapters/karafka_adapter.rb +2 -0
- data/lib/karafka/admin/configs/config.rb +81 -0
- data/lib/karafka/admin/configs/resource.rb +88 -0
- data/lib/karafka/admin/configs.rb +103 -0
- data/lib/karafka/admin.rb +211 -90
- data/lib/karafka/base_consumer.rb +2 -2
- data/lib/karafka/cli/info.rb +9 -7
- data/lib/karafka/cli/server.rb +7 -7
- data/lib/karafka/cli/topics/align.rb +109 -0
- data/lib/karafka/cli/topics/base.rb +66 -0
- data/lib/karafka/cli/topics/create.rb +35 -0
- data/lib/karafka/cli/topics/delete.rb +30 -0
- data/lib/karafka/cli/topics/migrate.rb +31 -0
- data/lib/karafka/cli/topics/plan.rb +169 -0
- data/lib/karafka/cli/topics/repartition.rb +41 -0
- data/lib/karafka/cli/topics/reset.rb +18 -0
- data/lib/karafka/cli/topics.rb +13 -123
- data/lib/karafka/connection/client.rb +55 -37
- data/lib/karafka/connection/listener.rb +22 -17
- data/lib/karafka/connection/proxy.rb +93 -4
- data/lib/karafka/connection/status.rb +14 -2
- data/lib/karafka/constraints.rb +3 -3
- data/lib/karafka/contracts/config.rb +14 -1
- data/lib/karafka/contracts/topic.rb +1 -1
- data/lib/karafka/deserializers/headers.rb +15 -0
- data/lib/karafka/deserializers/key.rb +15 -0
- data/lib/karafka/deserializers/payload.rb +16 -0
- data/lib/karafka/embedded.rb +2 -0
- data/lib/karafka/helpers/async.rb +5 -2
- data/lib/karafka/helpers/colorize.rb +6 -0
- data/lib/karafka/instrumentation/callbacks/oauthbearer_token_refresh.rb +29 -0
- data/lib/karafka/instrumentation/logger_listener.rb +23 -3
- data/lib/karafka/instrumentation/notifications.rb +10 -0
- data/lib/karafka/instrumentation/vendors/appsignal/client.rb +16 -2
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +20 -0
- data/lib/karafka/messages/batch_metadata.rb +1 -1
- data/lib/karafka/messages/builders/batch_metadata.rb +1 -1
- data/lib/karafka/messages/builders/message.rb +10 -6
- data/lib/karafka/messages/message.rb +2 -1
- data/lib/karafka/messages/metadata.rb +20 -4
- data/lib/karafka/messages/parser.rb +1 -1
- data/lib/karafka/pro/base_consumer.rb +12 -23
- data/lib/karafka/pro/encryption/cipher.rb +7 -3
- data/lib/karafka/pro/encryption/contracts/config.rb +1 -0
- data/lib/karafka/pro/encryption/errors.rb +4 -1
- data/lib/karafka/pro/encryption/messages/middleware.rb +13 -11
- data/lib/karafka/pro/encryption/messages/parser.rb +22 -20
- data/lib/karafka/pro/encryption/setup/config.rb +5 -0
- data/lib/karafka/pro/iterator/expander.rb +2 -1
- data/lib/karafka/pro/iterator/tpl_builder.rb +38 -0
- data/lib/karafka/pro/iterator.rb +28 -2
- data/lib/karafka/pro/loader.rb +3 -0
- data/lib/karafka/pro/processing/coordinator.rb +15 -2
- data/lib/karafka/pro/processing/expansions_selector.rb +2 -0
- data/lib/karafka/pro/processing/jobs_queue.rb +122 -5
- data/lib/karafka/pro/processing/periodic_job/consumer.rb +67 -0
- data/lib/karafka/pro/processing/piping/consumer.rb +126 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +2 -0
- data/lib/karafka/pro/processing/strategies/default.rb +5 -1
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +21 -5
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +2 -0
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +2 -0
- data/lib/karafka/pro/processing/subscription_groups_coordinator.rb +52 -0
- data/lib/karafka/pro/routing/features/direct_assignments/config.rb +27 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/consumer_group.rb +53 -0
- data/lib/karafka/pro/routing/features/direct_assignments/contracts/topic.rb +108 -0
- data/lib/karafka/pro/routing/features/direct_assignments/subscription_group.rb +77 -0
- data/lib/karafka/pro/routing/features/direct_assignments/topic.rb +69 -0
- data/lib/karafka/pro/routing/features/direct_assignments.rb +25 -0
- data/lib/karafka/pro/routing/features/patterns/builder.rb +1 -1
- data/lib/karafka/pro/routing/features/swarm/contracts/routing.rb +76 -0
- data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +16 -5
- data/lib/karafka/pro/routing/features/swarm/topic.rb +25 -2
- data/lib/karafka/pro/routing/features/swarm.rb +11 -0
- data/lib/karafka/pro/swarm/liveness_listener.rb +20 -0
- data/lib/karafka/processing/coordinator.rb +17 -8
- data/lib/karafka/processing/coordinators_buffer.rb +5 -2
- data/lib/karafka/processing/executor.rb +6 -2
- data/lib/karafka/processing/executors_buffer.rb +5 -2
- data/lib/karafka/processing/jobs_queue.rb +9 -4
- data/lib/karafka/processing/strategies/aj_dlq_mom.rb +1 -1
- data/lib/karafka/processing/strategies/default.rb +7 -1
- data/lib/karafka/processing/strategies/dlq.rb +17 -2
- data/lib/karafka/processing/workers_batch.rb +4 -1
- data/lib/karafka/routing/builder.rb +6 -2
- data/lib/karafka/routing/consumer_group.rb +2 -1
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +5 -0
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +8 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +10 -2
- data/lib/karafka/routing/features/deserializers/config.rb +18 -0
- data/lib/karafka/routing/features/deserializers/contracts/topic.rb +31 -0
- data/lib/karafka/routing/features/deserializers/topic.rb +51 -0
- data/lib/karafka/routing/features/deserializers.rb +11 -0
- data/lib/karafka/routing/proxy.rb +9 -14
- data/lib/karafka/routing/router.rb +11 -2
- data/lib/karafka/routing/subscription_group.rb +9 -1
- data/lib/karafka/routing/topic.rb +0 -1
- data/lib/karafka/runner.rb +1 -1
- data/lib/karafka/setup/config.rb +50 -9
- data/lib/karafka/status.rb +7 -8
- data/lib/karafka/swarm/supervisor.rb +16 -2
- data/lib/karafka/templates/karafka.rb.erb +28 -1
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +38 -12
- metadata.gz.sig +0 -0
- data/lib/karafka/routing/consumer_mapper.rb +0 -23
- data/lib/karafka/serialization/json/deserializer.rb +0 -19
- data/lib/karafka/time_trackers/partition_usage.rb +0 -56
|
@@ -99,8 +99,9 @@ module Karafka
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# @return [Boolean] is the coordinated work finished or not
|
|
102
|
+
# @note Used only in the consume operation context
|
|
102
103
|
def finished?
|
|
103
|
-
@running_jobs.zero?
|
|
104
|
+
@running_jobs[:consume].zero?
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
# Runs synchronized code once for a collective of virtual partitions prior to work being
|
|
@@ -122,7 +123,7 @@ module Karafka
|
|
|
122
123
|
end
|
|
123
124
|
end
|
|
124
125
|
|
|
125
|
-
# Runs once when all the work that is suppose to be coordinated is finished
|
|
126
|
+
# Runs given code once when all the work that is suppose to be coordinated is finished
|
|
126
127
|
# It runs once per all the coordinated jobs and should be used to run any type of post
|
|
127
128
|
# jobs coordination processing execution
|
|
128
129
|
def on_finished
|
|
@@ -143,6 +144,18 @@ module Karafka
|
|
|
143
144
|
end
|
|
144
145
|
end
|
|
145
146
|
|
|
147
|
+
# @param interval [Integer] milliseconds of activity
|
|
148
|
+
# @return [Boolean] was this partition in activity within last `interval` milliseconds
|
|
149
|
+
# @note Will return true also if currently active
|
|
150
|
+
def active_within?(interval)
|
|
151
|
+
# its always active if there's any job related to this coordinator that is still
|
|
152
|
+
# enqueued or running
|
|
153
|
+
return true if @running_jobs.values.any?(:positive?)
|
|
154
|
+
|
|
155
|
+
# Otherwise we check last time any job of this coordinator was active
|
|
156
|
+
@changed_at + interval > monotonic_now
|
|
157
|
+
end
|
|
158
|
+
|
|
146
159
|
private
|
|
147
160
|
|
|
148
161
|
# Checks if given action is executable once. If it is and true is returned, this method
|
|
@@ -23,7 +23,9 @@ module Karafka
|
|
|
23
23
|
def find(topic)
|
|
24
24
|
# Start with the non-pro expansions
|
|
25
25
|
expansions = super
|
|
26
|
+
expansions << Pro::Processing::Piping::Consumer
|
|
26
27
|
expansions << Pro::Processing::OffsetMetadata::Consumer if topic.offset_metadata?
|
|
28
|
+
expansions << Pro::Processing::PeriodicJob::Consumer if topic.periodic_job?
|
|
27
29
|
expansions
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -19,17 +19,36 @@ module Karafka
|
|
|
19
19
|
#
|
|
20
20
|
# Aside from the OSS queue capabilities it allows for jobless locking for advanced schedulers
|
|
21
21
|
class JobsQueue < Karafka::Processing::JobsQueue
|
|
22
|
+
include Core::Helpers::Time
|
|
23
|
+
|
|
22
24
|
attr_accessor :in_processing
|
|
23
25
|
|
|
26
|
+
# How long should we keep async lock (31 years)
|
|
27
|
+
WAIT_TIMEOUT = 10_000_000_000
|
|
28
|
+
|
|
29
|
+
private_constant :WAIT_TIMEOUT
|
|
30
|
+
|
|
24
31
|
# @return [Karafka::Pro::Processing::JobsQueue]
|
|
25
32
|
def initialize
|
|
26
33
|
super
|
|
27
34
|
|
|
28
35
|
@in_waiting = Hash.new { |h, k| h[k] = [] }
|
|
36
|
+
@locks = Hash.new { |h, k| h[k] = {} }
|
|
37
|
+
@async_locking = false
|
|
29
38
|
|
|
30
39
|
@statistics[:waiting] = 0
|
|
31
40
|
end
|
|
32
41
|
|
|
42
|
+
# Registers semaphore and a lock hash
|
|
43
|
+
#
|
|
44
|
+
# @param group_id [String]
|
|
45
|
+
def register(group_id)
|
|
46
|
+
super
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
@locks[group_id]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
33
52
|
# Method that allows us to lock queue on a given subscription group without enqueuing the a
|
|
34
53
|
# job. This can be used when building complex schedulers that want to postpone enqueuing
|
|
35
54
|
# before certain conditions are met.
|
|
@@ -64,6 +83,48 @@ module Karafka
|
|
|
64
83
|
end
|
|
65
84
|
end
|
|
66
85
|
|
|
86
|
+
# Allows for explicit locking of the queue of a given subscription group.
|
|
87
|
+
#
|
|
88
|
+
# This can be used for cross-topic synchronization.
|
|
89
|
+
#
|
|
90
|
+
# @param group_id [String] id of the group we want to lock
|
|
91
|
+
# @param lock_id [Object] unique id we want to use to identify our lock
|
|
92
|
+
# @param timeout [Integer] number of ms how long this lock should be valid. Useful for
|
|
93
|
+
# auto-expiring locks used to delay further processing without explicit pausing on
|
|
94
|
+
# the consumer
|
|
95
|
+
#
|
|
96
|
+
# @note We do not raise `Errors::JobsQueueSynchronizationError` similar to `#lock` here
|
|
97
|
+
# because we want to have ability to prolong time limited locks
|
|
98
|
+
def lock_async(group_id, lock_id, timeout: WAIT_TIMEOUT)
|
|
99
|
+
return if @queue.closed?
|
|
100
|
+
|
|
101
|
+
@async_locking = true
|
|
102
|
+
|
|
103
|
+
@mutex.synchronize do
|
|
104
|
+
@locks[group_id][lock_id] = monotonic_now + timeout
|
|
105
|
+
|
|
106
|
+
# We need to tick so our new time sensitive lock can reload time constraints on sleep
|
|
107
|
+
tick(group_id)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Allows for explicit unlocking of locked queue of a group
|
|
112
|
+
#
|
|
113
|
+
# @param group_id [String] id of the group we want to unlock
|
|
114
|
+
# @param lock_id [Object] unique id we want to use to identify our lock
|
|
115
|
+
#
|
|
116
|
+
def unlock_async(group_id, lock_id)
|
|
117
|
+
@mutex.synchronize do
|
|
118
|
+
if @locks[group_id].delete(lock_id)
|
|
119
|
+
tick(group_id)
|
|
120
|
+
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
raise(Errors::JobsQueueSynchronizationError, [group_id, lock_id])
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
67
128
|
# Clears the processing states for a provided group. Useful when a recovery happens and we
|
|
68
129
|
# need to clean up state but only for a given subscription group.
|
|
69
130
|
#
|
|
@@ -74,6 +135,8 @@ module Karafka
|
|
|
74
135
|
|
|
75
136
|
@statistics[:waiting] -= @in_waiting[group_id].size
|
|
76
137
|
@in_waiting[group_id].clear
|
|
138
|
+
@locks[group_id].clear
|
|
139
|
+
@async_locking = false
|
|
77
140
|
|
|
78
141
|
# We unlock it just in case it was blocked when clearing started
|
|
79
142
|
tick(group_id)
|
|
@@ -87,21 +150,75 @@ module Karafka
|
|
|
87
150
|
def empty?(group_id)
|
|
88
151
|
@mutex.synchronize do
|
|
89
152
|
@in_processing[group_id].empty? &&
|
|
90
|
-
@in_waiting[group_id].empty?
|
|
153
|
+
@in_waiting[group_id].empty? &&
|
|
154
|
+
!locked_async?(group_id)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Blocks when there are things in the queue in a given group and waits until all the
|
|
159
|
+
# blocking jobs from a given group are completed or any of the locks times out
|
|
160
|
+
# @param group_id [String] id of the group in which jobs we're interested.
|
|
161
|
+
# @see `Karafka::Processing::JobsQueue`
|
|
162
|
+
#
|
|
163
|
+
# @note Because checking that async locking is on happens on regular ticking, first lock
|
|
164
|
+
# on a group can take up to one tick. That is expected.
|
|
165
|
+
#
|
|
166
|
+
# @note This implementation takes into consideration temporary async locks that can happen.
|
|
167
|
+
# Thanks to the fact that we use the minimum lock time as a timeout, we do not have to
|
|
168
|
+
# wait a whole ticking period to unlock async locks.
|
|
169
|
+
def wait(group_id)
|
|
170
|
+
return super unless @async_locking
|
|
171
|
+
|
|
172
|
+
# We do not generalize this flow because this one is more expensive as it has to allocate
|
|
173
|
+
# extra objects. That's why we only use it when locks are actually in use
|
|
174
|
+
base_interval = tick_interval / 1_000.0
|
|
175
|
+
|
|
176
|
+
while wait?(group_id)
|
|
177
|
+
yield if block_given?
|
|
178
|
+
|
|
179
|
+
now = monotonic_now
|
|
180
|
+
|
|
181
|
+
wait_times = @locks[group_id].values.map! do |lock_time|
|
|
182
|
+
# Convert ms to seconds, seconds are required by Ruby queue engine
|
|
183
|
+
(lock_time - now) / 1_000
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
wait_times.delete_if(&:negative?)
|
|
187
|
+
wait_times << base_interval
|
|
188
|
+
|
|
189
|
+
@semaphores.fetch(group_id).pop(timeout: wait_times.min)
|
|
91
190
|
end
|
|
92
191
|
end
|
|
93
192
|
|
|
94
193
|
private
|
|
95
194
|
|
|
195
|
+
# Tells us if given group is locked
|
|
196
|
+
#
|
|
197
|
+
# @param group_id [String] id of the group in which we're interested.
|
|
198
|
+
# @return [Boolean] true if there are any active locks on the group, otherwise false
|
|
199
|
+
def locked_async?(group_id)
|
|
200
|
+
return false unless @async_locking
|
|
201
|
+
|
|
202
|
+
group = @locks[group_id]
|
|
203
|
+
|
|
204
|
+
return false if group.empty?
|
|
205
|
+
|
|
206
|
+
now = monotonic_now
|
|
207
|
+
|
|
208
|
+
group.delete_if { |_, wait_timeout| wait_timeout < now }
|
|
209
|
+
|
|
210
|
+
!group.empty?
|
|
211
|
+
end
|
|
212
|
+
|
|
96
213
|
# @param group_id [String] id of the group in which jobs we're interested.
|
|
97
214
|
# @return [Boolean] should we keep waiting or not
|
|
98
215
|
# @note We do not wait for non-blocking jobs. Their flow should allow for `poll` running
|
|
99
216
|
# as they may exceed `max.poll.interval`
|
|
100
217
|
def wait?(group_id)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
218
|
+
return true unless @in_processing[group_id].all?(&:non_blocking?)
|
|
219
|
+
return true unless @in_waiting[group_id].all?(&:non_blocking?)
|
|
220
|
+
|
|
221
|
+
locked_async?(group_id)
|
|
105
222
|
end
|
|
106
223
|
end
|
|
107
224
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
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 Processing
|
|
17
|
+
# Namespace for periodic jobs related processing APIs
|
|
18
|
+
module PeriodicJob
|
|
19
|
+
# Consumer extra methods useful only when periodic jobs are in use
|
|
20
|
+
module Consumer
|
|
21
|
+
class << self
|
|
22
|
+
# Defines an empty `#tick` method if not present
|
|
23
|
+
#
|
|
24
|
+
# We define it that way due to our injection strategy flow.
|
|
25
|
+
#
|
|
26
|
+
# @param consumer_singleton_class [Karafka::BaseConsumer] consumer singleton class
|
|
27
|
+
# that is being enriched with periodic jobs API
|
|
28
|
+
def included(consumer_singleton_class)
|
|
29
|
+
# Do not define empty tick method on consumer if it already exists
|
|
30
|
+
# We only define it when it does not exist to have empty periodic ticking
|
|
31
|
+
#
|
|
32
|
+
# We need to check both cases (public and private) since user is not expected to
|
|
33
|
+
# have this method public
|
|
34
|
+
return if consumer_singleton_class.instance_methods.include?(:tick)
|
|
35
|
+
return if consumer_singleton_class.private_instance_methods.include?(:tick)
|
|
36
|
+
|
|
37
|
+
# Create empty ticking method
|
|
38
|
+
consumer_singleton_class.class_eval do
|
|
39
|
+
def tick; end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Runs the on-schedule tick periodic operations
|
|
45
|
+
# This method is an alias but is part of the naming convention used for other flows, this
|
|
46
|
+
# is why we do not reference the `handle_before_schedule_tick` directly
|
|
47
|
+
def on_before_schedule_tick
|
|
48
|
+
handle_before_schedule_tick
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Used by the executor to trigger consumer tick
|
|
52
|
+
# @private
|
|
53
|
+
def on_tick
|
|
54
|
+
handle_tick
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
Karafka.monitor.instrument(
|
|
57
|
+
'error.occurred',
|
|
58
|
+
error: e,
|
|
59
|
+
caller: self,
|
|
60
|
+
type: 'consumer.tick.error'
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
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 Processing
|
|
17
|
+
# All code needed for messages piping in Karafka
|
|
18
|
+
module Piping
|
|
19
|
+
# Consumer piping functionality
|
|
20
|
+
#
|
|
21
|
+
# It provides way to pipe data in a consistent way with extra traceability headers similar
|
|
22
|
+
# to those in the enhanced DLQ.
|
|
23
|
+
module Consumer
|
|
24
|
+
# Empty hash to save on memory allocations
|
|
25
|
+
EMPTY_HASH = {}.freeze
|
|
26
|
+
|
|
27
|
+
private_constant :EMPTY_HASH
|
|
28
|
+
|
|
29
|
+
# Pipes given message to the provided topic with expected details. Useful for
|
|
30
|
+
# pass-through operations where deserialization is not needed. Upon usage it will include
|
|
31
|
+
# all the original headers + meta headers about the source of message.
|
|
32
|
+
#
|
|
33
|
+
# @param topic [String, Symbol] where we want to send the message
|
|
34
|
+
# @param message [Karafka::Messages::Message] original message to pipe
|
|
35
|
+
#
|
|
36
|
+
# @note It will NOT deserialize the payload so it is fast
|
|
37
|
+
#
|
|
38
|
+
# @note We assume that there can be different number of partitions in the target topic,
|
|
39
|
+
# this is why we use `key` based on the original topic partition number and not the
|
|
40
|
+
# partition id itself. This will not utilize partitions beyond the number of partitions
|
|
41
|
+
# of original topic, but will accommodate for topics with less partitions.
|
|
42
|
+
def pipe_async(topic:, message:)
|
|
43
|
+
produce_async(
|
|
44
|
+
build_pipe_message(topic: topic, message: message)
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Sync version of pipe for one message
|
|
49
|
+
#
|
|
50
|
+
# @param topic [String, Symbol] where we want to send the message
|
|
51
|
+
# @param message [Karafka::Messages::Message] original message to pipe
|
|
52
|
+
# @see [#pipe_async]
|
|
53
|
+
def pipe_sync(topic:, message:)
|
|
54
|
+
produce_sync(
|
|
55
|
+
build_pipe_message(topic: topic, message: message)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Async multi-message pipe
|
|
60
|
+
#
|
|
61
|
+
# @param topic [String, Symbol] where we want to send the message
|
|
62
|
+
# @param messages [Array<Karafka::Messages::Message>] original messages to pipe
|
|
63
|
+
#
|
|
64
|
+
# @note If transactional producer in use and dispatch is not wrapped with a transaction,
|
|
65
|
+
# it will automatically wrap the dispatch with a transaction
|
|
66
|
+
def pipe_many_async(topic:, messages:)
|
|
67
|
+
messages = messages.map do |message|
|
|
68
|
+
build_pipe_message(topic: topic, message: message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
produce_many_async(messages)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Sync multi-message pipe
|
|
75
|
+
#
|
|
76
|
+
# @param topic [String, Symbol] where we want to send the message
|
|
77
|
+
# @param messages [Array<Karafka::Messages::Message>] original messages to pipe
|
|
78
|
+
#
|
|
79
|
+
# @note If transactional producer in use and dispatch is not wrapped with a transaction,
|
|
80
|
+
# it will automatically wrap the dispatch with a transaction
|
|
81
|
+
def pipe_many_sync(topic:, messages:)
|
|
82
|
+
messages = messages.map do |message|
|
|
83
|
+
build_pipe_message(topic: topic, message: message)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
produce_many_sync(messages)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# @param topic [String, Symbol] where we want to send the message
|
|
92
|
+
# @param message [Karafka::Messages::Message] original message to pipe
|
|
93
|
+
# @return [Hash] hash with message to pipe.
|
|
94
|
+
#
|
|
95
|
+
# @note If you need to alter this, please define the `#enhance_pipe_message` method
|
|
96
|
+
def build_pipe_message(topic:, message:)
|
|
97
|
+
original_partition = message.partition.to_s
|
|
98
|
+
|
|
99
|
+
pipe_message = {
|
|
100
|
+
topic: topic,
|
|
101
|
+
key: original_partition,
|
|
102
|
+
payload: message.raw_payload,
|
|
103
|
+
headers: message.headers.merge(
|
|
104
|
+
'original_topic' => message.topic,
|
|
105
|
+
'original_partition' => original_partition,
|
|
106
|
+
'original_offset' => message.offset.to_s,
|
|
107
|
+
'original_consumer_group' => self.topic.consumer_group.id
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Optional method user can define in consumer to enhance the dlq message hash with
|
|
112
|
+
# some extra details if needed or to replace payload, etc
|
|
113
|
+
if respond_to?(:enhance_pipe_message, true)
|
|
114
|
+
enhance_pipe_message(
|
|
115
|
+
pipe_message,
|
|
116
|
+
message
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
pipe_message
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -57,7 +57,7 @@ module Karafka
|
|
|
57
57
|
# We can commit the offset here because we know that we skip it "forever" and
|
|
58
58
|
# since AJ consumer commits the offset after each job, we also know that the
|
|
59
59
|
# previous job was successful
|
|
60
|
-
|
|
60
|
+
mark_dispatched_to_dlq(skippable_message)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -55,7 +55,7 @@ module Karafka
|
|
|
55
55
|
# We can commit the offset here because we know that we skip it "forever" and
|
|
56
56
|
# since AJ consumer commits the offset after each job, we also know that the
|
|
57
57
|
# previous job was successful
|
|
58
|
-
|
|
58
|
+
mark_dispatched_to_dlq(skippable_message)
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -49,7 +49,7 @@ module Karafka
|
|
|
49
49
|
# We can commit the offset here because we know that we skip it "forever" and
|
|
50
50
|
# since AJ consumer commits the offset after each job, we also know that the
|
|
51
51
|
# previous job was successful
|
|
52
|
-
|
|
52
|
+
mark_dispatched_to_dlq(skippable_message)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -220,7 +220,7 @@ module Karafka
|
|
|
220
220
|
ensure
|
|
221
221
|
# We need to decrease number of jobs that this coordinator coordinates as it has
|
|
222
222
|
# finished
|
|
223
|
-
coordinator.decrement
|
|
223
|
+
coordinator.decrement(:consume)
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
# Standard flow without any features
|
|
@@ -254,6 +254,8 @@ module Karafka
|
|
|
254
254
|
Karafka.monitor.instrument('consumer.revoked', caller: self) do
|
|
255
255
|
revoked
|
|
256
256
|
end
|
|
257
|
+
ensure
|
|
258
|
+
coordinator.decrement(:revoked)
|
|
257
259
|
end
|
|
258
260
|
|
|
259
261
|
# No action needed for the tick standard flow
|
|
@@ -269,6 +271,8 @@ module Karafka
|
|
|
269
271
|
Karafka.monitor.instrument('consumer.ticked', caller: self) do
|
|
270
272
|
tick
|
|
271
273
|
end
|
|
274
|
+
ensure
|
|
275
|
+
coordinator.decrement(:periodic)
|
|
272
276
|
end
|
|
273
277
|
end
|
|
274
278
|
end
|
|
@@ -111,7 +111,8 @@ module Karafka
|
|
|
111
111
|
# should not be cleaned as it should go to the DLQ
|
|
112
112
|
raise(Cleaner::Errors::MessageCleanedError) if skippable_message.cleaned?
|
|
113
113
|
|
|
114
|
-
producer.
|
|
114
|
+
producer.public_send(
|
|
115
|
+
topic.dead_letter_queue.dispatch_method,
|
|
115
116
|
build_dlq_message(
|
|
116
117
|
skippable_message
|
|
117
118
|
)
|
|
@@ -134,7 +135,7 @@ module Karafka
|
|
|
134
135
|
|
|
135
136
|
dispatch = lambda do
|
|
136
137
|
dispatch_to_dlq(skippable_message) if dispatch_to_dlq?
|
|
137
|
-
|
|
138
|
+
mark_dispatched_to_dlq(skippable_message)
|
|
138
139
|
end
|
|
139
140
|
|
|
140
141
|
if dispatch_in_a_transaction?
|
|
@@ -157,7 +158,8 @@ module Karafka
|
|
|
157
158
|
'original_topic' => topic.name,
|
|
158
159
|
'original_partition' => original_partition,
|
|
159
160
|
'original_offset' => skippable_message.offset.to_s,
|
|
160
|
-
'original_consumer_group' => topic.consumer_group.id
|
|
161
|
+
'original_consumer_group' => topic.consumer_group.id,
|
|
162
|
+
'original_attempts' => attempt.to_s
|
|
161
163
|
)
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -210,14 +212,28 @@ module Karafka
|
|
|
210
212
|
raise Karafka::UnsupportedCaseError, flow
|
|
211
213
|
end
|
|
212
214
|
|
|
215
|
+
yield
|
|
216
|
+
|
|
213
217
|
# We reset the pause to indicate we will now consider it as "ok".
|
|
214
218
|
coordinator.pause_tracker.reset
|
|
215
219
|
|
|
216
|
-
yield
|
|
217
|
-
|
|
218
220
|
# Always backoff after DLQ dispatch even on skip to prevent overloads on errors
|
|
219
221
|
pause(coordinator.seek_offset, nil, false)
|
|
220
222
|
end
|
|
223
|
+
|
|
224
|
+
# Marks message that went to DLQ (if applicable) based on the requested method
|
|
225
|
+
# @param skippable_message [Karafka::Messages::Message]
|
|
226
|
+
def mark_dispatched_to_dlq(skippable_message)
|
|
227
|
+
case topic.dead_letter_queue.marking_method
|
|
228
|
+
when :mark_as_consumed
|
|
229
|
+
mark_as_consumed(skippable_message)
|
|
230
|
+
when :mark_as_consumed!
|
|
231
|
+
mark_as_consumed!(skippable_message)
|
|
232
|
+
else
|
|
233
|
+
# This should never happen. Bug if encountered. Please report
|
|
234
|
+
raise Karafka::Errors::UnsupportedCaseError
|
|
235
|
+
end
|
|
236
|
+
end
|
|
221
237
|
end
|
|
222
238
|
end
|
|
223
239
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
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 Processing
|
|
17
|
+
# Uses the jobs queue API to lock (pause) and unlock (resume) operations of a given
|
|
18
|
+
# subscription group. It is abstracted away from jobs queue on this layer because we do
|
|
19
|
+
# not want to introduce jobs queue as a concept to the consumers layer
|
|
20
|
+
class SubscriptionGroupsCoordinator
|
|
21
|
+
include Singleton
|
|
22
|
+
|
|
23
|
+
# @param subscription_group [Karafka::Routing::SubscriptionGroup] subscription group we
|
|
24
|
+
# want to pause
|
|
25
|
+
# @param lock_id [Object] key we want to use if we want to set multiple locks on the same
|
|
26
|
+
# subscription group
|
|
27
|
+
# @param kwargs [Object] Any keyword arguments accepted by the jobs queue lock.
|
|
28
|
+
def pause(subscription_group, lock_id = nil, **kwargs)
|
|
29
|
+
jobs_queue.lock_async(
|
|
30
|
+
subscription_group.id,
|
|
31
|
+
lock_id,
|
|
32
|
+
**kwargs
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param subscription_group [Karafka::Routing::SubscriptionGroup] subscription group we
|
|
37
|
+
# want to resume
|
|
38
|
+
# @param lock_id [Object] lock id (if it was used to pause)
|
|
39
|
+
def resume(subscription_group, lock_id = nil)
|
|
40
|
+
jobs_queue.unlock_async(subscription_group.id, lock_id)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# @return [Karafka::Pro::Processing::JobsQueue]
|
|
46
|
+
def jobs_queue
|
|
47
|
+
@jobs_queue ||= Karafka::Server.jobs_queue
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|