karafka 2.1.4 → 2.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +20 -20
- data/karafka.gemspec +2 -2
- data/lib/karafka/admin.rb +37 -4
- data/lib/karafka/base_consumer.rb +21 -5
- data/lib/karafka/connection/client.rb +118 -95
- data/lib/karafka/connection/rebalance_manager.rb +2 -4
- data/lib/karafka/errors.rb +4 -1
- data/lib/karafka/messages/builders/message.rb +0 -3
- data/lib/karafka/messages/seek.rb +3 -0
- data/lib/karafka/patches/rdkafka/bindings.rb +4 -6
- data/lib/karafka/pro/iterator/expander.rb +95 -0
- data/lib/karafka/pro/iterator/tpl_builder.rb +145 -0
- data/lib/karafka/pro/iterator.rb +2 -87
- data/lib/karafka/pro/processing/filters_applier.rb +1 -0
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom.rb +3 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_ftr_lrj_mom_vp.rb +3 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom.rb +3 -1
- data/lib/karafka/pro/processing/strategies/aj/dlq_lrj_mom_vp.rb +3 -1
- data/lib/karafka/pro/processing/strategies/aj/ftr_lrj_mom_vp.rb +3 -1
- data/lib/karafka/pro/processing/strategies/aj/lrj_mom_vp.rb +4 -1
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj.rb +2 -2
- data/lib/karafka/pro/processing/strategies/dlq/ftr_lrj_mom.rb +2 -2
- data/lib/karafka/pro/processing/strategies/dlq/lrj.rb +2 -1
- data/lib/karafka/pro/processing/strategies/dlq/lrj_mom.rb +3 -1
- data/lib/karafka/pro/processing/strategies/ftr/default.rb +8 -1
- data/lib/karafka/pro/processing/strategies/lrj/default.rb +1 -1
- data/lib/karafka/pro/processing/strategies/lrj/ftr.rb +2 -2
- data/lib/karafka/pro/processing/strategies/lrj/ftr_mom.rb +2 -2
- data/lib/karafka/pro/processing/strategies/lrj/mom.rb +3 -1
- data/lib/karafka/pro/processing/virtual_offset_manager.rb +1 -1
- data/lib/karafka/processing/coordinator.rb +14 -0
- data/lib/karafka/processing/strategies/default.rb +27 -11
- data/lib/karafka/railtie.rb +2 -2
- data/lib/karafka/setup/attributes_map.rb +1 -0
- data/lib/karafka/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +10 -9
- metadata.gz.sig +0 -0
- data/lib/karafka/patches/rdkafka/consumer.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6994a6d579728a877f84c87086d093aae8a1f830b891fcb4904883085432fe4
|
4
|
+
data.tar.gz: 13b21009a471194a72971ca81ddc718e044bb96587db0e8f186974f554e9ec62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4711880bde1d2cd1cb34959f740459979b74ff4d28a671a232f88adbe7473cf67e366fc2b492fac761c572f3a6dfc147a59d46fc08e1c5e18df8ac5f108afdd
|
7
|
+
data.tar.gz: c094600c2bd421ce309c0125d60ea82ed0106d5ce4566b3bb8c1aab13c553e7bd2f6651b98029e42ac831b132563b2c502dd1c76defbf8307cd9bd2393b258f7
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
# Karafka framework changelog
|
2
2
|
|
3
|
+
## 2.1.6 (2023-06-29)
|
4
|
+
- [Improvement] Provide time support for iterator
|
5
|
+
- [Improvement] Provide time support for admin `#read_topic`
|
6
|
+
- [Improvement] Provide time support for consumer `#seek`.
|
7
|
+
- [Improvement] Remove no longer needed locks for client operations.
|
8
|
+
- [Improvement] Raise `Karafka::Errors::TopicNotFoundError` when trying to iterate over non-existing topic.
|
9
|
+
- [Improvement] Ensure that Kafka multi-command operations run under mutex together.
|
10
|
+
- [Change] Require `waterdrop` `>= 2.6.2`
|
11
|
+
- [Change] Require `karafka-core` `>= 2.1.1`
|
12
|
+
- [Refactor] Clean-up iterator code.
|
13
|
+
- [Fix] Improve performance in dev environment for a Rails app (juike)
|
14
|
+
- [Fix] Rename `InvalidRealOffsetUsage` to `InvalidRealOffsetUsageError` to align with naming of other errors.
|
15
|
+
- [Fix] Fix unstable spec.
|
16
|
+
- [Fix] Fix a case where automatic `#seek` would overwrite manual seek of a user when running LRJ.
|
17
|
+
- [Fix] Make sure, that user direct `#seek` and `#pause` operations take precedence over system actions.
|
18
|
+
- [Fix] Make sure, that `#pause` and `#resume` with one underlying connection do not race-condition.
|
19
|
+
|
20
|
+
## 2.1.5 (2023-06-19)
|
21
|
+
- [Improvement] Drastically improve `#revoked?` response quality by checking the real time assignment lost state on librdkafka.
|
22
|
+
- [Improvement] Improve eviction of saturated jobs that would run on already revoked assignments.
|
23
|
+
- [Improvement] Expose `#commit_offsets` and `#commit_offsets!` methods in the consumer to provide ability to commit offsets directly to Kafka without having to mark new messages as consumed.
|
24
|
+
- [Improvement] No longer skip offset commit when no messages marked as consumed as `librdkafka` has fixed the crashes there.
|
25
|
+
- [Improvement] Remove no longer needed patches.
|
26
|
+
- [Improvement] Ensure, that the coordinator revocation status is switched upon revocation detection when using `#revoked?`
|
27
|
+
- [Improvement] Add benchmarks for marking as consumed (sync and async).
|
28
|
+
- [Change] Require `karafka-core` `>= 2.1.0`
|
29
|
+
- [Change] Require `waterdrop` `>= 2.6.1`
|
30
|
+
|
3
31
|
## 2.1.4 (2023-06-06)
|
4
32
|
- [Fix] `processing_lag` and `consumption_lag` on empty batch fail on shutdown usage (#1475)
|
5
33
|
|
data/Gemfile.lock
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
karafka (2.1.
|
5
|
-
karafka-core (>= 2.
|
4
|
+
karafka (2.1.6)
|
5
|
+
karafka-core (>= 2.1.1, < 2.2.0)
|
6
6
|
thor (>= 0.20)
|
7
|
-
waterdrop (>= 2.
|
7
|
+
waterdrop (>= 2.6.2, < 3.0.0)
|
8
8
|
zeitwerk (~> 2.3)
|
9
9
|
|
10
10
|
GEM
|
11
11
|
remote: https://rubygems.org/
|
12
12
|
specs:
|
13
|
-
activejob (7.0.
|
14
|
-
activesupport (= 7.0.
|
13
|
+
activejob (7.0.5)
|
14
|
+
activesupport (= 7.0.5)
|
15
15
|
globalid (>= 0.3.6)
|
16
|
-
activesupport (7.0.
|
16
|
+
activesupport (7.0.5)
|
17
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
18
|
i18n (>= 1.6, < 2)
|
19
19
|
minitest (>= 5.1)
|
@@ -28,26 +28,26 @@ GEM
|
|
28
28
|
ffi (1.15.5)
|
29
29
|
globalid (1.1.0)
|
30
30
|
activesupport (>= 5.0)
|
31
|
-
i18n (1.
|
31
|
+
i18n (1.14.1)
|
32
32
|
concurrent-ruby (~> 1.0)
|
33
|
-
karafka-core (2.
|
33
|
+
karafka-core (2.1.1)
|
34
34
|
concurrent-ruby (>= 1.1)
|
35
|
-
karafka-rdkafka (>= 0.
|
36
|
-
karafka-rdkafka (0.
|
35
|
+
karafka-rdkafka (>= 0.13.1, < 0.14.0)
|
36
|
+
karafka-rdkafka (0.13.1)
|
37
37
|
ffi (~> 1.15)
|
38
38
|
mini_portile2 (~> 2.6)
|
39
39
|
rake (> 12)
|
40
|
-
karafka-web (0.
|
40
|
+
karafka-web (0.6.1)
|
41
41
|
erubi (~> 1.4)
|
42
|
-
karafka (>= 2.
|
43
|
-
karafka-core (>= 2.0.
|
44
|
-
roda (~> 3.
|
42
|
+
karafka (>= 2.1.4, < 3.0.0)
|
43
|
+
karafka-core (>= 2.0.13, < 3.0.0)
|
44
|
+
roda (~> 3.68, >= 3.68)
|
45
45
|
tilt (~> 2.0)
|
46
46
|
mini_portile2 (2.8.2)
|
47
|
-
minitest (5.18.
|
48
|
-
rack (3.0.
|
47
|
+
minitest (5.18.1)
|
48
|
+
rack (3.0.8)
|
49
49
|
rake (13.0.6)
|
50
|
-
roda (3.
|
50
|
+
roda (3.69.0)
|
51
51
|
rack
|
52
52
|
rspec (3.12.0)
|
53
53
|
rspec-core (~> 3.12.0)
|
@@ -69,11 +69,11 @@ GEM
|
|
69
69
|
simplecov-html (0.12.3)
|
70
70
|
simplecov_json_formatter (0.1.4)
|
71
71
|
thor (1.2.2)
|
72
|
-
tilt (2.
|
72
|
+
tilt (2.2.0)
|
73
73
|
tzinfo (2.0.6)
|
74
74
|
concurrent-ruby (~> 1.0)
|
75
|
-
waterdrop (2.
|
76
|
-
karafka-core (>= 2.0
|
75
|
+
waterdrop (2.6.2)
|
76
|
+
karafka-core (>= 2.1.0, < 3.0.0)
|
77
77
|
zeitwerk (~> 2.3)
|
78
78
|
zeitwerk (2.6.8)
|
79
79
|
|
data/karafka.gemspec
CHANGED
@@ -21,9 +21,9 @@ Gem::Specification.new do |spec|
|
|
21
21
|
without having to focus on things that are not your business domain.
|
22
22
|
DESC
|
23
23
|
|
24
|
-
spec.add_dependency 'karafka-core', '>= 2.
|
24
|
+
spec.add_dependency 'karafka-core', '>= 2.1.1', '< 2.2.0'
|
25
25
|
spec.add_dependency 'thor', '>= 0.20'
|
26
|
-
spec.add_dependency 'waterdrop', '>= 2.
|
26
|
+
spec.add_dependency 'waterdrop', '>= 2.6.2', '< 3.0.0'
|
27
27
|
spec.add_dependency 'zeitwerk', '~> 2.3'
|
28
28
|
|
29
29
|
if $PROGRAM_NAME.end_with?('gem')
|
data/lib/karafka/admin.rb
CHANGED
@@ -18,6 +18,9 @@ module Karafka
|
|
18
18
|
# retry after checking that the operation was finished or failed using external factor.
|
19
19
|
MAX_WAIT_TIMEOUT = 1
|
20
20
|
|
21
|
+
# Max time for a TPL request. We increase it to compensate for remote clusters latency
|
22
|
+
TPL_REQUEST_TIMEOUT = 2_000
|
23
|
+
|
21
24
|
# How many times should be try. 1 x 60 => 60 seconds wait in total
|
22
25
|
MAX_ATTEMPTS = 60
|
23
26
|
|
@@ -34,7 +37,8 @@ module Karafka
|
|
34
37
|
'enable.auto.commit': false
|
35
38
|
}.freeze
|
36
39
|
|
37
|
-
private_constant :Topic, :CONFIG_DEFAULTS, :MAX_WAIT_TIMEOUT, :
|
40
|
+
private_constant :Topic, :CONFIG_DEFAULTS, :MAX_WAIT_TIMEOUT, :TPL_REQUEST_TIMEOUT,
|
41
|
+
:MAX_ATTEMPTS
|
38
42
|
|
39
43
|
class << self
|
40
44
|
# Allows us to read messages from the topic
|
@@ -42,8 +46,9 @@ module Karafka
|
|
42
46
|
# @param name [String, Symbol] topic name
|
43
47
|
# @param partition [Integer] partition
|
44
48
|
# @param count [Integer] how many messages we want to get at most
|
45
|
-
# @param start_offset [Integer] offset from which we should start. If -1 is provided
|
46
|
-
# (default) we will start from the latest offset
|
49
|
+
# @param start_offset [Integer, Time] offset from which we should start. If -1 is provided
|
50
|
+
# (default) we will start from the latest offset. If time is provided, the appropriate
|
51
|
+
# offset will be resolved.
|
47
52
|
# @param settings [Hash] kafka extra settings (optional)
|
48
53
|
#
|
49
54
|
# @return [Array<Karafka::Messages::Message>] array with messages
|
@@ -53,6 +58,9 @@ module Karafka
|
|
53
58
|
low_offset, high_offset = nil
|
54
59
|
|
55
60
|
with_consumer(settings) do |consumer|
|
61
|
+
# Convert the time offset (if needed)
|
62
|
+
start_offset = resolve_offset(consumer, name.to_s, partition, start_offset)
|
63
|
+
|
56
64
|
low_offset, high_offset = consumer.query_watermark_offsets(name, partition)
|
57
65
|
|
58
66
|
# Select offset dynamically if -1 or less
|
@@ -171,7 +179,9 @@ module Karafka
|
|
171
179
|
# @return [Rdkafka::Metadata] cluster metadata info
|
172
180
|
def cluster_info
|
173
181
|
with_admin do |admin|
|
174
|
-
|
182
|
+
admin.instance_variable_get('@native_kafka').with_inner do |inner|
|
183
|
+
Rdkafka::Metadata.new(inner)
|
184
|
+
end
|
175
185
|
end
|
176
186
|
end
|
177
187
|
|
@@ -241,6 +251,29 @@ module Karafka
|
|
241
251
|
|
242
252
|
::Rdkafka::Config.new(config_hash)
|
243
253
|
end
|
254
|
+
|
255
|
+
# Resolves the offset if offset is in a time format. Otherwise returns the offset without
|
256
|
+
# resolving.
|
257
|
+
# @param consumer [::Rdkafka::Consumer]
|
258
|
+
# @param name [String, Symbol] expected topic name
|
259
|
+
# @param partition [Integer]
|
260
|
+
# @param offset [Integer, Time]
|
261
|
+
# @return [Integer] expected offset
|
262
|
+
def resolve_offset(consumer, name, partition, offset)
|
263
|
+
if offset.is_a?(Time)
|
264
|
+
tpl = ::Rdkafka::Consumer::TopicPartitionList.new
|
265
|
+
tpl.add_topic_and_partitions_with_offsets(
|
266
|
+
name, partition => offset
|
267
|
+
)
|
268
|
+
|
269
|
+
real_offsets = consumer.offsets_for_times(tpl, TPL_REQUEST_TIMEOUT)
|
270
|
+
detected_offset = real_offsets.to_h.dig(name, partition)
|
271
|
+
|
272
|
+
detected_offset&.offset || raise(Errors::InvalidTimeBasedOffsetError)
|
273
|
+
else
|
274
|
+
offset
|
275
|
+
end
|
276
|
+
end
|
244
277
|
end
|
245
278
|
end
|
246
279
|
end
|
@@ -70,6 +70,7 @@ module Karafka
|
|
70
70
|
#
|
71
71
|
# @return [Boolean] true if there was no exception, otherwise false.
|
72
72
|
#
|
73
|
+
# @private
|
73
74
|
# @note We keep the seek offset tracking, and use it to compensate for async offset flushing
|
74
75
|
# that may not yet kick in when error occurs. That way we pause always on the last processed
|
75
76
|
# message.
|
@@ -203,8 +204,15 @@ module Karafka
|
|
203
204
|
|
204
205
|
# Seeks in the context of current topic and partition
|
205
206
|
#
|
206
|
-
# @param offset [Integer] offset where we want to seek
|
207
|
-
|
207
|
+
# @param offset [Integer, Time] offset where we want to seek or time of the offset where we
|
208
|
+
# want to seek.
|
209
|
+
# @param manual_seek [Boolean] Flag to differentiate between user seek and system/strategy
|
210
|
+
# based seek. User seek operations should take precedence over system actions, hence we need
|
211
|
+
# to know who invoked it.
|
212
|
+
# @note Please note, that if you are seeking to a time offset, getting the offset is blocking
|
213
|
+
def seek(offset, manual_seek = true)
|
214
|
+
coordinator.manual_seek if manual_seek
|
215
|
+
|
208
216
|
client.seek(
|
209
217
|
Karafka::Messages::Seek.new(
|
210
218
|
topic.name,
|
@@ -215,10 +223,18 @@ module Karafka
|
|
215
223
|
end
|
216
224
|
|
217
225
|
# @return [Boolean] true if partition was revoked from the current consumer
|
218
|
-
# @note
|
219
|
-
#
|
226
|
+
# @note There are two "levels" on which we can know that partition was revoked. First one is
|
227
|
+
# when we loose the assignment involuntarily and second is when coordinator gets this info
|
228
|
+
# after we poll with the rebalance callbacks. The first check allows us to get this notion
|
229
|
+
# even before we poll but it gets reset when polling happens, hence we also need to switch
|
230
|
+
# the coordinator state after the revocation (but prior to running more jobs)
|
220
231
|
def revoked?
|
221
|
-
coordinator.revoked?
|
232
|
+
return true if coordinator.revoked?
|
233
|
+
return false unless client.assignment_lost?
|
234
|
+
|
235
|
+
coordinator.revoke
|
236
|
+
|
237
|
+
true
|
222
238
|
end
|
223
239
|
|
224
240
|
# @return [Boolean] are we retrying processing after an error. This can be used to provide a
|
@@ -20,11 +20,14 @@ module Karafka
|
|
20
20
|
# How many times should we retry polling in case of a failure
|
21
21
|
MAX_POLL_RETRIES = 20
|
22
22
|
|
23
|
+
# Max time for a TPL request. We increase it to compensate for remote clusters latency
|
24
|
+
TPL_REQUEST_TIMEOUT = 2_000
|
25
|
+
|
23
26
|
# We want to make sure we never close several clients in the same moment to prevent
|
24
27
|
# potential race conditions and other issues
|
25
28
|
SHUTDOWN_MUTEX = Mutex.new
|
26
29
|
|
27
|
-
private_constant :MAX_POLL_RETRIES, :SHUTDOWN_MUTEX
|
30
|
+
private_constant :MAX_POLL_RETRIES, :SHUTDOWN_MUTEX, :TPL_REQUEST_TIMEOUT
|
28
31
|
|
29
32
|
# Creates a new consumer instance.
|
30
33
|
#
|
@@ -35,15 +38,16 @@ module Karafka
|
|
35
38
|
@id = SecureRandom.hex(6)
|
36
39
|
# Name is set when we build consumer
|
37
40
|
@name = ''
|
38
|
-
@mutex = Mutex.new
|
39
41
|
@closed = false
|
40
42
|
@subscription_group = subscription_group
|
41
43
|
@buffer = RawMessagesBuffer.new
|
42
44
|
@rebalance_manager = RebalanceManager.new
|
43
45
|
@kafka = build_consumer
|
44
|
-
#
|
45
|
-
#
|
46
|
-
|
46
|
+
# There are few operations that can happen in parallel from the listener threads as well
|
47
|
+
# as from the workers. They are not fully thread-safe because they may be composed out of
|
48
|
+
# few calls to Kafka or out of few internal state changes. That is why we mutex them.
|
49
|
+
# It mostly revolves around pausing and resuming.
|
50
|
+
@mutex = Mutex.new
|
47
51
|
# We need to keep track of what we have paused for resuming
|
48
52
|
# In case we loose partition, we still need to resume it, otherwise it won't be fetched
|
49
53
|
# again if we get reassigned to it later on. We need to keep them as after revocation we
|
@@ -104,13 +108,15 @@ module Karafka
|
|
104
108
|
#
|
105
109
|
# @param message [Karafka::Messages::Message]
|
106
110
|
def store_offset(message)
|
107
|
-
|
108
|
-
|
109
|
-
|
111
|
+
internal_store_offset(message)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] true if our current assignment has been lost involuntarily.
|
115
|
+
def assignment_lost?
|
116
|
+
@kafka.assignment_lost?
|
110
117
|
end
|
111
118
|
|
112
119
|
# Commits the offset on a current consumer in a non-blocking or blocking way.
|
113
|
-
# Ignoring a case where there would not be an offset (for example when rebalance occurs).
|
114
120
|
#
|
115
121
|
# @param async [Boolean] should the commit happen async or sync (async by default)
|
116
122
|
# @return [Boolean] did committing was successful. It may be not, when we no longer own
|
@@ -118,13 +124,13 @@ module Karafka
|
|
118
124
|
#
|
119
125
|
# @note This will commit all the offsets for the whole consumer. In order to achieve
|
120
126
|
# granular control over where the offset should be for particular topic partitions, the
|
121
|
-
# store_offset should be used to only store new offset when we want
|
127
|
+
# store_offset should be used to only store new offset when we want them to be flushed
|
128
|
+
#
|
129
|
+
# @note This method for async may return `true` despite involuntary partition revocation as
|
130
|
+
# it does **not** resolve to `lost_assignment?`. It returns only the commit state operation
|
131
|
+
# result.
|
122
132
|
def commit_offsets(async: true)
|
123
|
-
@mutex.lock
|
124
|
-
|
125
133
|
internal_commit_offsets(async: async)
|
126
|
-
ensure
|
127
|
-
@mutex.unlock
|
128
134
|
end
|
129
135
|
|
130
136
|
# Commits offset in a synchronous way.
|
@@ -137,13 +143,11 @@ module Karafka
|
|
137
143
|
# Seek to a particular message. The next poll on the topic/partition will return the
|
138
144
|
# message at the given offset.
|
139
145
|
#
|
140
|
-
# @param message [Messages::Message, Messages::Seek] message to which we want to seek to
|
146
|
+
# @param message [Messages::Message, Messages::Seek] message to which we want to seek to.
|
147
|
+
# It can have the time based offset.
|
148
|
+
# @note Please note, that if you are seeking to a time offset, getting the offset is blocking
|
141
149
|
def seek(message)
|
142
|
-
@mutex.
|
143
|
-
|
144
|
-
@kafka.seek(message)
|
145
|
-
ensure
|
146
|
-
@mutex.unlock
|
150
|
+
@mutex.synchronize { internal_seek(message) }
|
147
151
|
end
|
148
152
|
|
149
153
|
# Pauses given partition and moves back to last successful offset processed.
|
@@ -154,37 +158,34 @@ module Karafka
|
|
154
158
|
# be reprocessed after getting back to processing)
|
155
159
|
# @note This will pause indefinitely and requires manual `#resume`
|
156
160
|
def pause(topic, partition, offset)
|
157
|
-
@mutex.
|
158
|
-
|
159
|
-
|
160
|
-
return if @closed
|
161
|
-
|
162
|
-
pause_msg = Messages::Seek.new(topic, partition, offset)
|
161
|
+
@mutex.synchronize do
|
162
|
+
# Do not pause if the client got closed, would not change anything
|
163
|
+
return if @closed
|
163
164
|
|
164
|
-
|
165
|
+
pause_msg = Messages::Seek.new(topic, partition, offset)
|
165
166
|
|
166
|
-
|
167
|
-
# not own anymore.
|
168
|
-
tpl = topic_partition_list(topic, partition)
|
167
|
+
internal_commit_offsets(async: true)
|
169
168
|
|
170
|
-
|
169
|
+
# Here we do not use our cached tpls because we should not try to pause something we do
|
170
|
+
# not own anymore.
|
171
|
+
tpl = topic_partition_list(topic, partition)
|
171
172
|
|
172
|
-
|
173
|
-
'client.pause',
|
174
|
-
caller: self,
|
175
|
-
subscription_group: @subscription_group,
|
176
|
-
topic: topic,
|
177
|
-
partition: partition,
|
178
|
-
offset: offset
|
179
|
-
)
|
173
|
+
return unless tpl
|
180
174
|
|
181
|
-
|
175
|
+
Karafka.monitor.instrument(
|
176
|
+
'client.pause',
|
177
|
+
caller: self,
|
178
|
+
subscription_group: @subscription_group,
|
179
|
+
topic: topic,
|
180
|
+
partition: partition,
|
181
|
+
offset: offset
|
182
|
+
)
|
182
183
|
|
183
|
-
|
184
|
+
@paused_tpls[topic][partition] = tpl
|
184
185
|
|
185
|
-
|
186
|
-
|
187
|
-
|
186
|
+
@kafka.pause(tpl)
|
187
|
+
internal_seek(pause_msg)
|
188
|
+
end
|
188
189
|
end
|
189
190
|
|
190
191
|
# Resumes processing of a give topic partition after it was paused.
|
@@ -192,33 +193,31 @@ module Karafka
|
|
192
193
|
# @param topic [String] topic name
|
193
194
|
# @param partition [Integer] partition
|
194
195
|
def resume(topic, partition)
|
195
|
-
@mutex.
|
196
|
-
|
197
|
-
return if @closed
|
196
|
+
@mutex.synchronize do
|
197
|
+
return if @closed
|
198
198
|
|
199
|
-
|
200
|
-
|
199
|
+
# We now commit offsets on rebalances, thus we can do it async just to make sure
|
200
|
+
internal_commit_offsets(async: true)
|
201
201
|
|
202
|
-
|
203
|
-
|
202
|
+
# If we were not able, let's try to reuse the one we have (if we have)
|
203
|
+
tpl = topic_partition_list(topic, partition) || @paused_tpls[topic][partition]
|
204
204
|
|
205
|
-
|
205
|
+
return unless tpl
|
206
206
|
|
207
|
-
|
208
|
-
|
209
|
-
|
207
|
+
# If we did not have it, it means we never paused this partition, thus no resume should
|
208
|
+
# happen in the first place
|
209
|
+
return unless @paused_tpls[topic].delete(partition)
|
210
210
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
211
|
+
Karafka.monitor.instrument(
|
212
|
+
'client.resume',
|
213
|
+
caller: self,
|
214
|
+
subscription_group: @subscription_group,
|
215
|
+
topic: topic,
|
216
|
+
partition: partition
|
217
|
+
)
|
218
218
|
|
219
|
-
|
220
|
-
|
221
|
-
@mutex.unlock
|
219
|
+
@kafka.resume(tpl)
|
220
|
+
end
|
222
221
|
end
|
223
222
|
|
224
223
|
# Gracefully stops topic consumption.
|
@@ -235,9 +234,10 @@ module Karafka
|
|
235
234
|
# @param [Karafka::Messages::Message] message that we want to mark as processed
|
236
235
|
# @return [Boolean] true if successful. False if we no longer own given partition
|
237
236
|
# @note This method won't trigger automatic offsets commits, rather relying on the offset
|
238
|
-
# check-pointing trigger that happens with each batch processed
|
237
|
+
# check-pointing trigger that happens with each batch processed. It will however check the
|
238
|
+
# `librdkafka` assignment ownership to increase accuracy for involuntary revocations.
|
239
239
|
def mark_as_consumed(message)
|
240
|
-
store_offset(message)
|
240
|
+
store_offset(message) && !assignment_lost?
|
241
241
|
end
|
242
242
|
|
243
243
|
# Marks a given message as consumed and commits the offsets in a blocking way.
|
@@ -254,12 +254,9 @@ module Karafka
|
|
254
254
|
def reset
|
255
255
|
close
|
256
256
|
|
257
|
-
@
|
258
|
-
|
259
|
-
|
260
|
-
@paused_tpls.clear
|
261
|
-
@kafka = build_consumer
|
262
|
-
end
|
257
|
+
@closed = false
|
258
|
+
@paused_tpls.clear
|
259
|
+
@kafka = build_consumer
|
263
260
|
end
|
264
261
|
|
265
262
|
# Runs a single poll ignoring all the potential errors
|
@@ -281,7 +278,6 @@ module Karafka
|
|
281
278
|
# @param message [Karafka::Messages::Message]
|
282
279
|
# @return [Boolean] true if we could store the offset (if we still own the partition)
|
283
280
|
def internal_store_offset(message)
|
284
|
-
@offsetting = true
|
285
281
|
@kafka.store_offset(message)
|
286
282
|
true
|
287
283
|
rescue Rdkafka::RdkafkaError => e
|
@@ -294,11 +290,11 @@ module Karafka
|
|
294
290
|
# Non thread-safe message committing method
|
295
291
|
# @param async [Boolean] should the commit happen async or sync (async by default)
|
296
292
|
# @return [Boolean] true if offset commit worked, false if we've lost the assignment
|
293
|
+
# @note We do **not** consider `no_offset` as any problem and we allow to commit offsets
|
294
|
+
# even when no stored, because with sync commit, it refreshes the ownership state of the
|
295
|
+
# consumer in a sync way.
|
297
296
|
def internal_commit_offsets(async: true)
|
298
|
-
return true unless @offsetting
|
299
|
-
|
300
297
|
@kafka.commit(nil, async)
|
301
|
-
@offsetting = false
|
302
298
|
|
303
299
|
true
|
304
300
|
rescue Rdkafka::RdkafkaError => e
|
@@ -317,28 +313,55 @@ module Karafka
|
|
317
313
|
raise e
|
318
314
|
end
|
319
315
|
|
316
|
+
# Non-mutexed seek that should be used only internally. Outside we expose `#seek` that is
|
317
|
+
# wrapped with a mutex.
|
318
|
+
#
|
319
|
+
# @param message [Messages::Message, Messages::Seek] message to which we want to seek to.
|
320
|
+
# It can have the time based offset.
|
321
|
+
def internal_seek(message)
|
322
|
+
# If the seek message offset is in a time format, we need to find the closest "real"
|
323
|
+
# offset matching before we seek
|
324
|
+
if message.offset.is_a?(Time)
|
325
|
+
tpl = ::Rdkafka::Consumer::TopicPartitionList.new
|
326
|
+
tpl.add_topic_and_partitions_with_offsets(
|
327
|
+
message.topic,
|
328
|
+
message.partition => message.offset
|
329
|
+
)
|
330
|
+
|
331
|
+
# Now we can overwrite the seek message offset with our resolved offset and we can
|
332
|
+
# then seek to the appropriate message
|
333
|
+
# We set the timeout to 2_000 to make sure that remote clusters handle this well
|
334
|
+
real_offsets = @kafka.offsets_for_times(tpl, TPL_REQUEST_TIMEOUT)
|
335
|
+
detected_partition = real_offsets.to_h.dig(message.topic, message.partition)
|
336
|
+
|
337
|
+
# There always needs to be an offset. In case we seek into the future, where there
|
338
|
+
# are no offsets yet, we get -1 which indicates the most recent offset
|
339
|
+
# We should always detect offset, whether it is 0, -1 or a corresponding
|
340
|
+
message.offset = detected_partition&.offset || raise(Errors::InvalidTimeBasedOffsetError)
|
341
|
+
end
|
342
|
+
|
343
|
+
@kafka.seek(message)
|
344
|
+
end
|
345
|
+
|
320
346
|
# Commits the stored offsets in a sync way and closes the consumer.
|
321
347
|
def close
|
322
348
|
# Allow only one client to be closed at the same time
|
323
349
|
SHUTDOWN_MUTEX.synchronize do
|
324
|
-
#
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
# @note We do not clear rebalance manager here as we may still have revocation info
|
340
|
-
# here that we want to consider valid prior to running another reconnection
|
341
|
-
end
|
350
|
+
# Once client is closed, we should not close it again
|
351
|
+
# This could only happen in case of a race-condition when forceful shutdown happens
|
352
|
+
# and triggers this from a different thread
|
353
|
+
return if @closed
|
354
|
+
|
355
|
+
@closed = true
|
356
|
+
|
357
|
+
# Remove callbacks runners that were registered
|
358
|
+
::Karafka::Core::Instrumentation.statistics_callbacks.delete(@subscription_group.id)
|
359
|
+
::Karafka::Core::Instrumentation.error_callbacks.delete(@subscription_group.id)
|
360
|
+
|
361
|
+
@kafka.close
|
362
|
+
@buffer.clear
|
363
|
+
# @note We do not clear rebalance manager here as we may still have revocation info
|
364
|
+
# here that we want to consider valid prior to running another reconnection
|
342
365
|
end
|
343
366
|
end
|
344
367
|
|
@@ -49,9 +49,8 @@ module Karafka
|
|
49
49
|
# Callback that kicks in inside of rdkafka, when new partitions are assigned.
|
50
50
|
#
|
51
51
|
# @private
|
52
|
-
# @param _ [Rdkafka::Consumer]
|
53
52
|
# @param partitions [Rdkafka::Consumer::TopicPartitionList]
|
54
|
-
def on_partitions_assigned(
|
53
|
+
def on_partitions_assigned(partitions)
|
55
54
|
@assigned_partitions = partitions.to_h.transform_values { |part| part.map(&:partition) }
|
56
55
|
@changed = true
|
57
56
|
end
|
@@ -59,9 +58,8 @@ module Karafka
|
|
59
58
|
# Callback that kicks in inside of rdkafka, when partitions are revoked.
|
60
59
|
#
|
61
60
|
# @private
|
62
|
-
# @param _ [Rdkafka::Consumer]
|
63
61
|
# @param partitions [Rdkafka::Consumer::TopicPartitionList]
|
64
|
-
def on_partitions_revoked(
|
62
|
+
def on_partitions_revoked(partitions)
|
65
63
|
@revoked_partitions = partitions.to_h.transform_values { |part| part.map(&:partition) }
|
66
64
|
@changed = true
|
67
65
|
end
|
data/lib/karafka/errors.rb
CHANGED
@@ -48,6 +48,9 @@ module Karafka
|
|
48
48
|
StrategyNotFoundError = Class.new(BaseError)
|
49
49
|
|
50
50
|
# This should never happen. Please open an issue if it does.
|
51
|
-
|
51
|
+
InvalidRealOffsetUsageError = Class.new(BaseError)
|
52
|
+
|
53
|
+
# This should never happen. Please open an issue if it does.
|
54
|
+
InvalidTimeBasedOffsetError = Class.new(BaseError)
|
52
55
|
end
|
53
56
|
end
|
@@ -12,9 +12,6 @@ module Karafka
|
|
12
12
|
# @param received_at [Time] moment when we've received the message
|
13
13
|
# @return [Karafka::Messages::Message] message object with payload and metadata
|
14
14
|
def call(kafka_message, topic, received_at)
|
15
|
-
# @see https://github.com/appsignal/rdkafka-ruby/issues/168
|
16
|
-
kafka_message.headers.transform_keys!(&:to_s)
|
17
|
-
|
18
15
|
metadata = Karafka::Messages::Metadata.new(
|
19
16
|
timestamp: kafka_message.timestamp,
|
20
17
|
headers: kafka_message.headers,
|
@@ -4,6 +4,9 @@ module Karafka
|
|
4
4
|
module Messages
|
5
5
|
# "Fake" message that we use as an abstraction layer when seeking back.
|
6
6
|
# This allows us to encapsulate a seek with a simple abstraction
|
7
|
+
#
|
8
|
+
# @note `#offset` can be either the offset value or the time of the offset
|
9
|
+
# (first equal or greater)
|
7
10
|
Seek = Struct.new(:topic, :partition, :offset)
|
8
11
|
end
|
9
12
|
end
|