karafka 2.3.1 → 2.3.3

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 (77) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +20 -0
  5. data/Gemfile.lock +6 -6
  6. data/README.md +2 -2
  7. data/bin/integrations +2 -1
  8. data/bin/rspecs +6 -2
  9. data/config/locales/errors.yml +33 -8
  10. data/config/locales/pro_errors.yml +6 -0
  11. data/docker-compose.yml +1 -1
  12. data/lib/karafka/app.rb +14 -0
  13. data/lib/karafka/cli/base.rb +19 -0
  14. data/lib/karafka/cli/server.rb +62 -76
  15. data/lib/karafka/cli/swarm.rb +30 -0
  16. data/lib/karafka/connection/client.rb +7 -0
  17. data/lib/karafka/constraints.rb +3 -3
  18. data/lib/karafka/contracts/config.rb +41 -0
  19. data/lib/karafka/errors.rb +12 -0
  20. data/lib/karafka/helpers/config_importer.rb +30 -0
  21. data/lib/karafka/instrumentation/logger_listener.rb +31 -0
  22. data/lib/karafka/instrumentation/notifications.rb +9 -0
  23. data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +2 -0
  24. data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +34 -4
  25. data/lib/karafka/instrumentation/vendors/kubernetes/base_listener.rb +72 -0
  26. data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +11 -40
  27. data/lib/karafka/instrumentation/vendors/kubernetes/swarm_liveness_listener.rb +54 -0
  28. data/lib/karafka/pro/active_job/job_options_contract.rb +1 -1
  29. data/lib/karafka/pro/base_consumer.rb +16 -0
  30. data/lib/karafka/pro/connection/manager.rb +6 -1
  31. data/lib/karafka/pro/processing/coordinator.rb +13 -3
  32. data/lib/karafka/pro/processing/coordinators/errors_tracker.rb +74 -0
  33. data/lib/karafka/pro/processing/coordinators/filters_applier.rb +107 -0
  34. data/lib/karafka/pro/processing/coordinators/virtual_offset_manager.rb +180 -0
  35. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +5 -7
  36. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +5 -7
  37. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom.rb +8 -10
  38. data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_mom_vp.rb +8 -16
  39. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +5 -7
  40. data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +5 -7
  41. data/lib/karafka/pro/processing/strategies/aj/dlq_mom.rb +8 -10
  42. data/lib/karafka/pro/processing/strategies/aj/dlq_mom_vp.rb +7 -9
  43. data/lib/karafka/pro/processing/strategies/dlq/default.rb +36 -10
  44. data/lib/karafka/pro/processing/strategies/dlq/ftr.rb +3 -7
  45. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +4 -8
  46. data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +6 -9
  47. data/lib/karafka/pro/processing/strategies/dlq/ftr_mom.rb +5 -15
  48. data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +4 -8
  49. data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +6 -9
  50. data/lib/karafka/pro/processing/strategies/dlq/mom.rb +10 -20
  51. data/lib/karafka/pro/processing/strategies/vp/default.rb +7 -0
  52. data/lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb +6 -0
  53. data/lib/karafka/pro/routing/features/dead_letter_queue/topic.rb +39 -0
  54. data/lib/karafka/pro/routing/features/swarm/config.rb +31 -0
  55. data/lib/karafka/pro/routing/features/swarm/contracts/topic.rb +67 -0
  56. data/lib/karafka/pro/routing/features/swarm/topic.rb +54 -0
  57. data/lib/karafka/pro/routing/features/swarm.rb +25 -0
  58. data/lib/karafka/pro/swarm/liveness_listener.rb +171 -0
  59. data/lib/karafka/process.rb +27 -1
  60. data/lib/karafka/routing/features/dead_letter_queue/config.rb +2 -0
  61. data/lib/karafka/routing/subscription_group.rb +44 -9
  62. data/lib/karafka/server.rb +11 -13
  63. data/lib/karafka/setup/config.rb +41 -2
  64. data/lib/karafka/status.rb +4 -2
  65. data/lib/karafka/swarm/liveness_listener.rb +55 -0
  66. data/lib/karafka/swarm/manager.rb +229 -0
  67. data/lib/karafka/swarm/node.rb +179 -0
  68. data/lib/karafka/swarm/pidfd.rb +147 -0
  69. data/lib/karafka/swarm/supervisor.rb +187 -0
  70. data/lib/karafka/swarm.rb +27 -0
  71. data/lib/karafka/version.rb +1 -1
  72. data/lib/karafka.rb +1 -1
  73. data.tar.gz.sig +0 -0
  74. metadata +21 -4
  75. metadata.gz.sig +0 -0
  76. data/lib/karafka/pro/processing/filters_applier.rb +0 -105
  77. data/lib/karafka/pro/processing/virtual_offset_manager.rb +0 -177
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Swarm
5
+ # Supervisor that starts forks and uses monitor to monitor them. Also handles shutdown of
6
+ # all the processes including itself.
7
+ #
8
+ # In case any node dies, it will be restarted.
9
+ #
10
+ # @note Technically speaking supervisor is never in the running state because we do not want
11
+ # to have any sockets or anything else on it that could break under forking.
12
+ # It has its own "supervising" state from which it can go to the final shutdown.
13
+ class Supervisor
14
+ include Karafka::Core::Helpers::Time
15
+ include Helpers::ConfigImporter.new(
16
+ monitor: %i[monitor],
17
+ swarm: %i[internal swarm],
18
+ manager: %i[internal swarm manager],
19
+ supervision_interval: %i[internal swarm supervision_interval],
20
+ shutdown_timeout: %i[shutdown_timeout],
21
+ supervision_sleep: %i[internal supervision_sleep],
22
+ forceful_exit_code: %i[internal forceful_exit_code],
23
+ process: %i[internal process]
24
+ )
25
+
26
+ def initialize
27
+ @mutex = Mutex.new
28
+ @queue = Processing::TimedQueue.new
29
+ end
30
+
31
+ # Creates needed number of forks, installs signals and starts supervision
32
+ def run
33
+ # Close producer just in case. While it should not be used, we do not want even a
34
+ # theoretical case since librdkafka is not thread-safe.
35
+ # We close it prior to forking just to make sure, there is no issue with initialized
36
+ # producer (should not be initialized but just in case)
37
+ Karafka.producer.close
38
+
39
+ Karafka::App.warmup
40
+
41
+ manager.start
42
+
43
+ process.on_sigint { stop }
44
+ process.on_sigquit { stop }
45
+ process.on_sigterm { stop }
46
+ process.on_sigtstp { quiet }
47
+ process.on_sigttin { signal('TTIN') }
48
+ # Needed to be registered as we want to unlock on child changes
49
+ process.on_sigchld {}
50
+ process.on_any_active { unlock }
51
+ process.supervise
52
+
53
+ Karafka::App.supervise!
54
+
55
+ loop do
56
+ return if Karafka::App.terminated?
57
+
58
+ lock
59
+ control
60
+ end
61
+ # If anything went wrong, signal this and die
62
+ # Supervisor is meant to be thin and not cause any issues. If you encounter this case
63
+ # please report it as it should be considered critical
64
+ rescue StandardError => e
65
+ monitor.instrument(
66
+ 'error.occurred',
67
+ caller: self,
68
+ error: e,
69
+ manager: manager,
70
+ type: 'swarm.supervisor.error'
71
+ )
72
+
73
+ @nodes.terminate
74
+ end
75
+
76
+ private
77
+
78
+ # Keeps the lock on the queue so we control nodes only when it is needed
79
+ # @note We convert to seconds since the queue timeout requires seconds
80
+ def lock
81
+ @queue.pop(timeout: supervision_interval / 1_000.0)
82
+ end
83
+
84
+ # Frees the lock on events that could require nodes control
85
+ def unlock
86
+ @queue << true
87
+ end
88
+
89
+ # Stops all the nodes and supervisor once all nodes are dead.
90
+ # It will forcefully stop all nodes if they exit the shutdown timeout. While in theory each
91
+ # of the nodes anyhow has its own supervisor, this is a last resort to stop everything.
92
+ def stop
93
+ # Ensure that the stopping procedure is initialized only once
94
+ @mutex.synchronize do
95
+ return if @stopping
96
+
97
+ @stopping = true
98
+ end
99
+
100
+ initialized = true
101
+ Karafka::App.stop!
102
+
103
+ manager.stop
104
+
105
+ # We check from time to time (for the timeout period) if all the threads finished
106
+ # their work and if so, we can just return and normal shutdown process will take place
107
+ # We divide it by 1000 because we use time in ms.
108
+ ((shutdown_timeout / 1_000) * (1 / supervision_sleep)).to_i.times do
109
+ if manager.stopped?
110
+ manager.cleanup
111
+ return
112
+ end
113
+
114
+ sleep(supervision_sleep)
115
+ end
116
+
117
+ raise Errors::ForcefulShutdownError
118
+ rescue Errors::ForcefulShutdownError => e
119
+ monitor.instrument(
120
+ 'error.occurred',
121
+ caller: self,
122
+ error: e,
123
+ manager: manager,
124
+ type: 'app.stopping.error'
125
+ )
126
+
127
+ # Run forceful kill
128
+ manager.terminate
129
+ # And wait until linux kills them
130
+ # This prevents us from existing forcefully with any dead child process still existing
131
+ # Since we have sent the `KILL` signal, it must die, so we can wait until all dead
132
+ sleep(supervision_sleep) until manager.stopped?
133
+
134
+ # Cleanup the process table
135
+ manager.cleanup
136
+
137
+ # We do not use `exit!` here similar to regular server because we do not have to worry
138
+ # about any librdkafka related hanging connections, etc
139
+ Kernel.exit(forceful_exit_code)
140
+ ensure
141
+ if initialized
142
+ Karafka::App.stopped!
143
+ Karafka::App.terminate!
144
+ end
145
+ end
146
+
147
+ # Moves all the nodes and itself to the quiet state
148
+ def quiet
149
+ @mutex.synchronize do
150
+ return if @quieting
151
+
152
+ @quieting = true
153
+
154
+ Karafka::App.quiet!
155
+ manager.quiet
156
+ Karafka::App.quieted!
157
+ end
158
+ end
159
+
160
+ # Checks on the children nodes and takes appropriate actions.
161
+ # - If node is dead, will cleanup
162
+ # - If node is no longer reporting as healthy will start a graceful shutdown
163
+ # - If node does not want to close itself gracefully, will kill it
164
+ # - If node was dead, new node will be started as a recovery means
165
+ def control
166
+ @mutex.synchronize do
167
+ # If we are in quieting or stopping we should no longer control children
168
+ # Those states aim to finally shutdown nodes and we should not forcefully do anything
169
+ # to them. This especially applies to the quieting mode where any complex lifecycle
170
+ # reporting listeners may no longer report correctly
171
+ return if @quieting
172
+ return if @stopping
173
+
174
+ manager.control
175
+ end
176
+ end
177
+
178
+ # Sends desired signal to each node
179
+ # @param signal [String]
180
+ def signal(signal)
181
+ @mutex.synchronize do
182
+ manager.signal(signal)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ # Namespace for the Swarm capabilities.
5
+ #
6
+ # Karafka in the swarm mode will fork additional processes and use the parent process as a
7
+ # supervisor. This capability allows to run multiple processes alongside but saves some memory
8
+ # due to CoW.
9
+ module Swarm
10
+ class << self
11
+ # Raises an error if swarm is not supported on a given platform
12
+ def ensure_supported!
13
+ return if supported?
14
+
15
+ raise(
16
+ Errors::UnsupportedOptionError,
17
+ 'Swarm mode not supported on this platform'
18
+ )
19
+ end
20
+
21
+ # @return [Boolean] true if fork API and pidfd OS API are available, otherwise false
22
+ def supported?
23
+ ::Process.respond_to?(:fork) && Swarm::Pidfd.supported?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -3,5 +3,5 @@
3
3
  # Main module namespace
4
4
  module Karafka
5
5
  # Current Karafka version
6
- VERSION = '2.3.1'
6
+ VERSION = '2.3.3'
7
7
  end
data/lib/karafka.rb CHANGED
@@ -41,7 +41,7 @@ module Karafka
41
41
 
42
42
  # @return [WaterDrop::Producer] waterdrop messages producer
43
43
  def producer
44
- @producer ||= App.config.producer
44
+ App.config.producer
45
45
  end
46
46
 
47
47
  # @return [::Karafka::Monitor] monitor that we want to use
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: karafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -35,7 +35,7 @@ cert_chain:
35
35
  AnG1dJU+yL2BK7vaVytLTstJME5mepSZ46qqIJXMuWob/YPDmVaBF39TDSG9e34s
36
36
  msG3BiCqgOgHAnL23+CN3Rt8MsuRfEtoTKpJVcCfoEoNHOkc
37
37
  -----END CERTIFICATE-----
38
- date: 2024-02-09 00:00:00.000000000 Z
38
+ date: 2024-02-26 00:00:00.000000000 Z
39
39
  dependencies:
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: karafka-core
@@ -162,6 +162,7 @@ files:
162
162
  - lib/karafka/cli/info.rb
163
163
  - lib/karafka/cli/install.rb
164
164
  - lib/karafka/cli/server.rb
165
+ - lib/karafka/cli/swarm.rb
165
166
  - lib/karafka/cli/topics.rb
166
167
  - lib/karafka/connection/client.rb
167
168
  - lib/karafka/connection/conductor.rb
@@ -186,6 +187,7 @@ files:
186
187
  - lib/karafka/errors.rb
187
188
  - lib/karafka/helpers/async.rb
188
189
  - lib/karafka/helpers/colorize.rb
190
+ - lib/karafka/helpers/config_importer.rb
189
191
  - lib/karafka/helpers/interval_runner.rb
190
192
  - lib/karafka/helpers/multi_delegator.rb
191
193
  - lib/karafka/instrumentation/assignments_tracker.rb
@@ -205,7 +207,9 @@ files:
205
207
  - lib/karafka/instrumentation/vendors/datadog/dashboard.json
206
208
  - lib/karafka/instrumentation/vendors/datadog/logger_listener.rb
207
209
  - lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb
210
+ - lib/karafka/instrumentation/vendors/kubernetes/base_listener.rb
208
211
  - lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb
212
+ - lib/karafka/instrumentation/vendors/kubernetes/swarm_liveness_listener.rb
209
213
  - lib/karafka/licenser.rb
210
214
  - lib/karafka/messages/batch_metadata.rb
211
215
  - lib/karafka/messages/builders/batch_metadata.rb
@@ -245,6 +249,9 @@ files:
245
249
  - lib/karafka/pro/loader.rb
246
250
  - lib/karafka/pro/processing/collapser.rb
247
251
  - lib/karafka/pro/processing/coordinator.rb
252
+ - lib/karafka/pro/processing/coordinators/errors_tracker.rb
253
+ - lib/karafka/pro/processing/coordinators/filters_applier.rb
254
+ - lib/karafka/pro/processing/coordinators/virtual_offset_manager.rb
248
255
  - lib/karafka/pro/processing/executor.rb
249
256
  - lib/karafka/pro/processing/expansions_selector.rb
250
257
  - lib/karafka/pro/processing/filters/base.rb
@@ -253,7 +260,6 @@ files:
253
260
  - lib/karafka/pro/processing/filters/inline_insights_delayer.rb
254
261
  - lib/karafka/pro/processing/filters/throttler.rb
255
262
  - lib/karafka/pro/processing/filters/virtual_limiter.rb
256
- - lib/karafka/pro/processing/filters_applier.rb
257
263
  - lib/karafka/pro/processing/jobs/consume_non_blocking.rb
258
264
  - lib/karafka/pro/processing/jobs/periodic.rb
259
265
  - lib/karafka/pro/processing/jobs/periodic_non_blocking.rb
@@ -317,12 +323,12 @@ files:
317
323
  - lib/karafka/pro/processing/strategies/mom/vp.rb
318
324
  - lib/karafka/pro/processing/strategies/vp/default.rb
319
325
  - lib/karafka/pro/processing/strategy_selector.rb
320
- - lib/karafka/pro/processing/virtual_offset_manager.rb
321
326
  - lib/karafka/pro/routing/features/active_job.rb
322
327
  - lib/karafka/pro/routing/features/active_job/builder.rb
323
328
  - lib/karafka/pro/routing/features/base.rb
324
329
  - lib/karafka/pro/routing/features/dead_letter_queue.rb
325
330
  - lib/karafka/pro/routing/features/dead_letter_queue/contracts/topic.rb
331
+ - lib/karafka/pro/routing/features/dead_letter_queue/topic.rb
326
332
  - lib/karafka/pro/routing/features/delaying.rb
327
333
  - lib/karafka/pro/routing/features/delaying/config.rb
328
334
  - lib/karafka/pro/routing/features/delaying/contracts/topic.rb
@@ -375,6 +381,10 @@ files:
375
381
  - lib/karafka/pro/routing/features/periodic_job/config.rb
376
382
  - lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb
377
383
  - lib/karafka/pro/routing/features/periodic_job/topic.rb
384
+ - lib/karafka/pro/routing/features/swarm.rb
385
+ - lib/karafka/pro/routing/features/swarm/config.rb
386
+ - lib/karafka/pro/routing/features/swarm/contracts/topic.rb
387
+ - lib/karafka/pro/routing/features/swarm/topic.rb
378
388
  - lib/karafka/pro/routing/features/throttling.rb
379
389
  - lib/karafka/pro/routing/features/throttling/config.rb
380
390
  - lib/karafka/pro/routing/features/throttling/contracts/topic.rb
@@ -383,6 +393,7 @@ files:
383
393
  - lib/karafka/pro/routing/features/virtual_partitions/config.rb
384
394
  - lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb
385
395
  - lib/karafka/pro/routing/features/virtual_partitions/topic.rb
396
+ - lib/karafka/pro/swarm/liveness_listener.rb
386
397
  - lib/karafka/process.rb
387
398
  - lib/karafka/processing/coordinator.rb
388
399
  - lib/karafka/processing/coordinators_buffer.rb
@@ -455,6 +466,12 @@ files:
455
466
  - lib/karafka/setup/config.rb
456
467
  - lib/karafka/setup/dsl.rb
457
468
  - lib/karafka/status.rb
469
+ - lib/karafka/swarm.rb
470
+ - lib/karafka/swarm/liveness_listener.rb
471
+ - lib/karafka/swarm/manager.rb
472
+ - lib/karafka/swarm/node.rb
473
+ - lib/karafka/swarm/pidfd.rb
474
+ - lib/karafka/swarm/supervisor.rb
458
475
  - lib/karafka/templates/application_consumer.rb.erb
459
476
  - lib/karafka/templates/example_consumer.rb.erb
460
477
  - lib/karafka/templates/karafka.rb.erb
metadata.gz.sig CHANGED
Binary file
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This Karafka component is a Pro component under a commercial license.
4
- # This Karafka component is NOT licensed under LGPL.
5
- #
6
- # All of the commercial components are present in the lib/karafka/pro directory of this
7
- # repository and their usage requires commercial license agreement.
8
- #
9
- # Karafka has also commercial-friendly license, commercial support and commercial components.
10
- #
11
- # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
- # your code to Maciej Mensfeld.
13
-
14
- module Karafka
15
- module Pro
16
- module Processing
17
- # Applier for all filters we want to have. Whether related to limiting messages based
18
- # on the payload or any other things.
19
- #
20
- # From the outside world perspective, this encapsulates all the filters.
21
- # This means that this is the API we expose as a single filter, allowing us to control
22
- # the filtering via many filters easily.
23
- class FiltersApplier
24
- # @return [Array] registered filters array. Useful if we want to inject internal context
25
- # aware filters.
26
- attr_reader :filters
27
-
28
- # @param coordinator [Pro::Coordinator] pro coordinator
29
- def initialize(coordinator)
30
- # Builds filters out of their factories
31
- # We build it that way (providing topic and partition) because there may be a case where
32
- # someone wants to have a specific logic that is per topic or partition. Like for example
33
- # a case where there is a cache bypassing revocations for topic partition.
34
- #
35
- # We provide full Karafka routing topic here and not the name only, in case the filter
36
- # would be customized based on other topic settings (like VPs, etc)
37
- #
38
- # This setup allows for biggest flexibility also because topic object holds the reference
39
- # to the subscription group and consumer group
40
- @filters = coordinator.topic.filtering.factories.map do |factory|
41
- factory.call(coordinator.topic, coordinator.partition)
42
- end
43
- end
44
-
45
- # @param messages [Array<Karafka::Messages::Message>] array with messages from the
46
- # partition
47
- def apply!(messages)
48
- return unless active?
49
-
50
- @filters.each { |filter| filter.apply!(messages) }
51
- end
52
-
53
- # @return [Boolean] did we filter out any messages during filtering run
54
- def applied?
55
- return false unless active?
56
-
57
- !applied.empty?
58
- end
59
-
60
- # @return [Symbol] consumer post-filtering action that should be taken
61
- def action
62
- return :skip unless applied?
63
-
64
- # The highest priority is on a potential backoff from any of the filters because it is
65
- # the less risky (delay and continue later)
66
- return :pause if applied.any? { |filter| filter.action == :pause }
67
-
68
- # If none of the filters wanted to pause, we can check for any that would want to seek
69
- # and if there is any, we can go with this strategy
70
- return :seek if applied.any? { |filter| filter.action == :seek }
71
-
72
- :skip
73
- end
74
-
75
- # @return [Integer] minimum timeout we need to pause. This is the minimum for all the
76
- # filters to satisfy all of them.
77
- def timeout
78
- applied.map(&:timeout).compact.min || 0
79
- end
80
-
81
- # The first message we do need to get next time we poll. We use the minimum not to jump
82
- # accidentally by over any.
83
- # @return [Karafka::Messages::Message, nil] cursor message or nil if none
84
- # @note Cursor message can also return the offset in the time format
85
- def cursor
86
- return nil unless active?
87
-
88
- applied.map(&:cursor).compact.min_by(&:offset)
89
- end
90
-
91
- private
92
-
93
- # @return [Boolean] is filtering active
94
- def active?
95
- !@filters.empty?
96
- end
97
-
98
- # @return [Array<Object>] filters that applied any sort of messages limiting
99
- def applied
100
- @filters.select(&:applied?)
101
- end
102
- end
103
- end
104
- end
105
- end
@@ -1,177 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This Karafka component is a Pro component under a commercial license.
4
- # This Karafka component is NOT licensed under LGPL.
5
- #
6
- # All of the commercial components are present in the lib/karafka/pro directory of this
7
- # repository and their usage requires commercial license agreement.
8
- #
9
- # Karafka has also commercial-friendly license, commercial support and commercial components.
10
- #
11
- # By sending a pull request to the pro components, you are agreeing to transfer the copyright of
12
- # your code to Maciej Mensfeld.
13
-
14
- module Karafka
15
- module Pro
16
- module Processing
17
- # Manager that keeps track of our offsets with the virtualization layer that are local
18
- # to given partition assignment. It allows for easier offset management for virtual
19
- # virtual partition cases as it provides us ability to mark as consumed and move the
20
- # real offset behind as expected.
21
- #
22
- # @note We still use the regular coordinator "real" offset management as we want to have
23
- # them as separated as possible because the real seek offset management is also used for
24
- # pausing, filtering and others and should not be impacted by the virtual one
25
- #
26
- # @note This manager is **not** thread-safe by itself. It should operate from coordinator
27
- # locked locations.
28
- class VirtualOffsetManager
29
- attr_reader :groups
30
-
31
- # @param topic [String]
32
- # @param partition [Integer]
33
- # @param offset_metadata_strategy [Symbol] what metadata should we select. That is, should
34
- # we use the most recent or one picked from the offset that is going to be committed
35
- #
36
- # @note We need topic and partition because we use a seek message (virtual) for real offset
37
- # management. We could keep real message reference but this can be memory consuming
38
- # and not worth it.
39
- def initialize(topic, partition, offset_metadata_strategy)
40
- @topic = topic
41
- @partition = partition
42
- @groups = []
43
- @marked = {}
44
- @offsets_metadata = {}
45
- @real_offset = -1
46
- @offset_metadata_strategy = offset_metadata_strategy
47
- @current_offset_metadata = nil
48
- end
49
-
50
- # Clears the manager for a next collective operation
51
- def clear
52
- @groups.clear
53
- @offsets_metadata.clear
54
- @current_offset_metadata = nil
55
- @marked.clear
56
- @real_offset = -1
57
- end
58
-
59
- # Registers an offset group coming from one virtual consumer. In order to move the real
60
- # underlying offset accordingly, we need to make sure to track the virtual consumers
61
- # offsets groups independently and only materialize the end result.
62
- #
63
- # @param offsets_group [Array<Integer>] offsets from one virtual consumer
64
- def register(offsets_group)
65
- @groups << offsets_group
66
-
67
- offsets_group.each { |offset| @marked[offset] = false }
68
- end
69
-
70
- # Marks given message as marked (virtually consumed).
71
- # We mark given message offset and other earlier offsets from the same group as done
72
- # and we can refresh our real offset representation based on that as it might have changed
73
- # to a newer real offset.
74
- # @param message [Karafka::Messages::Message] message coming from VP we want to mark
75
- # @param offset_metadata [String, nil] offset metadata. `nil` if none
76
- def mark(message, offset_metadata)
77
- offset = message.offset
78
-
79
- # Store metadata when we materialize the most stable offset
80
- @offsets_metadata[offset] = offset_metadata
81
- @current_offset_metadata = offset_metadata
82
-
83
- group = @groups.find { |reg_group| reg_group.include?(offset) }
84
-
85
- # This case can happen when someone uses MoM and wants to mark message from a previous
86
- # batch as consumed. We can add it, since the real offset refresh will point to it
87
- unless group
88
- group = [offset]
89
- @groups << group
90
- end
91
-
92
- position = group.index(offset)
93
-
94
- # Mark all previous messages from the same group also as virtually consumed
95
- group[0..position].each do |markable_offset|
96
- # Set previous messages metadata offset as the offset of higher one for overwrites
97
- # unless a different metadata were set explicitely
98
- @offsets_metadata[markable_offset] ||= offset_metadata
99
- @marked[markable_offset] = true
100
- end
101
-
102
- # Recompute the real offset representation
103
- materialize_real_offset
104
- end
105
-
106
- # Mark all from all groups including the `message`.
107
- # Useful when operating in a collapsed state for marking
108
- # @param message [Karafka::Messages::Message]
109
- # @param offset_metadata [String, nil]
110
- def mark_until(message, offset_metadata)
111
- mark(message, offset_metadata)
112
-
113
- @groups.each do |group|
114
- group.each do |offset|
115
- next if offset > message.offset
116
-
117
- @offsets_metadata[offset] = offset_metadata
118
- @marked[offset] = true
119
- end
120
- end
121
-
122
- materialize_real_offset
123
- end
124
-
125
- # @return [Array<Integer>] Offsets of messages already marked as consumed virtually
126
- def marked
127
- @marked.select { |_, status| status }.map(&:first).sort
128
- end
129
-
130
- # Is there a real offset we can mark as consumed
131
- # @return [Boolean]
132
- def markable?
133
- !@real_offset.negative?
134
- end
135
-
136
- # @return [Array<Messages::Seek, String>] markable message for real offset marking and
137
- # its associated metadata
138
- def markable
139
- raise Errors::InvalidRealOffsetUsageError unless markable?
140
-
141
- offset_metadata = case @offset_metadata_strategy
142
- when :exact
143
- @offsets_metadata.fetch(@real_offset)
144
- when :current
145
- @current_offset_metadata
146
- else
147
- raise Errors::UnsupportedCaseError, @offset_metadata_strategy
148
- end
149
-
150
- [
151
- Messages::Seek.new(
152
- @topic,
153
- @partition,
154
- @real_offset
155
- ),
156
- offset_metadata
157
- ]
158
- end
159
-
160
- private
161
-
162
- # Recomputes the biggest possible real offset we can have.
163
- # It picks the the biggest offset that has uninterrupted stream of virtually marked as
164
- # consumed because this will be the collective offset.
165
- def materialize_real_offset
166
- @marked.to_a.sort_by(&:first).each do |offset, marked|
167
- break unless marked
168
-
169
- @real_offset = offset
170
- end
171
-
172
- @real_offset = (@marked.keys.min - 1) if @real_offset.negative?
173
- end
174
- end
175
- end
176
- end
177
- end