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.
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