karafka 2.2.14 → 2.3.0.alpha2
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 +38 -12
- data/.ruby-version +1 -1
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +16 -16
- data/README.md +0 -2
- data/SECURITY.md +23 -0
- data/bin/integrations +1 -1
- data/config/locales/errors.yml +7 -1
- data/config/locales/pro_errors.yml +22 -0
- data/docker-compose.yml +1 -1
- data/karafka.gemspec +2 -2
- data/lib/karafka/admin/acl.rb +287 -0
- data/lib/karafka/admin.rb +9 -13
- data/lib/karafka/app.rb +5 -3
- data/lib/karafka/base_consumer.rb +9 -1
- data/lib/karafka/cli/base.rb +1 -1
- data/lib/karafka/connection/client.rb +83 -76
- data/lib/karafka/connection/conductor.rb +28 -0
- data/lib/karafka/connection/listener.rb +159 -42
- data/lib/karafka/connection/listeners_batch.rb +5 -11
- data/lib/karafka/connection/manager.rb +72 -0
- data/lib/karafka/connection/messages_buffer.rb +12 -0
- data/lib/karafka/connection/proxy.rb +17 -0
- data/lib/karafka/connection/status.rb +75 -0
- data/lib/karafka/contracts/config.rb +14 -10
- data/lib/karafka/contracts/consumer_group.rb +9 -1
- data/lib/karafka/contracts/topic.rb +3 -1
- data/lib/karafka/errors.rb +17 -0
- data/lib/karafka/instrumentation/logger_listener.rb +3 -0
- data/lib/karafka/instrumentation/notifications.rb +13 -5
- data/lib/karafka/instrumentation/vendors/appsignal/metrics_listener.rb +31 -28
- data/lib/karafka/instrumentation/vendors/datadog/logger_listener.rb +20 -1
- data/lib/karafka/instrumentation/vendors/datadog/metrics_listener.rb +15 -12
- data/lib/karafka/instrumentation/vendors/kubernetes/liveness_listener.rb +39 -36
- data/lib/karafka/pro/base_consumer.rb +47 -0
- data/lib/karafka/pro/connection/manager.rb +269 -0
- data/lib/karafka/pro/connection/multiplexing/listener.rb +40 -0
- data/lib/karafka/pro/iterator/tpl_builder.rb +1 -1
- data/lib/karafka/pro/iterator.rb +1 -6
- data/lib/karafka/pro/loader.rb +14 -0
- data/lib/karafka/pro/processing/coordinator.rb +2 -1
- data/lib/karafka/pro/processing/executor.rb +37 -0
- data/lib/karafka/pro/processing/expansions_selector.rb +32 -0
- data/lib/karafka/pro/processing/jobs/periodic.rb +41 -0
- data/lib/karafka/pro/processing/jobs/periodic_non_blocking.rb +32 -0
- data/lib/karafka/pro/processing/jobs_builder.rb +14 -3
- data/lib/karafka/pro/processing/offset_metadata/consumer.rb +44 -0
- data/lib/karafka/pro/processing/offset_metadata/fetcher.rb +131 -0
- data/lib/karafka/pro/processing/offset_metadata/listener.rb +46 -0
- data/lib/karafka/pro/processing/schedulers/base.rb +39 -23
- data/lib/karafka/pro/processing/schedulers/default.rb +12 -14
- data/lib/karafka/pro/processing/strategies/default.rb +154 -1
- data/lib/karafka/pro/processing/strategies/dlq/default.rb +39 -0
- data/lib/karafka/pro/processing/strategies/vp/default.rb +65 -25
- data/lib/karafka/pro/processing/virtual_offset_manager.rb +41 -11
- data/lib/karafka/pro/routing/features/long_running_job/topic.rb +2 -0
- data/lib/karafka/pro/routing/features/multiplexing/config.rb +38 -0
- data/lib/karafka/pro/routing/features/multiplexing/contracts/topic.rb +114 -0
- data/lib/karafka/pro/routing/features/multiplexing/patches/contracts/consumer_group.rb +42 -0
- data/lib/karafka/pro/routing/features/multiplexing/proxy.rb +38 -0
- data/lib/karafka/pro/routing/features/multiplexing/subscription_group.rb +42 -0
- data/lib/karafka/pro/routing/features/multiplexing/subscription_groups_builder.rb +40 -0
- data/lib/karafka/pro/routing/features/multiplexing.rb +59 -0
- data/lib/karafka/pro/routing/features/non_blocking_job/topic.rb +32 -0
- data/lib/karafka/pro/routing/features/non_blocking_job.rb +37 -0
- data/lib/karafka/pro/routing/features/offset_metadata/config.rb +33 -0
- data/lib/karafka/pro/routing/features/offset_metadata/contracts/topic.rb +42 -0
- data/lib/karafka/pro/routing/features/offset_metadata/topic.rb +65 -0
- data/lib/karafka/pro/routing/features/offset_metadata.rb +40 -0
- data/lib/karafka/pro/routing/features/patterns/contracts/consumer_group.rb +4 -0
- data/lib/karafka/pro/routing/features/patterns/detector.rb +18 -10
- data/lib/karafka/pro/routing/features/periodic_job/config.rb +37 -0
- data/lib/karafka/pro/routing/features/periodic_job/contracts/topic.rb +44 -0
- data/lib/karafka/pro/routing/features/periodic_job/topic.rb +94 -0
- data/lib/karafka/pro/routing/features/periodic_job.rb +27 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/config.rb +1 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/contracts/topic.rb +1 -0
- data/lib/karafka/pro/routing/features/virtual_partitions/topic.rb +7 -2
- data/lib/karafka/process.rb +5 -3
- data/lib/karafka/processing/coordinator.rb +5 -1
- data/lib/karafka/processing/executor.rb +16 -10
- data/lib/karafka/processing/executors_buffer.rb +19 -4
- data/lib/karafka/processing/schedulers/default.rb +3 -2
- data/lib/karafka/processing/strategies/default.rb +6 -0
- data/lib/karafka/processing/strategies/dlq.rb +36 -0
- data/lib/karafka/routing/builder.rb +12 -2
- data/lib/karafka/routing/consumer_group.rb +5 -5
- data/lib/karafka/routing/features/base.rb +44 -8
- data/lib/karafka/routing/features/dead_letter_queue/config.rb +6 -1
- data/lib/karafka/routing/features/dead_letter_queue/contracts/topic.rb +1 -0
- data/lib/karafka/routing/features/dead_letter_queue/topic.rb +9 -2
- data/lib/karafka/routing/subscription_group.rb +2 -2
- data/lib/karafka/routing/subscription_groups_builder.rb +11 -2
- data/lib/karafka/routing/topic.rb +8 -10
- data/lib/karafka/runner.rb +13 -3
- data/lib/karafka/server.rb +5 -9
- data/lib/karafka/setup/config.rb +17 -0
- data/lib/karafka/status.rb +23 -14
- data/lib/karafka/templates/karafka.rb.erb +7 -0
- data/lib/karafka/time_trackers/partition_usage.rb +56 -0
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +42 -10
- metadata.gz.sig +0 -0
- data/lib/karafka/connection/consumer_group_coordinator.rb +0 -48
|
@@ -27,6 +27,144 @@ module Karafka
|
|
|
27
27
|
# Apply strategy for a non-feature based flow
|
|
28
28
|
FEATURES = %i[].freeze
|
|
29
29
|
|
|
30
|
+
# Allows to set offset metadata that will be used with the upcoming marking as consumed
|
|
31
|
+
# as long as a different offset metadata was not used. After it was used either via
|
|
32
|
+
# `#mark_as_consumed` or `#mark_as_consumed!` it will be set back to `nil`. It is done
|
|
33
|
+
# that way to provide the end user with ability to influence metadata on the non-user
|
|
34
|
+
# initiated markings in complex flows.
|
|
35
|
+
#
|
|
36
|
+
# @param offset_metadata [String, nil] metadata we want to store with the upcoming
|
|
37
|
+
# marking as consumed
|
|
38
|
+
#
|
|
39
|
+
# @note Please be aware, that offset metadata set this way will be passed to any marking
|
|
40
|
+
# as consumed even if it was not user initiated. For example in the DLQ flow.
|
|
41
|
+
def store_offset_metadata(offset_metadata)
|
|
42
|
+
@_current_offset_metadata = offset_metadata
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Marks message as consumed in an async way.
|
|
46
|
+
#
|
|
47
|
+
# @param message [Messages::Message] last successfully processed message.
|
|
48
|
+
# @param offset_metadata [String, nil] offset metadata string or nil if nothing
|
|
49
|
+
# @return [Boolean] true if we were able to mark the offset, false otherwise.
|
|
50
|
+
# False indicates that we were not able and that we have lost the partition.
|
|
51
|
+
#
|
|
52
|
+
# @note We keep track of this offset in case we would mark as consumed and got error when
|
|
53
|
+
# processing another message. In case like this we do not pause on the message we've
|
|
54
|
+
# already processed but rather at the next one. This applies to both sync and async
|
|
55
|
+
# versions of this method.
|
|
56
|
+
def mark_as_consumed(message, offset_metadata = @_current_offset_metadata)
|
|
57
|
+
if @_in_transaction
|
|
58
|
+
mark_in_transaction(message, offset_metadata, true)
|
|
59
|
+
else
|
|
60
|
+
# seek offset can be nil only in case `#seek` was invoked with offset reset request
|
|
61
|
+
# In case like this we ignore marking
|
|
62
|
+
return true if coordinator.seek_offset.nil?
|
|
63
|
+
# Ignore earlier offsets than the one we already committed
|
|
64
|
+
return true if coordinator.seek_offset > message.offset
|
|
65
|
+
return false if revoked?
|
|
66
|
+
return revoked? unless client.mark_as_consumed(message, offset_metadata)
|
|
67
|
+
|
|
68
|
+
coordinator.seek_offset = message.offset + 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
ensure
|
|
73
|
+
@_current_offset_metadata = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Marks message as consumed in a sync way.
|
|
77
|
+
#
|
|
78
|
+
# @param message [Messages::Message] last successfully processed message.
|
|
79
|
+
# @param offset_metadata [String, nil] offset metadata string or nil if nothing
|
|
80
|
+
# @return [Boolean] true if we were able to mark the offset, false otherwise.
|
|
81
|
+
# False indicates that we were not able and that we have lost the partition.
|
|
82
|
+
def mark_as_consumed!(message, offset_metadata = @_current_offset_metadata)
|
|
83
|
+
if @_in_transaction
|
|
84
|
+
mark_in_transaction(message, offset_metadata, false)
|
|
85
|
+
else
|
|
86
|
+
# seek offset can be nil only in case `#seek` was invoked with offset reset request
|
|
87
|
+
# In case like this we ignore marking
|
|
88
|
+
return true if coordinator.seek_offset.nil?
|
|
89
|
+
# Ignore earlier offsets than the one we already committed
|
|
90
|
+
return true if coordinator.seek_offset > message.offset
|
|
91
|
+
return false if revoked?
|
|
92
|
+
|
|
93
|
+
return revoked? unless client.mark_as_consumed!(message, offset_metadata)
|
|
94
|
+
|
|
95
|
+
coordinator.seek_offset = message.offset + 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
true
|
|
99
|
+
ensure
|
|
100
|
+
@_current_offset_metadata = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Starts producer transaction, saves the transaction context for transactional marking
|
|
104
|
+
# and runs user code in this context
|
|
105
|
+
#
|
|
106
|
+
# Transactions on a consumer level differ from those initiated by the producer as they
|
|
107
|
+
# allow to mark offsets inside of the transaction. If the transaction is initialized
|
|
108
|
+
# only from the consumer, the offset will be stored in a regular fashion.
|
|
109
|
+
#
|
|
110
|
+
# @param block [Proc] code that we want to run in a transaction
|
|
111
|
+
def transaction(&block)
|
|
112
|
+
transaction_started = false
|
|
113
|
+
|
|
114
|
+
# Prevent from nested transactions. It would not make any sense
|
|
115
|
+
raise Errors::TransactionAlreadyInitializedError if @_in_transaction
|
|
116
|
+
|
|
117
|
+
transaction_started = true
|
|
118
|
+
@_transaction_marked = []
|
|
119
|
+
@_in_transaction = true
|
|
120
|
+
|
|
121
|
+
producer.transaction(&block)
|
|
122
|
+
|
|
123
|
+
@_in_transaction = false
|
|
124
|
+
|
|
125
|
+
# This offset is already stored in transaction but we set it here anyhow because we
|
|
126
|
+
# want to make sure our internal in-memory state is aligned with the transaction
|
|
127
|
+
#
|
|
128
|
+
# @note We never need to use the blocking `#mark_as_consumed!` here because the offset
|
|
129
|
+
# anyhow was already stored during the transaction
|
|
130
|
+
#
|
|
131
|
+
# @note In theory we could only keep reference to the most recent marking and reject
|
|
132
|
+
# others. We however do not do it for two reasons:
|
|
133
|
+
# - User may have non standard flow relying on some alternative order and we want to
|
|
134
|
+
# mimic this
|
|
135
|
+
# - Complex strategies like VPs can use this in VPs to mark in parallel without
|
|
136
|
+
# having to redefine the transactional flow completely
|
|
137
|
+
@_transaction_marked.each do |marking|
|
|
138
|
+
marking.pop ? mark_as_consumed(*marking) : mark_as_consumed!(*marking)
|
|
139
|
+
end
|
|
140
|
+
ensure
|
|
141
|
+
if transaction_started
|
|
142
|
+
@_transaction_marked.clear
|
|
143
|
+
@_in_transaction = false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Stores the next offset for processing inside of the transaction and stores it in a
|
|
148
|
+
# local accumulator for post-transaction status update
|
|
149
|
+
#
|
|
150
|
+
# @param message [Messages::Message] message we want to commit inside of a transaction
|
|
151
|
+
# @param offset_metadata [String, nil] offset metadata or nil if none
|
|
152
|
+
# @param async [Boolean] should we mark in async or sync way (applicable only to post
|
|
153
|
+
# transaction state synchronization usage as within transaction it is always sync)
|
|
154
|
+
def mark_in_transaction(message, offset_metadata, async)
|
|
155
|
+
raise Errors::TransactionRequiredError unless @_in_transaction
|
|
156
|
+
raise Errors::AssignmentLostError if revoked?
|
|
157
|
+
|
|
158
|
+
producer.transaction_mark_as_consumed(
|
|
159
|
+
client,
|
|
160
|
+
message,
|
|
161
|
+
offset_metadata
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@_transaction_marked ||= []
|
|
165
|
+
@_transaction_marked << [message, offset_metadata, async]
|
|
166
|
+
end
|
|
167
|
+
|
|
30
168
|
# No actions needed for the standard flow here
|
|
31
169
|
def handle_before_schedule_consume
|
|
32
170
|
Karafka.monitor.instrument('consumer.before_schedule_consume', caller: self)
|
|
@@ -87,7 +225,7 @@ module Karafka
|
|
|
87
225
|
end
|
|
88
226
|
end
|
|
89
227
|
|
|
90
|
-
# Standard
|
|
228
|
+
# Standard flow for revocation
|
|
91
229
|
def handle_revoked
|
|
92
230
|
coordinator.on_revoked do
|
|
93
231
|
resume
|
|
@@ -100,6 +238,21 @@ module Karafka
|
|
|
100
238
|
revoked
|
|
101
239
|
end
|
|
102
240
|
end
|
|
241
|
+
|
|
242
|
+
# No action needed for the tick standard flow
|
|
243
|
+
def handle_before_schedule_tick
|
|
244
|
+
Karafka.monitor.instrument('consumer.before_schedule_tick', caller: self)
|
|
245
|
+
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Runs the consumer `#tick` method with reporting
|
|
250
|
+
def handle_tick
|
|
251
|
+
Karafka.monitor.instrument('consumer.tick', caller: self)
|
|
252
|
+
Karafka.monitor.instrument('consumer.ticked', caller: self) do
|
|
253
|
+
tick
|
|
254
|
+
end
|
|
255
|
+
end
|
|
103
256
|
end
|
|
104
257
|
end
|
|
105
258
|
end
|
|
@@ -26,6 +26,45 @@ module Karafka
|
|
|
26
26
|
dead_letter_queue
|
|
27
27
|
].freeze
|
|
28
28
|
|
|
29
|
+
# Override of the standard `#mark_as_consumed` in order to handle the pause tracker
|
|
30
|
+
# reset in case DLQ is marked as fully independent. When DLQ is marked independent,
|
|
31
|
+
# any offset marking causes the pause count tracker to reset. This is useful when
|
|
32
|
+
# the error is not due to the collective batch operations state but due to intermediate
|
|
33
|
+
# "crawling" errors that move with it
|
|
34
|
+
#
|
|
35
|
+
# @see `Strategies::Default#mark_as_consumed` for more details
|
|
36
|
+
# @param message [Messages::Message]
|
|
37
|
+
# @param offset_metadata [String, nil]
|
|
38
|
+
def mark_as_consumed(message, offset_metadata = @_current_offset_metadata)
|
|
39
|
+
return super unless retrying?
|
|
40
|
+
return super unless topic.dead_letter_queue.independent?
|
|
41
|
+
return false unless super
|
|
42
|
+
|
|
43
|
+
coordinator.pause_tracker.reset
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
ensure
|
|
47
|
+
@_current_offset_metadata = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Override of the standard `#mark_as_consumed!`. Resets the pause tracker count in case
|
|
51
|
+
# DLQ was configured with the `independent` flag.
|
|
52
|
+
#
|
|
53
|
+
# @see `Strategies::Default#mark_as_consumed!` for more details
|
|
54
|
+
# @param message [Messages::Message]
|
|
55
|
+
# @param offset_metadata [String, nil]
|
|
56
|
+
def mark_as_consumed!(message, offset_metadata = @_current_offset_metadata)
|
|
57
|
+
return super unless retrying?
|
|
58
|
+
return super unless topic.dead_letter_queue.independent?
|
|
59
|
+
return false unless super
|
|
60
|
+
|
|
61
|
+
coordinator.pause_tracker.reset
|
|
62
|
+
|
|
63
|
+
true
|
|
64
|
+
ensure
|
|
65
|
+
@_current_offset_metadata = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
29
68
|
# When we encounter non-recoverable message, we skip it and go on with our lives
|
|
30
69
|
def handle_after_consume
|
|
31
70
|
coordinator.on_finished do |last_group_message|
|
|
@@ -29,40 +29,80 @@ module Karafka
|
|
|
29
29
|
].freeze
|
|
30
30
|
|
|
31
31
|
# @param message [Karafka::Messages::Message] marks message as consumed
|
|
32
|
+
# @param offset_metadata [String, nil]
|
|
32
33
|
# @note This virtual offset management uses a regular default marking API underneath.
|
|
33
34
|
# We do not alter the "real" marking API, as VPs are just one of many cases we want
|
|
34
35
|
# to support and we do not want to impact them with collective offsets management
|
|
35
|
-
def mark_as_consumed(message)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
manager.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
def mark_as_consumed(message, offset_metadata = @_current_offset_metadata)
|
|
37
|
+
if @_in_transaction && !collapsed?
|
|
38
|
+
mark_in_transaction(message, offset_metadata, true)
|
|
39
|
+
elsif collapsed?
|
|
40
|
+
super
|
|
41
|
+
else
|
|
42
|
+
manager = coordinator.virtual_offset_manager
|
|
43
|
+
|
|
44
|
+
coordinator.synchronize do
|
|
45
|
+
manager.mark(message, offset_metadata)
|
|
46
|
+
# If this is last marking on a finished flow, we can use the original
|
|
47
|
+
# last message and in order to do so, we need to mark all previous messages as
|
|
48
|
+
# consumed as otherwise the computed offset could be different
|
|
49
|
+
# We mark until our offset just in case of a DLQ flow or similar, where we do not
|
|
50
|
+
# want to mark all but until the expected location
|
|
51
|
+
manager.mark_until(message, offset_metadata) if coordinator.finished?
|
|
52
|
+
|
|
53
|
+
return revoked? unless manager.markable?
|
|
54
|
+
|
|
55
|
+
manager.markable? ? super(*manager.markable) : revoked?
|
|
56
|
+
end
|
|
52
57
|
end
|
|
58
|
+
ensure
|
|
59
|
+
@_current_offset_metadata = nil
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
# @param message [Karafka::Messages::Message] blocking marks message as consumed
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
# @param offset_metadata [String, nil]
|
|
64
|
+
def mark_as_consumed!(message, offset_metadata = @_current_offset_metadata)
|
|
65
|
+
if @_in_transaction && !collapsed?
|
|
66
|
+
mark_in_transaction(message, offset_metadata, false)
|
|
67
|
+
elsif collapsed?
|
|
68
|
+
super
|
|
69
|
+
else
|
|
70
|
+
manager = coordinator.virtual_offset_manager
|
|
71
|
+
|
|
72
|
+
coordinator.synchronize do
|
|
73
|
+
manager.mark(message, offset_metadata)
|
|
74
|
+
manager.mark_until(message, offset_metadata) if coordinator.finished?
|
|
75
|
+
manager.markable? ? super(*manager.markable) : revoked?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
ensure
|
|
79
|
+
@_current_offset_metadata = nil
|
|
80
|
+
end
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
# Stores the next offset for processing inside of the transaction when collapsed and
|
|
83
|
+
# accumulates marking as consumed in the local buffer.
|
|
84
|
+
#
|
|
85
|
+
# Due to nature of VPs we cannot provide full EOS support but we can simulate it,
|
|
86
|
+
# making sure that no offset are stored unless transaction is finished. We do it by
|
|
87
|
+
# accumulating the post-transaction marking requests and after it is successfully done
|
|
88
|
+
# we mark each as consumed. This effectively on errors "rollbacks" the state and
|
|
89
|
+
# prevents offset storage.
|
|
90
|
+
#
|
|
91
|
+
# Since the EOS here is "weak", we do not have to worry about the race-conditions and
|
|
92
|
+
# we do not have to have any mutexes.
|
|
93
|
+
#
|
|
94
|
+
# @param message [Messages::Message] message we want to commit inside of a transaction
|
|
95
|
+
# @param offset_metadata [String, nil] offset metadata or nil if none
|
|
96
|
+
# @param async [Boolean] should we mark in async or sync way (applicable only to post
|
|
97
|
+
# transaction state synchronization usage as within transaction it is always sync)
|
|
98
|
+
def mark_in_transaction(message, offset_metadata, async)
|
|
99
|
+
raise Errors::TransactionRequiredError unless @_in_transaction
|
|
100
|
+
# Prevent from attempts of offset storage when we no longer own the assignment
|
|
101
|
+
raise Errors::AssignmentLostError if revoked?
|
|
60
102
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
manager.markable? ? super(manager.markable) : revoked?
|
|
65
|
-
end
|
|
103
|
+
return super if collapsed?
|
|
104
|
+
|
|
105
|
+
@_transaction_marked << [message, offset_metadata, async]
|
|
66
106
|
end
|
|
67
107
|
|
|
68
108
|
# @return [Boolean] is the virtual processing collapsed in the context of given
|
|
@@ -30,22 +30,29 @@ module Karafka
|
|
|
30
30
|
|
|
31
31
|
# @param topic [String]
|
|
32
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
|
|
33
35
|
#
|
|
34
36
|
# @note We need topic and partition because we use a seek message (virtual) for real offset
|
|
35
37
|
# management. We could keep real message reference but this can be memory consuming
|
|
36
38
|
# and not worth it.
|
|
37
|
-
def initialize(topic, partition)
|
|
39
|
+
def initialize(topic, partition, offset_metadata_strategy)
|
|
38
40
|
@topic = topic
|
|
39
41
|
@partition = partition
|
|
40
42
|
@groups = []
|
|
41
43
|
@marked = {}
|
|
44
|
+
@offsets_metadata = {}
|
|
42
45
|
@real_offset = -1
|
|
46
|
+
@offset_metadata_strategy = offset_metadata_strategy
|
|
47
|
+
@current_offset_metadata = nil
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# Clears the manager for a next collective operation
|
|
46
51
|
def clear
|
|
47
52
|
@groups.clear
|
|
48
|
-
@
|
|
53
|
+
@offsets_metadata.clear
|
|
54
|
+
@current_offset_metadata = nil
|
|
55
|
+
@marked.clear
|
|
49
56
|
@real_offset = -1
|
|
50
57
|
end
|
|
51
58
|
|
|
@@ -65,9 +72,14 @@ module Karafka
|
|
|
65
72
|
# and we can refresh our real offset representation based on that as it might have changed
|
|
66
73
|
# to a newer real offset.
|
|
67
74
|
# @param message [Karafka::Messages::Message] message coming from VP we want to mark
|
|
68
|
-
|
|
75
|
+
# @param offset_metadata [String, nil] offset metadata. `nil` if none
|
|
76
|
+
def mark(message, offset_metadata)
|
|
69
77
|
offset = message.offset
|
|
70
78
|
|
|
79
|
+
# Store metadata when we materialize the most stable offset
|
|
80
|
+
@offsets_metadata[offset] = offset_metadata
|
|
81
|
+
@current_offset_metadata = offset_metadata
|
|
82
|
+
|
|
71
83
|
group = @groups.find { |reg_group| reg_group.include?(offset) }
|
|
72
84
|
|
|
73
85
|
# This case can happen when someone uses MoM and wants to mark message from a previous
|
|
@@ -81,6 +93,9 @@ module Karafka
|
|
|
81
93
|
|
|
82
94
|
# Mark all previous messages from the same group also as virtually consumed
|
|
83
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
|
|
84
99
|
@marked[markable_offset] = true
|
|
85
100
|
end
|
|
86
101
|
|
|
@@ -91,13 +106,15 @@ module Karafka
|
|
|
91
106
|
# Mark all from all groups including the `message`.
|
|
92
107
|
# Useful when operating in a collapsed state for marking
|
|
93
108
|
# @param message [Karafka::Messages::Message]
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
# @param offset_metadata [String, nil]
|
|
110
|
+
def mark_until(message, offset_metadata)
|
|
111
|
+
mark(message, offset_metadata)
|
|
96
112
|
|
|
97
113
|
@groups.each do |group|
|
|
98
114
|
group.each do |offset|
|
|
99
115
|
next if offset > message.offset
|
|
100
116
|
|
|
117
|
+
@offsets_metadata[offset] = offset_metadata
|
|
101
118
|
@marked[offset] = true
|
|
102
119
|
end
|
|
103
120
|
end
|
|
@@ -116,15 +133,28 @@ module Karafka
|
|
|
116
133
|
!@real_offset.negative?
|
|
117
134
|
end
|
|
118
135
|
|
|
119
|
-
# @return [Messages::Seek] markable message for real offset marking
|
|
136
|
+
# @return [Array<Messages::Seek, String>] markable message for real offset marking and
|
|
137
|
+
# its associated metadata
|
|
120
138
|
def markable
|
|
121
139
|
raise Errors::InvalidRealOffsetUsageError unless markable?
|
|
122
140
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
]
|
|
128
158
|
end
|
|
129
159
|
|
|
130
160
|
private
|
|
@@ -0,0 +1,38 @@
|
|
|
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 Routing
|
|
17
|
+
module Features
|
|
18
|
+
class Multiplexing < Base
|
|
19
|
+
# Multiplexing configuration
|
|
20
|
+
Config = Struct.new(
|
|
21
|
+
:active,
|
|
22
|
+
:min,
|
|
23
|
+
:max,
|
|
24
|
+
:boot,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
alias_method :active?, :active
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] true if we are allowed to upscale and downscale
|
|
30
|
+
def dynamic?
|
|
31
|
+
min < max
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
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 Routing
|
|
17
|
+
module Features
|
|
18
|
+
class Multiplexing < Base
|
|
19
|
+
# Namespace for multiplexing feature contracts
|
|
20
|
+
module Contracts
|
|
21
|
+
# Validates the subscription group multiplexing setup
|
|
22
|
+
# We validate it on the topic level as subscription groups are not built during the
|
|
23
|
+
# routing as they are pre-run dynamically built.
|
|
24
|
+
#
|
|
25
|
+
# multiplexing attributes are optional since multiplexing may not be enabled
|
|
26
|
+
class Topic < Karafka::Contracts::Base
|
|
27
|
+
configure do |config|
|
|
28
|
+
config.error_messages = YAML.safe_load(
|
|
29
|
+
File.read(
|
|
30
|
+
File.join(Karafka.gem_root, 'config', 'locales', 'pro_errors.yml')
|
|
31
|
+
)
|
|
32
|
+
).fetch('en').fetch('validations').fetch('topic')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
nested(:subscription_group_details) do
|
|
36
|
+
optional(:multiplexing_min) { |val| val.is_a?(Integer) && val >= 1 }
|
|
37
|
+
optional(:multiplexing_max) { |val| val.is_a?(Integer) && val >= 1 }
|
|
38
|
+
optional(:multiplexing_boot) { |val| val.is_a?(Integer) && val >= 1 }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Makes sure min is not more than max
|
|
42
|
+
virtual do |data, errors|
|
|
43
|
+
next unless errors.empty?
|
|
44
|
+
next unless min(data)
|
|
45
|
+
next unless max(data)
|
|
46
|
+
|
|
47
|
+
min = min(data)
|
|
48
|
+
max = max(data)
|
|
49
|
+
|
|
50
|
+
next if min <= max
|
|
51
|
+
|
|
52
|
+
[[%w[subscription_group_details], :multiplexing_min_max_mismatch]]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Makes sure, that boot is between min and max
|
|
56
|
+
virtual do |data, errors|
|
|
57
|
+
next unless errors.empty?
|
|
58
|
+
next unless min(data)
|
|
59
|
+
next unless max(data)
|
|
60
|
+
next unless boot(data)
|
|
61
|
+
|
|
62
|
+
min = min(data)
|
|
63
|
+
max = max(data)
|
|
64
|
+
boot = boot(data)
|
|
65
|
+
|
|
66
|
+
next if boot >= min && boot <= max
|
|
67
|
+
|
|
68
|
+
[[%w[subscription_group_details], :multiplexing_boot_mismatch]]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Makes sure, that boot is equal to min and max when not in dynamic mode
|
|
72
|
+
virtual do |data, errors|
|
|
73
|
+
next unless errors.empty?
|
|
74
|
+
next unless min(data)
|
|
75
|
+
next unless max(data)
|
|
76
|
+
next unless boot(data)
|
|
77
|
+
|
|
78
|
+
min = min(data)
|
|
79
|
+
max = max(data)
|
|
80
|
+
boot = boot(data)
|
|
81
|
+
|
|
82
|
+
# In dynamic mode there are other rules to check boot
|
|
83
|
+
next if min != max
|
|
84
|
+
next if boot == min
|
|
85
|
+
|
|
86
|
+
[[%w[subscription_group_details], :multiplexing_boot_not_dynamic]]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class << self
|
|
90
|
+
# @param data [Hash] topic details
|
|
91
|
+
# @return [Integer, false] min or false if missing
|
|
92
|
+
def min(data)
|
|
93
|
+
data[:subscription_group_details].fetch(:multiplexing_min, false)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param data [Hash] topic details
|
|
97
|
+
# @return [Integer, false] max or false if missing
|
|
98
|
+
def max(data)
|
|
99
|
+
data[:subscription_group_details].fetch(:multiplexing_max, false)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param data [Hash] topic details
|
|
103
|
+
# @return [Integer, false] boot or false if missing
|
|
104
|
+
def boot(data)
|
|
105
|
+
data[:subscription_group_details].fetch(:multiplexing_boot, false)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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 Routing
|
|
17
|
+
module Features
|
|
18
|
+
class Multiplexing < Base
|
|
19
|
+
# Patches to Karafka OSS
|
|
20
|
+
module Patches
|
|
21
|
+
# Contracts patches
|
|
22
|
+
module Contracts
|
|
23
|
+
# Consumer group contract patches
|
|
24
|
+
module ConsumerGroup
|
|
25
|
+
# Redefines the setup allowing for multiple sgs as long as with different names
|
|
26
|
+
#
|
|
27
|
+
# @param topic [Hash] topic config hash
|
|
28
|
+
# @return [Array] topic unique key for validators
|
|
29
|
+
def topic_unique_key(topic)
|
|
30
|
+
[
|
|
31
|
+
topic[:name],
|
|
32
|
+
topic[:subscription_group_details]
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|