karafka 2.2.13 → 2.3.0.alpha1
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 +0 -0
- data/.github/workflows/ci.yml +38 -12
- data/.ruby-version +1 -1
- data/CHANGELOG.md +161 -125
- data/Gemfile.lock +12 -12
- data/README.md +0 -2
- data/SECURITY.md +23 -0
- data/config/locales/errors.yml +7 -1
- data/config/locales/pro_errors.yml +22 -0
- data/docker-compose.yml +3 -1
- data/karafka.gemspec +2 -2
- data/lib/karafka/admin/acl.rb +287 -0
- data/lib/karafka/admin.rb +118 -16
- data/lib/karafka/app.rb +12 -3
- data/lib/karafka/base_consumer.rb +32 -31
- data/lib/karafka/cli/base.rb +1 -1
- data/lib/karafka/connection/client.rb +94 -84
- data/lib/karafka/connection/conductor.rb +28 -0
- data/lib/karafka/connection/listener.rb +165 -46
- 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 +13 -0
- data/lib/karafka/instrumentation/assignments_tracker.rb +96 -0
- data/lib/karafka/instrumentation/callbacks/rebalance.rb +10 -7
- data/lib/karafka/instrumentation/logger_listener.rb +3 -9
- data/lib/karafka/instrumentation/notifications.rb +19 -9
- data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +31 -28
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +22 -3
- 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 +300 -0
- data/lib/karafka/pro/connection/multiplexing/listener.rb +40 -0
- data/lib/karafka/pro/instrumentation/performance_tracker.rb +85 -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 +16 -2
- 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 +143 -0
- data/lib/karafka/pro/processing/schedulers/default.rb +107 -0
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +1 -1
- data/lib/karafka/pro/processing/strategies/default.rb +136 -3
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +35 -0
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +1 -1
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +1 -1
- data/lib/karafka/pro/processing/strategies/vp/default.rb +60 -26
- 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 +43 -13
- data/lib/karafka/processing/executors_buffer.rb +22 -7
- data/lib/karafka/processing/jobs/base.rb +19 -2
- data/lib/karafka/processing/jobs/consume.rb +3 -3
- data/lib/karafka/processing/jobs/idle.rb +5 -0
- data/lib/karafka/processing/jobs/revoked.rb +5 -0
- data/lib/karafka/processing/jobs/shutdown.rb +5 -0
- data/lib/karafka/processing/jobs_queue.rb +19 -8
- data/lib/karafka/processing/schedulers/default.rb +42 -0
- data/lib/karafka/processing/strategies/base.rb +13 -4
- data/lib/karafka/processing/strategies/default.rb +23 -7
- data/lib/karafka/processing/strategies/dlq.rb +36 -0
- data/lib/karafka/processing/worker.rb +4 -1
- 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/proxy.rb +4 -3
- 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/routing/topics.rb +1 -1
- data/lib/karafka/runner.rb +13 -3
- data/lib/karafka/server.rb +5 -9
- data/lib/karafka/setup/config.rb +21 -1
- 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 +47 -13
- metadata.gz.sig +0 -0
- data/lib/karafka/connection/consumer_group_coordinator.rb +0 -48
- data/lib/karafka/pro/performance_tracker.rb +0 -84
- data/lib/karafka/pro/processing/scheduler.rb +0 -74
- data/lib/karafka/processing/scheduler.rb +0 -38
@@ -0,0 +1,300 @@
|
|
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 Connection
|
17
|
+
# Manager that can handle working with multiplexed connections.
|
18
|
+
#
|
19
|
+
# This manager takes into consideration the number of partitions assigned to the topics and
|
20
|
+
# does its best to balance. Additional connections may not always be utilized because
|
21
|
+
# alongside of them, other processes may "hijack" the assignment. In such cases those extra
|
22
|
+
# empty connections will be turned off after a while.
|
23
|
+
#
|
24
|
+
# @note Manager operations relate to consumer groups and not subscription groups. Since
|
25
|
+
# cluster operations can cause consumer group wide effects, we always apply only one
|
26
|
+
# change on a consumer group.
|
27
|
+
#
|
28
|
+
# @note Since we collect statistical data from listeners and this happens in a background
|
29
|
+
# thread, we need to make sure we lock not to have race conditions with expired data
|
30
|
+
# eviction.
|
31
|
+
class Manager < Karafka::Connection::Manager
|
32
|
+
include Core::Helpers::Time
|
33
|
+
|
34
|
+
# How long should we keep stale stats before evicting them completely
|
35
|
+
EVICTION_DELAY = 5 * 60 * 1_000
|
36
|
+
|
37
|
+
private_constant :EVICTION_DELAY
|
38
|
+
|
39
|
+
# How long should we wait after a rebalance before doing anything on a consumer group
|
40
|
+
#
|
41
|
+
# @param scale_delay [Integer] How long should we wait before making any changes. Any
|
42
|
+
# change related to this consumer group will postpone the scaling operations. This is
|
43
|
+
# done that way to prevent too many friction in the cluster. It is 1 minute by default
|
44
|
+
def initialize(scale_delay = 60 * 1_000)
|
45
|
+
super()
|
46
|
+
@scale_delay = scale_delay
|
47
|
+
@mutex = Mutex.new
|
48
|
+
@changes = Hash.new do |h, k|
|
49
|
+
h[k] = {
|
50
|
+
state: '',
|
51
|
+
join_state: '',
|
52
|
+
state_age: 0,
|
53
|
+
state_age_sync: monotonic_now,
|
54
|
+
changed_at: monotonic_now
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Registers listeners and starts the scaling procedures
|
60
|
+
#
|
61
|
+
# When using dynamic multiplexing, it will start the absolute minimum of connections for
|
62
|
+
# subscription group available.
|
63
|
+
#
|
64
|
+
# @param listeners [Connection::ListenersBatch]
|
65
|
+
def register(listeners)
|
66
|
+
@listeners = listeners
|
67
|
+
|
68
|
+
in_sg_families do |first_subscription_group, sg_listeners|
|
69
|
+
multiplexing = first_subscription_group.multiplexing
|
70
|
+
|
71
|
+
if multiplexing.active? && multiplexing.dynamic?
|
72
|
+
# Start as many boot listeners as user wants. If not configured, starts half of max.
|
73
|
+
sg_listeners.first(multiplexing.boot).each(&:start!)
|
74
|
+
else
|
75
|
+
sg_listeners.each(&:start!)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Collects data from the statistics about given subscription group. This is used to ensure
|
81
|
+
# that we do not rescale short after rebalances, deployments, etc.
|
82
|
+
# @param subscription_group_id [String] id of the subscription group for which statistics
|
83
|
+
# were emitted
|
84
|
+
# @param statistics [Hash] emitted statistics
|
85
|
+
#
|
86
|
+
# @note Please note that while we collect here per subscription group, we use those metrics
|
87
|
+
# collectively on a whole consumer group. This reduces the friction.
|
88
|
+
def notice(subscription_group_id, statistics)
|
89
|
+
@mutex.synchronize do
|
90
|
+
times = []
|
91
|
+
# stateage is in microseconds
|
92
|
+
# We monitor broker changes to make sure we do not introduce extra friction
|
93
|
+
times << statistics['brokers'].values.map { |stats| stats['stateage'] }.min / 1_000
|
94
|
+
times << statistics['cgrp']['rebalance_age']
|
95
|
+
times << statistics['cgrp']['stateage']
|
96
|
+
|
97
|
+
# Keep the previous change age for changes that were triggered by us
|
98
|
+
previous_changed_at = @changes[subscription_group_id][:changed_at]
|
99
|
+
|
100
|
+
@changes[subscription_group_id] = {
|
101
|
+
state_age: times.min,
|
102
|
+
changed_at: previous_changed_at,
|
103
|
+
join_state: statistics['cgrp']['join_state'],
|
104
|
+
state: statistics['cgrp']['state'],
|
105
|
+
state_age_sync: monotonic_now
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Shuts down all the listeners when it is time (including moving to quiet) or rescales
|
111
|
+
# when it is needed
|
112
|
+
def control
|
113
|
+
Karafka::App.done? ? shutdown : rescale
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Handles the shutdown and quiet flows
|
119
|
+
def shutdown
|
120
|
+
active_listeners = @listeners.active
|
121
|
+
|
122
|
+
# When we are done processing immediately quiet all the listeners so they do not pick up
|
123
|
+
# new work to do
|
124
|
+
once(:quiet!) { active_listeners.each(&:quiet!) }
|
125
|
+
|
126
|
+
# If we are in the process of moving to quiet state, we need to check it.
|
127
|
+
if Karafka::App.quieting? && active_listeners.all?(&:quiet?)
|
128
|
+
once(:quieted!) { Karafka::App.quieted! }
|
129
|
+
end
|
130
|
+
|
131
|
+
return if Karafka::App.quiet?
|
132
|
+
|
133
|
+
# Since separate subscription groups are subscribed to different topics, there is no risk
|
134
|
+
# in shutting them down independently even if they operate in the same subscription group
|
135
|
+
in_sg_families do |first_subscription_group, sg_listeners|
|
136
|
+
active_sg_listeners = sg_listeners.select(&:active?)
|
137
|
+
|
138
|
+
# Do nothing until all listeners from the same consumer group are quiet. Otherwise we
|
139
|
+
# could have problems with in-flight rebalances during shutdown
|
140
|
+
next unless active_sg_listeners.all?(&:quiet?)
|
141
|
+
|
142
|
+
# Do not stop the same family twice
|
143
|
+
once(:stop!, first_subscription_group.name) { active_sg_listeners.each(&:stop!) }
|
144
|
+
end
|
145
|
+
|
146
|
+
return unless @listeners.active.all?(&:stopped?)
|
147
|
+
|
148
|
+
# All listeners including pending need to be moved at the end to stopped state for
|
149
|
+
# the whole server to stop
|
150
|
+
once(:stop!) { @listeners.each(&:stopped!) }
|
151
|
+
end
|
152
|
+
|
153
|
+
# Handles two scenarios:
|
154
|
+
# - Selects subscriptions that could benefit from having more parallel connections
|
155
|
+
# to kafka and then upscales them
|
156
|
+
# - Selects subscriptions that are idle (have nothing subscribed to them) and then shuts
|
157
|
+
# them down
|
158
|
+
#
|
159
|
+
# We always run scaling down and up because it may be applicable to different CGs
|
160
|
+
def rescale
|
161
|
+
evict
|
162
|
+
|
163
|
+
scale_down
|
164
|
+
scale_up
|
165
|
+
end
|
166
|
+
|
167
|
+
# Checks for connections without any assignments and scales them down.
|
168
|
+
# Does that only for dynamically multiplexed subscription groups
|
169
|
+
def scale_down
|
170
|
+
sgs_in_use = Karafka::App.assignments.keys.map(&:subscription_group).uniq
|
171
|
+
|
172
|
+
# Select connections for scaling down
|
173
|
+
in_sg_families do |first_subscription_group, sg_listeners|
|
174
|
+
next unless stable?(sg_listeners)
|
175
|
+
|
176
|
+
multiplexing = first_subscription_group.multiplexing
|
177
|
+
|
178
|
+
next unless multiplexing.active?
|
179
|
+
next unless multiplexing.dynamic?
|
180
|
+
|
181
|
+
# If we cannot downscale, do not
|
182
|
+
next if sg_listeners.count(&:active?) <= multiplexing.min
|
183
|
+
|
184
|
+
sg_listeners.each do |sg_listener|
|
185
|
+
# Do not stop connections with subscriptions
|
186
|
+
next if sgs_in_use.include?(sg_listener.subscription_group)
|
187
|
+
# Skip listeners that are already in standby
|
188
|
+
next unless sg_listener.active?
|
189
|
+
|
190
|
+
touch(sg_listener.subscription_group.id)
|
191
|
+
|
192
|
+
# Shut down not used connection
|
193
|
+
sg_listener.stop!
|
194
|
+
|
195
|
+
break
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Checks if we have space to scale and if there are any assignments with multiple topics
|
201
|
+
# partitions assigned in sgs that can be scaled. If that is the case, we scale up.
|
202
|
+
def scale_up
|
203
|
+
multi_part_sgs_families = Karafka::App
|
204
|
+
.assignments
|
205
|
+
.select { |_, partitions| partitions.size > 1 }
|
206
|
+
.keys
|
207
|
+
.map(&:subscription_group)
|
208
|
+
.map(&:name)
|
209
|
+
.uniq
|
210
|
+
|
211
|
+
# Select connections for scaling up
|
212
|
+
in_sg_families do |first_subscription_group, sg_listeners|
|
213
|
+
next unless stable?(sg_listeners)
|
214
|
+
|
215
|
+
multiplexing = first_subscription_group.multiplexing
|
216
|
+
|
217
|
+
next unless multiplexing.active?
|
218
|
+
next unless multiplexing.dynamic?
|
219
|
+
# If we cannot downscale, do not
|
220
|
+
next if sg_listeners.count(&:active?) >= multiplexing.max
|
221
|
+
|
222
|
+
sg_listeners.each do |sg_listener|
|
223
|
+
next unless multi_part_sgs_families.include?(sg_listener.subscription_group.name)
|
224
|
+
# Skip already active connections
|
225
|
+
next unless sg_listener.pending? || sg_listener.stopped?
|
226
|
+
|
227
|
+
touch(sg_listener.subscription_group.id)
|
228
|
+
sg_listener.start!
|
229
|
+
|
230
|
+
break
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Removes states that are no longer being reported for stopped/pending listeners
|
236
|
+
def evict
|
237
|
+
@mutex.synchronize do
|
238
|
+
@changes.delete_if do |_, details|
|
239
|
+
monotonic_now - details[:state_age_sync] >= EVICTION_DELAY
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Indicates, that something has changed on a subscription group. We consider every single
|
245
|
+
# change we make as a change to the setup as well.
|
246
|
+
# @param subscription_group_id [String]
|
247
|
+
def touch(subscription_group_id)
|
248
|
+
@mutex.synchronize do
|
249
|
+
@changes[subscription_group_id][:changed_at] = 0
|
250
|
+
@changes[subscription_group_id][:state_age_sync] = monotonic_now
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# @param sg_listeners [Array<Listener>] listeners from one multiplexed sg
|
255
|
+
# @return [Boolean] is given subscription group listeners set stable. It is considered
|
256
|
+
# stable when it had no changes happening on it recently and all relevant states in it
|
257
|
+
# are also stable. This is a strong indicator that no rebalances or other operations are
|
258
|
+
# happening at a given moment.
|
259
|
+
def stable?(sg_listeners)
|
260
|
+
# If none of listeners has changes reported it means we did not yet start collecting
|
261
|
+
# metrics about any of them and at least one must be present. We do not consider it
|
262
|
+
# stable in such case as we still are waiting for metrics.
|
263
|
+
return false if sg_listeners.none? do |sg_listener|
|
264
|
+
@changes.key?(sg_listener.subscription_group.id)
|
265
|
+
end
|
266
|
+
|
267
|
+
sg_listeners.all? do |sg_listener|
|
268
|
+
# Not all SGs may be started initially or may be stopped, we ignore them here as they
|
269
|
+
# are irrelevant from the point of view of establishing stability
|
270
|
+
next true unless @changes.key?(sg_listener.subscription_group.id)
|
271
|
+
|
272
|
+
state = @changes[sg_listener.subscription_group.id]
|
273
|
+
|
274
|
+
state[:state_age] >= @scale_delay &&
|
275
|
+
(monotonic_now - state[:changed_at]) >= @scale_delay &&
|
276
|
+
state[:state] == 'up' &&
|
277
|
+
state[:join_state] == 'steady'
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Yields listeners in groups based on their subscription groups
|
282
|
+
# @yieldparam [Karafka::Routing::SubscriptionGroup] first subscription group out of the
|
283
|
+
# family
|
284
|
+
# @yieldparam [Array<Listener>] listeners of a single subscription group
|
285
|
+
def in_sg_families
|
286
|
+
grouped = @listeners.group_by { |listener| listener.subscription_group.name }
|
287
|
+
|
288
|
+
grouped.each_value do |listeners|
|
289
|
+
listener = listeners.first
|
290
|
+
|
291
|
+
yield(
|
292
|
+
listener.subscription_group,
|
293
|
+
listeners
|
294
|
+
)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
+
# Namespace for Pro connections related components
|
17
|
+
module Connection
|
18
|
+
# Namespace for Multiplexing management related components
|
19
|
+
module Multiplexing
|
20
|
+
# Listener used to connect listeners manager to the lifecycle events that are significant
|
21
|
+
# to its operations
|
22
|
+
class Listener
|
23
|
+
def initialize
|
24
|
+
@manager = App.config.internal.connection.manager
|
25
|
+
end
|
26
|
+
|
27
|
+
# Triggers connection manage subscription groups details noticing
|
28
|
+
#
|
29
|
+
# @param event [Karafka::Core::Monitoring::Event] event with statistics
|
30
|
+
def on_statistics_emitted(event)
|
31
|
+
@manager.notice(
|
32
|
+
event[:subscription_group_id],
|
33
|
+
event[:statistics]
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,85 @@
|
|
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
|
+
# Namespace for Pro components instrumentation related code
|
17
|
+
module Instrumentation
|
18
|
+
# Tracker used to keep track of performance metrics
|
19
|
+
# It provides insights that can be used to optimize processing flow
|
20
|
+
# @note Even if we have some race-conditions here it is relevant due to the quantity of data.
|
21
|
+
# This is why we do not mutex it.
|
22
|
+
class PerformanceTracker
|
23
|
+
include Singleton
|
24
|
+
|
25
|
+
# How many samples do we collect per topic partition
|
26
|
+
SAMPLES_COUNT = 200
|
27
|
+
|
28
|
+
private_constant :SAMPLES_COUNT
|
29
|
+
|
30
|
+
# Builds up nested concurrent hash for data tracking
|
31
|
+
def initialize
|
32
|
+
@processing_times = Hash.new do |topics_hash, topic|
|
33
|
+
topics_hash[topic] = Hash.new do |partitions_hash, partition|
|
34
|
+
partitions_hash[partition] = []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param topic [String]
|
40
|
+
# @param partition [Integer]
|
41
|
+
# @return [Float] p95 processing time of a single message from a single topic partition
|
42
|
+
def processing_time_p95(topic, partition)
|
43
|
+
values = @processing_times[topic][partition]
|
44
|
+
|
45
|
+
return 0 if values.empty?
|
46
|
+
return values.first if values.size == 1
|
47
|
+
|
48
|
+
percentile(0.95, values)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @private
|
52
|
+
# @param event [Karafka::Core::Monitoring::Event] event details
|
53
|
+
# Tracks time taken to process a single message of a given topic partition
|
54
|
+
def on_consumer_consumed(event)
|
55
|
+
consumer = event[:caller]
|
56
|
+
messages = consumer.messages
|
57
|
+
topic = messages.metadata.topic
|
58
|
+
partition = messages.metadata.partition
|
59
|
+
|
60
|
+
samples = @processing_times[topic][partition]
|
61
|
+
samples << event[:time] / messages.count
|
62
|
+
|
63
|
+
return unless samples.size > SAMPLES_COUNT
|
64
|
+
|
65
|
+
samples.shift
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Computers the requested percentile out of provided values
|
71
|
+
# @param percentile [Float]
|
72
|
+
# @param values [Array<String>] all the values based on which we should
|
73
|
+
# @return [Float] computed percentile
|
74
|
+
def percentile(percentile, values)
|
75
|
+
values_sorted = values.sort
|
76
|
+
|
77
|
+
floor = (percentile * (values_sorted.length - 1) + 1).floor - 1
|
78
|
+
mod = (percentile * (values_sorted.length - 1) + 1).modulo(1)
|
79
|
+
|
80
|
+
values_sorted[floor] + (mod * (values_sorted[floor + 1] - values_sorted[floor]))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -25,7 +25,7 @@ module Karafka
|
|
25
25
|
# @param consumer [::Rdkafka::Consumer] consumer instance needed to talk with Kafka
|
26
26
|
# @param expanded_topics [Hash] hash with expanded and normalized topics data
|
27
27
|
def initialize(consumer, expanded_topics)
|
28
|
-
@consumer = Connection::Proxy.new(consumer)
|
28
|
+
@consumer = ::Karafka::Connection::Proxy.new(consumer)
|
29
29
|
@expanded_topics = expanded_topics
|
30
30
|
@mapped_topics = Hash.new { |h, k| h[k] = {} }
|
31
31
|
end
|
data/lib/karafka/pro/iterator.rb
CHANGED
@@ -22,11 +22,6 @@ module Karafka
|
|
22
22
|
#
|
23
23
|
# It does **not** create a consumer group and does not have any offset management.
|
24
24
|
class Iterator
|
25
|
-
# Local partition reference for librdkafka
|
26
|
-
Partition = Struct.new(:partition, :offset)
|
27
|
-
|
28
|
-
private_constant :Partition
|
29
|
-
|
30
25
|
# A simple API allowing to iterate over topic/partition data, without having to subscribe
|
31
26
|
# and deal with rebalances. This API allows for multi-partition streaming and is optimized
|
32
27
|
# for data lookups. It allows for explicit stopping iteration over any partition during
|
@@ -127,7 +122,7 @@ module Karafka
|
|
127
122
|
|
128
123
|
@current_consumer.pause(
|
129
124
|
Rdkafka::Consumer::TopicPartitionList.new(
|
130
|
-
name => [Partition.new(partition, 0)]
|
125
|
+
name => [Rdkafka::Consumer::Partition.new(partition, 0)]
|
131
126
|
)
|
132
127
|
)
|
133
128
|
end
|
data/lib/karafka/pro/loader.rb
CHANGED
@@ -53,6 +53,7 @@ module Karafka
|
|
53
53
|
features.each { |feature| feature.pre_setup(config) }
|
54
54
|
|
55
55
|
reconfigure(config)
|
56
|
+
expand
|
56
57
|
|
57
58
|
load_topic_features
|
58
59
|
end
|
@@ -82,18 +83,31 @@ module Karafka
|
|
82
83
|
|
83
84
|
icfg.cli.contract = Contracts::ServerCliOptions.new
|
84
85
|
|
86
|
+
# Use manager that supports multiplexing
|
87
|
+
icfg.connection.manager = Connection::Manager.new
|
88
|
+
|
85
89
|
icfg.processing.coordinator_class = Processing::Coordinator
|
86
90
|
icfg.processing.partitioner_class = Processing::Partitioner
|
87
|
-
icfg.processing.scheduler_class = Processing::
|
91
|
+
icfg.processing.scheduler_class = Processing::Schedulers::Default
|
88
92
|
icfg.processing.jobs_queue_class = Processing::JobsQueue
|
93
|
+
icfg.processing.executor_class = Processing::Executor
|
89
94
|
icfg.processing.jobs_builder = Processing::JobsBuilder.new
|
90
95
|
icfg.processing.strategy_selector = Processing::StrategySelector.new
|
96
|
+
icfg.processing.expansions_selector = Processing::ExpansionsSelector.new
|
91
97
|
|
92
98
|
icfg.active_job.consumer_class = ActiveJob::Consumer
|
93
99
|
icfg.active_job.dispatcher = ActiveJob::Dispatcher.new
|
94
100
|
icfg.active_job.job_options_contract = ActiveJob::JobOptionsContract.new
|
95
101
|
|
96
|
-
config.monitor.subscribe(PerformanceTracker.instance)
|
102
|
+
config.monitor.subscribe(Instrumentation::PerformanceTracker.instance)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Adds extra modules to certain classes
|
106
|
+
# This expands their functionalities with things that are needed when operating in Pro
|
107
|
+
# It is used only when given class is part of the end user API and cannot be swapped by
|
108
|
+
# a pluggable component
|
109
|
+
def expand
|
110
|
+
Karafka::BaseConsumer.include Pro::BaseConsumer
|
97
111
|
end
|
98
112
|
|
99
113
|
# Loads the Pro features of Karafka
|
@@ -42,7 +42,8 @@ module Karafka
|
|
42
42
|
|
43
43
|
@virtual_offset_manager = VirtualOffsetManager.new(
|
44
44
|
topic.name,
|
45
|
-
partition
|
45
|
+
partition,
|
46
|
+
topic.virtual_partitions.offset_metadata_strategy
|
46
47
|
)
|
47
48
|
|
48
49
|
# We register our own "internal" filter to support filtering of messages that were marked
|
@@ -0,0 +1,37 @@
|
|
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
|
+
# Pro executor that supports periodic jobs
|
18
|
+
class Executor < Karafka::Processing::Executor
|
19
|
+
# Runs the code that should happen before periodic job is scheduled
|
20
|
+
#
|
21
|
+
# @note While jobs are called `Periodic`, from the consumer perspective it is "ticking".
|
22
|
+
# This name was taken for a reason: we may want to introduce periodic ticking also not
|
23
|
+
# only during polling but for example on wait and a name "poll" would not align well.
|
24
|
+
# A name "periodic" is not a verb and our other consumer actions are verbs like:
|
25
|
+
# consume or revoked. So for the sake of consistency we have ticking here.
|
26
|
+
def before_schedule_periodic
|
27
|
+
consumer.on_before_schedule_tick
|
28
|
+
end
|
29
|
+
|
30
|
+
# Triggers consumer ticking
|
31
|
+
def periodic
|
32
|
+
consumer.on_tick
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
+
# Pro selector of appropriate topic setup based features enhancements.
|
18
|
+
class ExpansionsSelector < Karafka::Processing::ExpansionsSelector
|
19
|
+
# @param topic [Karafka::Routing::Topic] topic with settings based on which we find
|
20
|
+
# expansions
|
21
|
+
# @return [Array<Module>] modules with proper expansions we're suppose to use to enhance
|
22
|
+
# the consumer
|
23
|
+
def find(topic)
|
24
|
+
# Start with the non-pro expansions
|
25
|
+
expansions = super
|
26
|
+
expansions << Pro::Processing::OffsetMetadata::Consumer if topic.offset_metadata?
|
27
|
+
expansions
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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
|
+
module Jobs
|
18
|
+
# Job that represents a "ticking" work. Work that we run periodically for the Periodics
|
19
|
+
# enabled topics.
|
20
|
+
class Periodic < ::Karafka::Processing::Jobs::Base
|
21
|
+
# @param executor [Karafka::Pro::Processing::Executor] pro executor that is suppose to
|
22
|
+
# run a given job
|
23
|
+
def initialize(executor)
|
24
|
+
@executor = executor
|
25
|
+
super()
|
26
|
+
end
|
27
|
+
|
28
|
+
# Code executed before we schedule this job
|
29
|
+
def before_schedule
|
30
|
+
executor.before_schedule_periodic
|
31
|
+
end
|
32
|
+
|
33
|
+
# Runs the executor periodic action
|
34
|
+
def call
|
35
|
+
executor.periodic
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
+
module Jobs
|
18
|
+
# Non-Blocking version of the Periodic job
|
19
|
+
# We use this version for LRJ topics for cases where saturated resources would not allow
|
20
|
+
# to run this job for extended period of time. Under such scenarios, if we would not use
|
21
|
+
# a non-blocking one, we would reach max.poll.interval.ms.
|
22
|
+
class PeriodicNonBlocking < Periodic
|
23
|
+
# @param args [Array] any arguments accepted by `::Karafka::Processing::Jobs::Periodic`
|
24
|
+
def initialize(*args)
|
25
|
+
super
|
26
|
+
@non_blocking = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|