karafka 2.0.0.beta1 → 2.0.0.beta4
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 +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
|