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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/ci.yml +9 -23
  4. data/CHANGELOG.md +47 -0
  5. data/Gemfile.lock +8 -8
  6. data/bin/integrations +36 -14
  7. data/bin/scenario +29 -0
  8. data/bin/wait_for_kafka +20 -0
  9. data/config/errors.yml +1 -0
  10. data/docker-compose.yml +12 -0
  11. data/karafka.gemspec +2 -2
  12. data/lib/active_job/karafka.rb +2 -2
  13. data/lib/karafka/active_job/routing/extensions.rb +31 -0
  14. data/lib/karafka/base_consumer.rb +65 -42
  15. data/lib/karafka/connection/client.rb +65 -19
  16. data/lib/karafka/connection/listener.rb +99 -34
  17. data/lib/karafka/connection/listeners_batch.rb +24 -0
  18. data/lib/karafka/connection/messages_buffer.rb +50 -54
  19. data/lib/karafka/connection/raw_messages_buffer.rb +101 -0
  20. data/lib/karafka/contracts/config.rb +9 -1
  21. data/lib/karafka/helpers/async.rb +33 -0
  22. data/lib/karafka/instrumentation/logger_listener.rb +34 -10
  23. data/lib/karafka/instrumentation/monitor.rb +3 -1
  24. data/lib/karafka/licenser.rb +26 -7
  25. data/lib/karafka/messages/batch_metadata.rb +26 -3
  26. data/lib/karafka/messages/builders/batch_metadata.rb +17 -29
  27. data/lib/karafka/messages/builders/message.rb +1 -0
  28. data/lib/karafka/messages/builders/messages.rb +4 -12
  29. data/lib/karafka/pro/active_job/consumer.rb +49 -0
  30. data/lib/karafka/pro/active_job/dispatcher.rb +10 -10
  31. data/lib/karafka/pro/active_job/job_options_contract.rb +9 -9
  32. data/lib/karafka/pro/base_consumer.rb +76 -0
  33. data/lib/karafka/pro/loader.rb +30 -13
  34. data/lib/karafka/pro/performance_tracker.rb +9 -9
  35. data/lib/karafka/pro/processing/jobs/consume_non_blocking.rb +37 -0
  36. data/lib/karafka/pro/processing/jobs_builder.rb +31 -0
  37. data/lib/karafka/pro/routing/extensions.rb +32 -0
  38. data/lib/karafka/pro/scheduler.rb +54 -0
  39. data/lib/karafka/processing/executor.rb +34 -7
  40. data/lib/karafka/processing/executors_buffer.rb +15 -7
  41. data/lib/karafka/processing/jobs/base.rb +21 -4
  42. data/lib/karafka/processing/jobs/consume.rb +12 -5
  43. data/lib/karafka/processing/jobs_builder.rb +28 -0
  44. data/lib/karafka/processing/jobs_queue.rb +15 -12
  45. data/lib/karafka/processing/result.rb +34 -0
  46. data/lib/karafka/processing/worker.rb +23 -17
  47. data/lib/karafka/processing/workers_batch.rb +5 -0
  48. data/lib/karafka/routing/consumer_group.rb +1 -1
  49. data/lib/karafka/routing/subscription_group.rb +2 -2
  50. data/lib/karafka/routing/subscription_groups_builder.rb +3 -2
  51. data/lib/karafka/routing/topic.rb +5 -0
  52. data/lib/karafka/routing/topics.rb +38 -0
  53. data/lib/karafka/runner.rb +19 -27
  54. data/lib/karafka/scheduler.rb +10 -11
  55. data/lib/karafka/server.rb +24 -23
  56. data/lib/karafka/setup/config.rb +4 -1
  57. data/lib/karafka/status.rb +1 -3
  58. data/lib/karafka/version.rb +1 -1
  59. data.tar.gz.sig +0 -0
  60. metadata +20 -5
  61. metadata.gz.sig +0 -0
  62. 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 = MessagesBuffer.new
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
- @offsetting = true
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
- return unless @offsetting
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
- pause_msg = Messages::Seek.new(topic, partition, offset)
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
- @rebalance_manager.clear
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
- retry
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
- wait(@subscription_group) if schedule_revoke_lost_partitions_jobs
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(@subscription_group)
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 { |topic, partition| @client.resume(topic, partition) }
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
- # @return [Boolean] was there anything to revoke
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
- return false if revoked_partitions.empty?
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
- @jobs_queue << Processing::Jobs::Revoked.new(executor)
157
+ jobs << @jobs_builder.revoked(executor)
120
158
  end
121
159
  end
122
160
 
123
- true
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
- # Takes the messages per topic partition and enqueues processing jobs in threads.
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
- # @param messages_buffer [Karafka::Connection::MessagesBuffer] buffer with messages
129
- def schedule_partitions_jobs(messages_buffer)
130
- @scheduler.call(messages_buffer) do |topic, partition, messages|
131
- pause = @pauses_manager.fetch(topic, partition)
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
- next if pause.paused?
191
+ jobs = []
134
192
 
135
- executor = @executors.fetch(topic, partition, pause)
193
+ @messages_buffer.each do |topic, partition, messages|
194
+ pause_tracker = @pauses_manager.fetch(topic, partition)
136
195
 
137
- @jobs_queue << Processing::Jobs::Consume.new(executor, messages)
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
- # @param subscription_group [Karafka::Routing::SubscriptionGroup]
143
- def wait(subscription_group)
144
- @jobs_queue.wait(subscription_group.id)
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 for messages.
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
- # @note This buffer is NOT threadsafe.
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
- extend Forwardable
14
-
15
- def_delegators :@groups, :each
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
- # 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
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
- # Removes given topic and partition data out of the buffer
37
- # This is used when there's a partition revocation
38
- # @param topic [String] topic we're interested in
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
- topic_data = @groups.fetch(topic)
45
- topic_data.delete(partition)
42
+ raw_messages_buffer.each do |topic, partition, messages|
43
+ @size += messages.count
46
44
 
47
- recount!
45
+ ktopic = @subscription_group.topics.find(topic)
48
46
 
49
- # If there are no more partitions to handle in a given topic, remove it completely
50
- @groups.delete(topic) if topic_data.empty?
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
- # Removes duplicated messages from the same partitions
54
- # This should be used only when rebalance occurs, as we may get data again we already have
55
- # due to the processing from the last offset. In cases like this, we may get same data
56
- # again and we do want to ensure as few duplications as possible
57
- def uniq!
58
- @groups.each_value do |partitions|
59
- partitions.each_value do |messages|
60
- messages.uniq!(&:offset)
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
- # Removes all the data from the buffer.
68
- #
69
- # @note We do not clear the whole groups hash but rather we clear the partition hashes, so
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
- # Updates the messages count if we performed any operations that could change the state
81
- def recount!
82
- @size = @groups.each_value.sum do |partitions|
83
- partitions.each_value.map(&:count).sum
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