karafka 2.0.0.beta1 → 2.0.0.beta4
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 +9 -23
- data/CHANGELOG.md +47 -0
- data/Gemfile.lock +8 -8
- data/bin/integrations +36 -14
- data/bin/scenario +29 -0
- data/bin/wait_for_kafka +20 -0
- data/config/errors.yml +1 -0
- data/docker-compose.yml +12 -0
- data/karafka.gemspec +2 -2
- data/lib/active_job/karafka.rb +2 -2
- data/lib/karafka/active_job/routing/extensions.rb +31 -0
- data/lib/karafka/base_consumer.rb +65 -42
- data/lib/karafka/connection/client.rb +65 -19
- data/lib/karafka/connection/listener.rb +99 -34
- data/lib/karafka/connection/listeners_batch.rb +24 -0
- data/lib/karafka/connection/messages_buffer.rb +50 -54
- data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
- data/lib/karafka/contracts/config.rb +9 -1
- data/lib/karafka/helpers/async.rb +33 -0
- data/lib/karafka/instrumentation/logger_listener.rb +34 -10
- data/lib/karafka/instrumentation/monitor.rb +3 -1
- data/lib/karafka/licenser.rb +26 -7
- data/lib/karafka/messages/batch_metadata.rb +26 -3
- data/lib/karafka/messages/builders/batch_metadata.rb +17 -29
- data/lib/karafka/messages/builders/message.rb +1 -0
- data/lib/karafka/messages/builders/messages.rb +4 -12
- data/lib/karafka/pro/active_job/consumer.rb +49 -0
- data/lib/karafka/pro/active_job/dispatcher.rb +10 -10
- data/lib/karafka/pro/active_job/job_options_contract.rb +9 -9
- data/lib/karafka/pro/base_consumer.rb +76 -0
- data/lib/karafka/pro/loader.rb +30 -13
- data/lib/karafka/pro/performance_tracker.rb +9 -9
- data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +31 -0
- data/lib/karafka/pro/routing/extensions.rb +32 -0
- data/lib/karafka/pro/scheduler.rb +54 -0
- data/lib/karafka/processing/executor.rb +34 -7
- data/lib/karafka/processing/executors_buffer.rb +15 -7
- data/lib/karafka/processing/jobs/base.rb +21 -4
- data/lib/karafka/processing/jobs/consume.rb +12 -5
- data/lib/karafka/processing/jobs_builder.rb +28 -0
- data/lib/karafka/processing/jobs_queue.rb +15 -12
- data/lib/karafka/processing/result.rb +34 -0
- data/lib/karafka/processing/worker.rb +23 -17
- data/lib/karafka/processing/workers_batch.rb +5 -0
- data/lib/karafka/routing/consumer_group.rb +1 -1
- data/lib/karafka/routing/subscription_group.rb +2 -2
- data/lib/karafka/routing/subscription_groups_builder.rb +3 -2
- data/lib/karafka/routing/topic.rb +5 -0
- data/lib/karafka/routing/topics.rb +38 -0
- data/lib/karafka/runner.rb +19 -27
- data/lib/karafka/scheduler.rb +10 -11
- data/lib/karafka/server.rb +24 -23
- data/lib/karafka/setup/config.rb +4 -1
- data/lib/karafka/status.rb +1 -3
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +20 -5
- metadata.gz.sig +0 -0
- data/lib/karafka/active_job/routing_extensions.rb +0 -18
@@ -30,7 +30,7 @@ module Karafka
|
|
30
30
|
@mutex = Mutex.new
|
31
31
|
@closed = false
|
32
32
|
@subscription_group = subscription_group
|
33
|
-
@buffer =
|
33
|
+
@buffer = RawMessagesBuffer.new
|
34
34
|
@rebalance_manager = RebalanceManager.new
|
35
35
|
@kafka = build_consumer
|
36
36
|
# Marks if we need to offset. If we did not store offsets, we should not commit the offset
|
@@ -86,8 +86,7 @@ module Karafka
|
|
86
86
|
# @param message [Karafka::Messages::Message]
|
87
87
|
def store_offset(message)
|
88
88
|
@mutex.synchronize do
|
89
|
-
|
90
|
-
@kafka.store_offset(message)
|
89
|
+
internal_store_offset(message)
|
91
90
|
end
|
92
91
|
end
|
93
92
|
|
@@ -104,14 +103,7 @@ module Karafka
|
|
104
103
|
def commit_offsets(async: true)
|
105
104
|
@mutex.lock
|
106
105
|
|
107
|
-
|
108
|
-
|
109
|
-
@kafka.commit(nil, async)
|
110
|
-
@offsetting = false
|
111
|
-
rescue Rdkafka::RdkafkaError => e
|
112
|
-
return if e.code == :no_offset
|
113
|
-
|
114
|
-
raise e
|
106
|
+
internal_commit_offsets(async: async)
|
115
107
|
ensure
|
116
108
|
@mutex.unlock
|
117
109
|
end
|
@@ -128,7 +120,11 @@ module Karafka
|
|
128
120
|
#
|
129
121
|
# @param message [Messages::Message, Messages::Seek] message to which we want to seek to
|
130
122
|
def seek(message)
|
123
|
+
@mutex.lock
|
124
|
+
|
131
125
|
@kafka.seek(message)
|
126
|
+
ensure
|
127
|
+
@mutex.unlock
|
132
128
|
end
|
133
129
|
|
134
130
|
# Pauses given partition and moves back to last successful offset processed.
|
@@ -144,15 +140,17 @@ module Karafka
|
|
144
140
|
# Do not pause if the client got closed, would not change anything
|
145
141
|
return if @closed
|
146
142
|
|
143
|
+
pause_msg = Messages::Seek.new(topic, partition, offset)
|
144
|
+
|
145
|
+
internal_commit_offsets(async: false)
|
146
|
+
|
147
147
|
tpl = topic_partition_list(topic, partition)
|
148
148
|
|
149
149
|
return unless tpl
|
150
150
|
|
151
151
|
@kafka.pause(tpl)
|
152
152
|
|
153
|
-
|
154
|
-
|
155
|
-
seek(pause_msg)
|
153
|
+
@kafka.seek(pause_msg)
|
156
154
|
ensure
|
157
155
|
@mutex.unlock
|
158
156
|
end
|
@@ -166,6 +164,11 @@ module Karafka
|
|
166
164
|
|
167
165
|
return if @closed
|
168
166
|
|
167
|
+
# Always commit synchronously offsets if any when we resume
|
168
|
+
# This prevents resuming without offset in case it would not be committed prior
|
169
|
+
# We can skip performance penalty since resuming should not happen too often
|
170
|
+
internal_commit_offsets(async: false)
|
171
|
+
|
169
172
|
tpl = topic_partition_list(topic, partition)
|
170
173
|
|
171
174
|
return unless tpl
|
@@ -187,6 +190,7 @@ module Karafka
|
|
187
190
|
# Marks given message as consumed.
|
188
191
|
#
|
189
192
|
# @param [Karafka::Messages::Message] message that we want to mark as processed
|
193
|
+
# @return [Boolean] true if successful. False if we no longer own given partition
|
190
194
|
# @note This method won't trigger automatic offsets commits, rather relying on the offset
|
191
195
|
# check-pointing trigger that happens with each batch processed
|
192
196
|
def mark_as_consumed(message)
|
@@ -196,8 +200,10 @@ module Karafka
|
|
196
200
|
# Marks a given message as consumed and commits the offsets in a blocking way.
|
197
201
|
#
|
198
202
|
# @param [Karafka::Messages::Message] message that we want to mark as processed
|
203
|
+
# @return [Boolean] true if successful. False if we no longer own given partition
|
199
204
|
def mark_as_consumed!(message)
|
200
|
-
mark_as_consumed(message)
|
205
|
+
return false unless mark_as_consumed(message)
|
206
|
+
|
201
207
|
commit_offsets!
|
202
208
|
end
|
203
209
|
|
@@ -214,11 +220,44 @@ module Karafka
|
|
214
220
|
|
215
221
|
private
|
216
222
|
|
223
|
+
# When we cannot store an offset, it means we no longer own the partition
|
224
|
+
#
|
225
|
+
# Non thread-safe offset storing method
|
226
|
+
# @param message [Karafka::Messages::Message]
|
227
|
+
# @return [Boolean] true if we could store the offset (if we still own the partition)
|
228
|
+
def internal_store_offset(message)
|
229
|
+
@offsetting = true
|
230
|
+
@kafka.store_offset(message)
|
231
|
+
true
|
232
|
+
rescue Rdkafka::RdkafkaError => e
|
233
|
+
return false if e.code == :assignment_lost
|
234
|
+
return false if e.code == :state
|
235
|
+
|
236
|
+
raise e
|
237
|
+
end
|
238
|
+
|
239
|
+
# Non thread-safe message committing method
|
240
|
+
# @param async [Boolean] should the commit happen async or sync (async by default)
|
241
|
+
# @return [Boolean] true if offset commit worked, false if we've lost the assignment
|
242
|
+
def internal_commit_offsets(async: true)
|
243
|
+
return true unless @offsetting
|
244
|
+
|
245
|
+
@kafka.commit(nil, async)
|
246
|
+
@offsetting = false
|
247
|
+
|
248
|
+
true
|
249
|
+
rescue Rdkafka::RdkafkaError => e
|
250
|
+
return false if e.code == :assignment_lost
|
251
|
+
return false if e.code == :no_offset
|
252
|
+
|
253
|
+
raise e
|
254
|
+
end
|
255
|
+
|
217
256
|
# Commits the stored offsets in a sync way and closes the consumer.
|
218
257
|
def close
|
219
|
-
commit_offsets!
|
220
|
-
|
221
258
|
@mutex.synchronize do
|
259
|
+
internal_commit_offsets(async: false)
|
260
|
+
|
222
261
|
@closed = true
|
223
262
|
|
224
263
|
# Remove callbacks runners that were registered
|
@@ -227,7 +266,8 @@ module Karafka
|
|
227
266
|
|
228
267
|
@kafka.close
|
229
268
|
@buffer.clear
|
230
|
-
@
|
269
|
+
# @note We do not clear rebalance manager here as we may still have revocation info here
|
270
|
+
# that we want to consider valid prior to running another reconnection
|
231
271
|
end
|
232
272
|
end
|
233
273
|
|
@@ -280,7 +320,13 @@ module Karafka
|
|
280
320
|
|
281
321
|
time_poll.backoff
|
282
322
|
|
283
|
-
|
323
|
+
# We return nil, so we do not restart until running the whole loop
|
324
|
+
# This allows us to run revocation jobs and other things and we will pick up new work
|
325
|
+
# next time after dispatching all the things that are needed
|
326
|
+
#
|
327
|
+
# If we would retry here, the client reset would become transparent and we would not have
|
328
|
+
# a chance to take any actions
|
329
|
+
nil
|
284
330
|
end
|
285
331
|
|
286
332
|
# Builds a new rdkafka consumer instance based on the subscription group configuration
|
@@ -3,20 +3,34 @@
|
|
3
3
|
module Karafka
|
4
4
|
module Connection
|
5
5
|
# A single listener that listens to incoming messages from a single subscription group.
|
6
|
-
# It polls the messages and then enqueues. It also takes care of potential recovery from
|
6
|
+
# It polls the messages and then enqueues jobs. It also takes care of potential recovery from
|
7
7
|
# critical errors by restarting everything in a safe manner.
|
8
|
+
#
|
9
|
+
# This is the heart of the consumption process.
|
8
10
|
class Listener
|
11
|
+
include Helpers::Async
|
12
|
+
|
13
|
+
# Can be useful for logging
|
14
|
+
# @return [String] id of this listener
|
15
|
+
attr_reader :id
|
16
|
+
|
9
17
|
# @param subscription_group [Karafka::Routing::SubscriptionGroup]
|
10
18
|
# @param jobs_queue [Karafka::Processing::JobsQueue] queue where we should push work
|
11
19
|
# @return [Karafka::Connection::Listener] listener instance
|
12
20
|
def initialize(subscription_group, jobs_queue)
|
21
|
+
@id = SecureRandom.uuid
|
13
22
|
@subscription_group = subscription_group
|
14
23
|
@jobs_queue = jobs_queue
|
24
|
+
@jobs_builder = ::Karafka::App.config.internal.jobs_builder
|
15
25
|
@pauses_manager = PausesManager.new
|
16
26
|
@client = Client.new(@subscription_group)
|
17
27
|
@executors = Processing::ExecutorsBuffer.new(@client, subscription_group)
|
18
28
|
# We reference scheduler here as it is much faster than fetching this each time
|
19
29
|
@scheduler = ::Karafka::App.config.internal.scheduler
|
30
|
+
# We keep one buffer for messages to preserve memory and not allocate extra objects
|
31
|
+
# We can do this that way because we always first schedule jobs using messages before we
|
32
|
+
# fetch another batch.
|
33
|
+
@messages_buffer = MessagesBuffer.new(subscription_group)
|
20
34
|
end
|
21
35
|
|
22
36
|
# Runs the main listener fetch loop.
|
@@ -53,33 +67,55 @@ module Karafka
|
|
53
67
|
)
|
54
68
|
|
55
69
|
resume_paused_partitions
|
56
|
-
# We need to fetch data before we revoke lost partitions details as during the polling
|
57
|
-
# the callbacks for tracking lost partitions are triggered. Otherwise we would be always
|
58
|
-
# one batch behind.
|
59
|
-
messages_buffer = @client.batch_poll
|
60
70
|
|
61
71
|
Karafka.monitor.instrument(
|
62
72
|
'connection.listener.fetch_loop.received',
|
63
73
|
caller: self,
|
64
|
-
messages_buffer: messages_buffer
|
65
|
-
)
|
74
|
+
messages_buffer: @messages_buffer
|
75
|
+
) do
|
76
|
+
# We need to fetch data before we revoke lost partitions details as during the polling
|
77
|
+
# the callbacks for tracking lost partitions are triggered. Otherwise we would be
|
78
|
+
# always one batch behind.
|
79
|
+
poll_and_remap_messages
|
80
|
+
end
|
66
81
|
|
67
82
|
# If there were revoked partitions, we need to wait on their jobs to finish before
|
68
83
|
# distributing consuming jobs as upon revoking, we might get assigned to the same
|
69
84
|
# partitions, thus getting their jobs. The revoking jobs need to finish before
|
70
85
|
# appropriate consumers are taken down and re-created
|
71
|
-
|
72
|
-
|
73
|
-
schedule_partitions_jobs(messages_buffer)
|
86
|
+
build_and_schedule_revoke_lost_partitions_jobs
|
74
87
|
|
75
88
|
# We wait only on jobs from our subscription group. Other groups are independent.
|
76
|
-
wait
|
89
|
+
wait
|
90
|
+
|
91
|
+
build_and_schedule_consumption_jobs
|
92
|
+
|
93
|
+
wait
|
77
94
|
|
78
95
|
# We don't use the `#commit_offsets!` here for performance reasons. This can be achieved
|
79
96
|
# if needed by using manual offset management.
|
80
97
|
@client.commit_offsets
|
81
98
|
end
|
82
99
|
|
100
|
+
# If we are stopping we will no longer schedule any jobs despite polling.
|
101
|
+
# We need to keep polling not to exceed the `max.poll.interval` for long-running
|
102
|
+
# non-blocking jobs and we need to allow them to finish. We however do not want to
|
103
|
+
# enqueue any new jobs. It's worth keeping in mind that it is the end user responsibility
|
104
|
+
# to detect shutdown in their long-running logic or else Karafka will force shutdown
|
105
|
+
# after a while.
|
106
|
+
#
|
107
|
+
# We do not care about resuming any partitions or lost jobs as we do not plan to do
|
108
|
+
# anything with them as we're in the shutdown phase.
|
109
|
+
wait_with_poll
|
110
|
+
|
111
|
+
# We do not want to schedule the shutdown jobs prior to finishing all the jobs
|
112
|
+
# (including non-blocking) as there might be a long-running job with a shutdown and then
|
113
|
+
# we would run two jobs in parallel for the same executor and consumer. We do not want that
|
114
|
+
# as it could create a race-condition.
|
115
|
+
build_and_schedule_shutdown_jobs
|
116
|
+
|
117
|
+
wait_with_poll
|
118
|
+
|
83
119
|
shutdown
|
84
120
|
|
85
121
|
# This is on purpose - see the notes for this method
|
@@ -100,57 +136,86 @@ module Karafka
|
|
100
136
|
|
101
137
|
# Resumes processing of partitions that were paused due to an error.
|
102
138
|
def resume_paused_partitions
|
103
|
-
@pauses_manager.resume
|
139
|
+
@pauses_manager.resume do |topic, partition|
|
140
|
+
@client.resume(topic, partition)
|
141
|
+
end
|
104
142
|
end
|
105
143
|
|
106
144
|
# Enqueues revoking jobs for partitions that were taken away from the running process.
|
107
|
-
|
108
|
-
# @note We do not use scheduler here as those jobs are not meant to be order optimized in
|
109
|
-
# any way. Since they operate occasionally it is irrelevant.
|
110
|
-
def schedule_revoke_lost_partitions_jobs
|
145
|
+
def build_and_schedule_revoke_lost_partitions_jobs
|
111
146
|
revoked_partitions = @client.rebalance_manager.revoked_partitions
|
112
147
|
|
113
|
-
|
148
|
+
# Stop early to save on some execution and array allocation
|
149
|
+
return if revoked_partitions.empty?
|
150
|
+
|
151
|
+
jobs = []
|
114
152
|
|
115
153
|
revoked_partitions.each do |topic, partitions|
|
116
154
|
partitions.each do |partition|
|
117
155
|
pause_tracker = @pauses_manager.fetch(topic, partition)
|
118
156
|
executor = @executors.fetch(topic, partition, pause_tracker)
|
119
|
-
|
157
|
+
jobs << @jobs_builder.revoked(executor)
|
120
158
|
end
|
121
159
|
end
|
122
160
|
|
123
|
-
|
161
|
+
@scheduler.schedule_revocation(@jobs_queue, jobs)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Enqueues the shutdown jobs for all the executors that exist in our subscription group
|
165
|
+
def build_and_schedule_shutdown_jobs
|
166
|
+
jobs = []
|
167
|
+
|
168
|
+
@executors.each do |_, _, executor|
|
169
|
+
jobs << @jobs_builder.shutdown(executor)
|
170
|
+
end
|
171
|
+
|
172
|
+
@scheduler.schedule_shutdown(@jobs_queue, jobs)
|
124
173
|
end
|
125
174
|
|
126
|
-
#
|
175
|
+
# Polls messages within the time and amount boundaries defined in the settings and then
|
176
|
+
# builds karafka messages based on the raw rdkafka messages buffer returned by the
|
177
|
+
# `#batch_poll` method.
|
127
178
|
#
|
128
|
-
# @
|
129
|
-
def
|
130
|
-
@
|
131
|
-
|
179
|
+
# @note There are two buffers, one for raw messages and one for "built" karafka messages
|
180
|
+
def poll_and_remap_messages
|
181
|
+
@messages_buffer.remap(
|
182
|
+
@client.batch_poll
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Takes the messages per topic partition and enqueues processing jobs in threads using
|
187
|
+
# given scheduler.
|
188
|
+
def build_and_schedule_consumption_jobs
|
189
|
+
return if @messages_buffer.empty?
|
132
190
|
|
133
|
-
|
191
|
+
jobs = []
|
134
192
|
|
135
|
-
|
193
|
+
@messages_buffer.each do |topic, partition, messages|
|
194
|
+
pause_tracker = @pauses_manager.fetch(topic, partition)
|
136
195
|
|
137
|
-
|
196
|
+
executor = @executors.fetch(topic, partition, pause_tracker)
|
197
|
+
|
198
|
+
jobs << @jobs_builder.consume(executor, messages)
|
138
199
|
end
|
200
|
+
|
201
|
+
@scheduler.schedule_consumption(@jobs_queue, jobs)
|
139
202
|
end
|
140
203
|
|
141
204
|
# Waits for all the jobs from a given subscription group to finish before moving forward
|
142
|
-
|
143
|
-
|
144
|
-
|
205
|
+
def wait
|
206
|
+
@jobs_queue.wait(@subscription_group.id)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Waits without blocking the polling
|
210
|
+
# This should be used only when we no longer plan to use any incoming data and we can safely
|
211
|
+
# discard it
|
212
|
+
def wait_with_poll
|
213
|
+
@client.batch_poll until @jobs_queue.empty?(@subscription_group.id)
|
145
214
|
end
|
146
215
|
|
147
216
|
# Stops the jobs queue, triggers shutdown on all the executors (sync), commits offsets and
|
148
217
|
# stops kafka client.
|
149
218
|
def shutdown
|
150
|
-
@jobs_queue.close
|
151
|
-
# This runs synchronously, making sure we finish all the shutdowns before we stop the
|
152
|
-
# client.
|
153
|
-
@executors.shutdown
|
154
219
|
@client.commit_offsets!
|
155
220
|
@client.stop
|
156
221
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Connection
|
5
|
+
# Abstraction layer around listeners batch.
|
6
|
+
class ListenersBatch
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @param jobs_queue [JobsQueue]
|
10
|
+
# @return [ListenersBatch]
|
11
|
+
def initialize(jobs_queue)
|
12
|
+
@batch = App.subscription_groups.map do |subscription_group|
|
13
|
+
Connection::Listener.new(subscription_group, jobs_queue)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Iterates over available listeners and yields each listener
|
18
|
+
# @param block [Proc] block we want to run
|
19
|
+
def each(&block)
|
20
|
+
@batch.each(&block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -2,20 +2,26 @@
|
|
2
2
|
|
3
3
|
module Karafka
|
4
4
|
module Connection
|
5
|
-
# Buffer
|
6
|
-
# When message is added to this buffer, it gets assigned to an array with other messages from
|
7
|
-
# the same topic and partition.
|
5
|
+
# Buffer used to build and store karafka messages built based on raw librdkafka messages.
|
8
6
|
#
|
9
|
-
#
|
7
|
+
# Why do we have two buffers? `RawMessagesBuffer` is used to store raw messages and to handle
|
8
|
+
# cases related to partition revocation and reconnections. It is "internal" to the listening
|
9
|
+
# process. `MessagesBuffer` on the other hand is used to "translate" those raw messages that
|
10
|
+
# we know that are ok into Karafka messages and to simplify further work with them.
|
11
|
+
#
|
12
|
+
# While it adds a bit of overhead, it makes conceptual things much easier and it adds only two
|
13
|
+
# simple hash iterations over messages batch.
|
14
|
+
#
|
15
|
+
# @note This buffer is NOT thread safe. We do not worry about it as we do not use it outside
|
16
|
+
# of the main listener loop. It can be cleared after the jobs are scheduled with messages
|
17
|
+
# it stores, because messages arrays are not "cleared" in any way directly and their
|
18
|
+
# reference stays.
|
10
19
|
class MessagesBuffer
|
11
20
|
attr_reader :size
|
12
21
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# @return [Karafka::Connection::MessagesBuffer] buffer instance
|
18
|
-
def initialize
|
22
|
+
# @param subscription_group [Karafka::Routing::SubscriptionGroup]
|
23
|
+
def initialize(subscription_group)
|
24
|
+
@subscription_group = subscription_group
|
19
25
|
@size = 0
|
20
26
|
@groups = Hash.new do |topic_groups, topic|
|
21
27
|
topic_groups[topic] = Hash.new do |partition_groups, partition|
|
@@ -24,64 +30,54 @@ module Karafka
|
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
27
|
-
#
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
def <<(message)
|
32
|
-
@size += 1
|
33
|
-
@groups[message.topic][message.partition] << message
|
34
|
-
end
|
33
|
+
# Remaps raw messages from the raw messages buffer to Karafka messages
|
34
|
+
# @param raw_messages_buffer [RawMessagesBuffer] buffer with raw messages
|
35
|
+
def remap(raw_messages_buffer)
|
36
|
+
clear unless @size.zero?
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# @param partition [Integer] partition of which data we want to remove
|
40
|
-
def delete(topic, partition)
|
41
|
-
return unless @groups.key?(topic)
|
42
|
-
return unless @groups.fetch(topic).key?(partition)
|
38
|
+
# Since it happens "right after" we've received the messages, it is close enough it time
|
39
|
+
# to be used as the moment we received messages.
|
40
|
+
received_at = Time.now
|
43
41
|
|
44
|
-
|
45
|
-
|
42
|
+
raw_messages_buffer.each do |topic, partition, messages|
|
43
|
+
@size += messages.count
|
46
44
|
|
47
|
-
|
45
|
+
ktopic = @subscription_group.topics.find(topic)
|
48
46
|
|
49
|
-
|
50
|
-
|
47
|
+
@groups[topic][partition] = messages.map do |message|
|
48
|
+
Messages::Builders::Message.call(
|
49
|
+
message,
|
50
|
+
ktopic,
|
51
|
+
received_at
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
51
55
|
end
|
52
56
|
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
57
|
+
# Allows to iterate over all the topics and partitions messages
|
58
|
+
#
|
59
|
+
# @yieldparam [String] topic name
|
60
|
+
# @yieldparam [Integer] partition number
|
61
|
+
# @yieldparam [Array<Karafka::Messages::Message>] messages from a given topic partition
|
62
|
+
def each
|
63
|
+
@groups.each do |topic, partitions|
|
64
|
+
partitions.each do |partition, messages|
|
65
|
+
yield(topic, partition, messages)
|
61
66
|
end
|
62
67
|
end
|
63
|
-
|
64
|
-
recount!
|
65
68
|
end
|
66
69
|
|
67
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
# we save ourselves some objects allocations. We cannot clear the underlying arrays as they
|
71
|
-
# may be used in other threads for data processing, thus if we would clear it, we could
|
72
|
-
# potentially clear a raw messages array for a job that is in the jobs queue.
|
73
|
-
def clear
|
74
|
-
@size = 0
|
75
|
-
@groups.each_value(&:clear)
|
70
|
+
# @return [Boolean] is the buffer empty or does it contain any messages
|
71
|
+
def empty?
|
72
|
+
@size.zero?
|
76
73
|
end
|
77
74
|
|
78
75
|
private
|
79
76
|
|
80
|
-
#
|
81
|
-
def
|
82
|
-
@size =
|
83
|
-
|
84
|
-
end
|
77
|
+
# Clears the buffer completely
|
78
|
+
def clear
|
79
|
+
@size = 0
|
80
|
+
@groups.clear
|
85
81
|
end
|
86
82
|
end
|
87
83
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Connection
|
5
|
+
# Buffer for raw librdkafka messages.
|
6
|
+
#
|
7
|
+
# When message is added to this buffer, it gets assigned to an array with other messages from
|
8
|
+
# the same topic and partition.
|
9
|
+
#
|
10
|
+
# @note This buffer is NOT threadsafe.
|
11
|
+
#
|
12
|
+
# @note We store data here in groups per topic partition to handle the revocation case, where
|
13
|
+
# we may need to remove messages from a single topic partition.
|
14
|
+
class RawMessagesBuffer
|
15
|
+
attr_reader :size
|
16
|
+
|
17
|
+
# @return [Karafka::Connection::MessagesBuffer] buffer instance
|
18
|
+
def initialize
|
19
|
+
@size = 0
|
20
|
+
@groups = Hash.new do |topic_groups, topic|
|
21
|
+
topic_groups[topic] = Hash.new do |partition_groups, partition|
|
22
|
+
partition_groups[partition] = []
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adds a message to the buffer.
|
28
|
+
#
|
29
|
+
# @param message [Rdkafka::Consumer::Message] raw rdkafka message
|
30
|
+
# @return [Array<Rdkafka::Consumer::Message>] given partition topic sub-buffer array
|
31
|
+
def <<(message)
|
32
|
+
@size += 1
|
33
|
+
@groups[message.topic][message.partition] << message
|
34
|
+
end
|
35
|
+
|
36
|
+
# Allows to iterate over all the topics and partitions messages
|
37
|
+
#
|
38
|
+
# @yieldparam [String] topic name
|
39
|
+
# @yieldparam [Integer] partition number
|
40
|
+
# @yieldparam [Array<Rdkafka::Consumer::Message>] topic partition aggregated results
|
41
|
+
def each
|
42
|
+
@groups.each do |topic, partitions|
|
43
|
+
partitions.each do |partition, messages|
|
44
|
+
yield(topic, partition, messages)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Removes given topic and partition data out of the buffer
|
50
|
+
# This is used when there's a partition revocation
|
51
|
+
# @param topic [String] topic we're interested in
|
52
|
+
# @param partition [Integer] partition of which data we want to remove
|
53
|
+
def delete(topic, partition)
|
54
|
+
return unless @groups.key?(topic)
|
55
|
+
return unless @groups.fetch(topic).key?(partition)
|
56
|
+
|
57
|
+
topic_data = @groups.fetch(topic)
|
58
|
+
topic_data.delete(partition)
|
59
|
+
|
60
|
+
recount!
|
61
|
+
|
62
|
+
# If there are no more partitions to handle in a given topic, remove it completely
|
63
|
+
@groups.delete(topic) if topic_data.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Removes duplicated messages from the same partitions
|
67
|
+
# This should be used only when rebalance occurs, as we may get data again we already have
|
68
|
+
# due to the processing from the last offset. In cases like this, we may get same data
|
69
|
+
# again and we do want to ensure as few duplications as possible
|
70
|
+
def uniq!
|
71
|
+
@groups.each_value do |partitions|
|
72
|
+
partitions.each_value do |messages|
|
73
|
+
messages.uniq!(&:offset)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
recount!
|
78
|
+
end
|
79
|
+
|
80
|
+
# Removes all the data from the buffer.
|
81
|
+
#
|
82
|
+
# @note We do not clear the whole groups hash but rather we clear the partition hashes, so
|
83
|
+
# we save ourselves some objects allocations. We cannot clear the underlying arrays as they
|
84
|
+
# may be used in other threads for data processing, thus if we would clear it, we could
|
85
|
+
# potentially clear a raw messages array for a job that is in the jobs queue.
|
86
|
+
def clear
|
87
|
+
@size = 0
|
88
|
+
@groups.each_value(&:clear)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Updates the messages count if we performed any operations that could change the state
|
94
|
+
def recount!
|
95
|
+
@size = @groups.each_value.sum do |partitions|
|
96
|
+
partitions.each_value.map(&:count).sum
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -25,15 +25,17 @@ module Karafka
|
|
25
25
|
required(:pause_max_timeout) { int? & gt?(0) }
|
26
26
|
required(:pause_with_exponential_backoff).filled(:bool?)
|
27
27
|
required(:shutdown_timeout) { int? & gt?(0) }
|
28
|
+
required(:max_wait_time) { int? & gt?(0) }
|
28
29
|
required(:kafka).filled(:hash)
|
29
30
|
|
30
31
|
# We validate internals just to be sure, that they are present and working
|
31
32
|
required(:internal).schema do
|
32
33
|
required(:routing_builder)
|
34
|
+
required(:subscription_groups_builder)
|
35
|
+
required(:jobs_builder)
|
33
36
|
required(:status)
|
34
37
|
required(:process)
|
35
38
|
required(:scheduler)
|
36
|
-
required(:subscription_groups_builder)
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
@@ -53,6 +55,12 @@ module Karafka
|
|
53
55
|
key(:pause_timeout).failure(:max_timeout_vs_pause_max_timeout)
|
54
56
|
end
|
55
57
|
end
|
58
|
+
|
59
|
+
rule(:shutdown_timeout, :max_wait_time) do
|
60
|
+
if values[:max_wait_time].to_i >= values[:shutdown_timeout].to_i
|
61
|
+
key(:shutdown_timeout).failure(:shutdown_timeout_vs_max_wait_time)
|
62
|
+
end
|
63
|
+
end
|
56
64
|
end
|
57
65
|
end
|
58
66
|
end
|