karafka-testing 2.6.3 → 2.6.4
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
- data/.github/workflows/ci.yml +9 -9
- data/.github/workflows/push.yml +1 -1
- data/.github/workflows/verify-action-pins.yml +8 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +9 -1
- data/Gemfile +2 -0
- data/Gemfile.lock +8 -7
- data/lib/karafka/testing/minitest/helpers.rb +64 -19
- data/lib/karafka/testing/minitest/proxy.rb +11 -0
- data/lib/karafka/testing/rspec/helpers.rb +69 -9
- data/lib/karafka/testing/rspec/proxy.rb +11 -0
- data/lib/karafka/testing/spec_consumer_client.rb +36 -0
- data/lib/karafka/testing/spec_consumer_resume.rb +22 -0
- data/lib/karafka/testing/spec_producer_client.rb +28 -2
- data/lib/karafka/testing/version.rb +1 -1
- data/package-lock.json +3 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08d462de8e80468e64eef6f71a2c17e47a4a1ca597dd9e290feaf0ccfc5f49d6'
|
|
4
|
+
data.tar.gz: d7971f8a44ed5f442de78467dce1bbd243596c5bc2bd05f2046b7778b3dfa2c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b94d77ccaed5dbb9c8d3fe1742e01b622b7a81a5fa9a3666584240217ef8100c12f76d1043cee6e313506d1e082c31becc1a11af7f7bcfea1d7f9edcc6c44dd0
|
|
7
|
+
data.tar.gz: a8682074bb85e6f5c1be92d3207933395311882f9c6fbebf446ea558b53e57b7f12a2ff5c2fc3a6142ce3adf6b6903ef740a8dc304a13ae728f72d2f261c956a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
|
|
37
37
|
|
|
38
38
|
- name: Set up Ruby
|
|
39
|
-
uses: ruby/setup-ruby@
|
|
39
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
40
40
|
with:
|
|
41
41
|
ruby-version: ${{matrix.ruby}}
|
|
42
42
|
bundler: 'latest'
|
|
@@ -66,9 +66,9 @@ jobs:
|
|
|
66
66
|
with:
|
|
67
67
|
fetch-depth: 0
|
|
68
68
|
- name: Set up Ruby
|
|
69
|
-
uses: ruby/setup-ruby@
|
|
69
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
70
70
|
with:
|
|
71
|
-
ruby-version: '4.0.
|
|
71
|
+
ruby-version: '4.0.9'
|
|
72
72
|
bundler-cache: true
|
|
73
73
|
- name: Run rubocop
|
|
74
74
|
run: bundle exec rubocop
|
|
@@ -81,9 +81,9 @@ jobs:
|
|
|
81
81
|
steps:
|
|
82
82
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
83
83
|
- name: Set up Ruby
|
|
84
|
-
uses: ruby/setup-ruby@
|
|
84
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
85
85
|
with:
|
|
86
|
-
ruby-version: '4.0.
|
|
86
|
+
ruby-version: '4.0.9'
|
|
87
87
|
bundler-cache: true
|
|
88
88
|
- run: bundle exec yard-lint lib/
|
|
89
89
|
|
|
@@ -95,9 +95,9 @@ jobs:
|
|
|
95
95
|
with:
|
|
96
96
|
fetch-depth: 0
|
|
97
97
|
- name: Set up Ruby
|
|
98
|
-
uses: ruby/setup-ruby@
|
|
98
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
99
99
|
with:
|
|
100
|
-
ruby-version: '4.0.
|
|
100
|
+
ruby-version: '4.0.9'
|
|
101
101
|
bundler: 'latest'
|
|
102
102
|
- name: Install dependencies
|
|
103
103
|
run: bundle install --jobs 4 --retry 3
|
|
@@ -112,9 +112,9 @@ jobs:
|
|
|
112
112
|
with:
|
|
113
113
|
fetch-depth: 0
|
|
114
114
|
- name: Set up Ruby
|
|
115
|
-
uses: ruby/setup-ruby@
|
|
115
|
+
uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
|
|
116
116
|
with:
|
|
117
|
-
ruby-version: '4.0.
|
|
117
|
+
ruby-version: '4.0.9'
|
|
118
118
|
bundler: 'latest'
|
|
119
119
|
- name: Install dependencies
|
|
120
120
|
run: bundle install --jobs 4 --retry 3
|
data/.github/workflows/push.yml
CHANGED
|
@@ -10,7 +10,14 @@ jobs:
|
|
|
10
10
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
11
11
|
- name: Check SHA pins
|
|
12
12
|
run: |
|
|
13
|
-
|
|
13
|
+
# Every remote action must be pinned to a full 40-character commit SHA. Local actions
|
|
14
|
+
# (./) and docker:// images are not checked.
|
|
15
|
+
unpinned=$(grep -E -rn --include="*.yml" --include="*.yaml" \
|
|
16
|
+
"^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*['\"]?[^.'\"[:space:]]" .github/workflows/ \
|
|
17
|
+
| grep -Ev "uses:[[:space:]]*['\"]?docker://" \
|
|
18
|
+
| grep -Ev "@[0-9a-f]{40}['\"]?([[:space:]]|$)" || true)
|
|
19
|
+
if [ -n "$unpinned" ]; then
|
|
20
|
+
echo "$unpinned"
|
|
14
21
|
echo "::error::Actions should use SHA pins, not tags or branch names"
|
|
15
22
|
exit 1
|
|
16
23
|
fi
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Karafka Testing Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.4 (2026-11-04)
|
|
4
|
+
- [Feature] Record consumer pauses and resumes. Assert them with `karafka.pauses` and `karafka.resumes` (#214).
|
|
5
|
+
- [Maintenance] Declare `rake` in the `Gemfile`. It came only through `karafka-rdkafka`, which will stop depending on it for precompiled platform gems.
|
|
6
|
+
- [Fix] Give each consumer in one test a batch of only its own messages. Several consumers no longer share one batch, and offsets count per consumer.
|
|
7
|
+
- [Fix] Stub `pause` and `resume` on the spec consumer client. Consumers that call `#pause` no longer fail with `NoMethodError`.
|
|
8
|
+
- [Fix] Make `Minitest::Spec.include(Karafka::Testing::Minitest::Helpers)` work. Specs no longer fail with `ArgumentError: wrong number of arguments`.
|
|
9
|
+
- [Fix] Make producing in a test safe from many threads. On JRuby, messages produced from 2+ threads at once were lost or got duplicate offsets.
|
|
10
|
+
|
|
3
11
|
## 2.6.3 (2026-10-25)
|
|
4
12
|
- **[EOL]** Drop Ruby 3.2 support; require Ruby 3.3+.
|
|
5
13
|
- [Maintenance] Align internal naming and YARD docs with the Karafka ecosystem conventions.
|
|
@@ -26,8 +34,8 @@
|
|
|
26
34
|
- [Fix] Fix consumer mapping collision when multiple consumer groups listen to the same topic.
|
|
27
35
|
|
|
28
36
|
## 2.5.4 (2025-10-03)
|
|
29
|
-
- [Fix] Topic present in second dynamically built subscription group not available for dispatch (#291).
|
|
30
37
|
- **[EOL]** Remove Ruby `3.1` according to the EOL schedule.
|
|
38
|
+
- [Fix] Topic present in second dynamically built subscription group not available for dispatch (#291).
|
|
31
39
|
|
|
32
40
|
## 2.5.3 (2025-08-12)
|
|
33
41
|
- [Fix] Allow testing consumers with Pro expansions (i.e. Piping).
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
karafka-testing (2.6.
|
|
4
|
+
karafka-testing (2.6.4)
|
|
5
5
|
karafka (>= 2.6.0.rc2, < 2.7.0)
|
|
6
6
|
waterdrop (>= 2.10.0)
|
|
7
7
|
|
|
@@ -12,22 +12,22 @@ GEM
|
|
|
12
12
|
drb (2.2.3)
|
|
13
13
|
ffi (1.17.4)
|
|
14
14
|
ffi (1.17.4-x86_64-linux-gnu)
|
|
15
|
-
json (3.0
|
|
15
|
+
json (3.1.0)
|
|
16
16
|
karafka (2.6.1)
|
|
17
17
|
karafka-core (>= 2.6.2, < 2.7.0)
|
|
18
18
|
karafka-rdkafka (>= 0.28.0)
|
|
19
19
|
waterdrop (>= 2.10.2, < 3.0.0)
|
|
20
20
|
zeitwerk (~> 2.3)
|
|
21
|
-
karafka-core (2.6.
|
|
21
|
+
karafka-core (2.6.6)
|
|
22
22
|
karafka-rdkafka (>= 0.20.0)
|
|
23
23
|
logger (>= 1.6.0)
|
|
24
|
-
karafka-rdkafka (0.
|
|
24
|
+
karafka-rdkafka (0.31.0)
|
|
25
25
|
ffi (~> 1.17.1)
|
|
26
26
|
json (> 2.0)
|
|
27
27
|
logger
|
|
28
28
|
mini_portile2 (~> 2.6)
|
|
29
29
|
rake (> 12)
|
|
30
|
-
karafka-rdkafka (0.
|
|
30
|
+
karafka-rdkafka (0.31.0-x86_64-linux-gnu)
|
|
31
31
|
ffi (~> 1.17.1)
|
|
32
32
|
json (> 2.0)
|
|
33
33
|
logger
|
|
@@ -58,9 +58,9 @@ GEM
|
|
|
58
58
|
ruby2_keywords (0.0.5)
|
|
59
59
|
simplecov (1.3.2)
|
|
60
60
|
warning (1.6.0)
|
|
61
|
-
waterdrop (2.10.
|
|
61
|
+
waterdrop (2.10.9)
|
|
62
62
|
karafka-core (>= 2.5.12, < 3.0.0)
|
|
63
|
-
karafka-rdkafka (>= 0.
|
|
63
|
+
karafka-rdkafka (>= 0.31.0)
|
|
64
64
|
zeitwerk (~> 2.3)
|
|
65
65
|
zeitwerk (2.8.3)
|
|
66
66
|
|
|
@@ -72,6 +72,7 @@ DEPENDENCIES
|
|
|
72
72
|
karafka-testing!
|
|
73
73
|
minitest
|
|
74
74
|
mocha
|
|
75
|
+
rake
|
|
75
76
|
rspec
|
|
76
77
|
simplecov
|
|
77
78
|
warning
|
|
@@ -4,6 +4,7 @@ require "karafka/testing"
|
|
|
4
4
|
require "karafka/testing/errors"
|
|
5
5
|
require "karafka/testing/helpers"
|
|
6
6
|
require "karafka/testing/spec_consumer_client"
|
|
7
|
+
require "karafka/testing/spec_consumer_resume"
|
|
7
8
|
require "karafka/testing/spec_producer_client"
|
|
8
9
|
require "karafka/testing/minitest/proxy"
|
|
9
10
|
|
|
@@ -39,18 +40,13 @@ module Karafka
|
|
|
39
40
|
|
|
40
41
|
@_karafka_consumer_mappings = {}
|
|
41
42
|
|
|
42
|
-
# Mocha
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
producer_client = @_karafka_producer_client
|
|
48
|
-
Karafka.producer.define_singleton_method(:client) { producer_client }
|
|
49
|
-
@_karafka_producer_client_stubbed = true
|
|
50
|
-
end
|
|
43
|
+
# Not a Mocha stub even when Mocha is loaded: Mocha records every call in an
|
|
44
|
+
# unsynchronized array, which breaks when code under test produces from many threads
|
|
45
|
+
producer_client = @_karafka_producer_client
|
|
46
|
+
Karafka.producer.define_singleton_method(:client) { producer_client }
|
|
47
|
+
@_karafka_producer_client_stubbed = true
|
|
51
48
|
end
|
|
52
49
|
|
|
53
|
-
# Mocha removes its own stubs, so we only remove the one we defined
|
|
54
50
|
teardown_flow = lambda do
|
|
55
51
|
next unless @_karafka_producer_client_stubbed
|
|
56
52
|
|
|
@@ -59,8 +55,9 @@ module Karafka
|
|
|
59
55
|
end
|
|
60
56
|
|
|
61
57
|
if base.to_s == "Minitest::Spec"
|
|
58
|
+
# `before` passes the test to its block, which the lambda does not take
|
|
62
59
|
base.class_eval do
|
|
63
|
-
before(&eval_flow)
|
|
60
|
+
before { instance_exec(&eval_flow) }
|
|
64
61
|
end
|
|
65
62
|
elsif base.respond_to?(:setup) && base.method(:setup).arity != 0
|
|
66
63
|
base.class_eval do
|
|
@@ -149,8 +146,10 @@ module Karafka
|
|
|
149
146
|
return if message[:consumer_group] &&
|
|
150
147
|
message[:consumer_group].to_s != consumer_obj.topic.consumer_group.name
|
|
151
148
|
|
|
149
|
+
batch = _karafka_consumer_batch(consumer_obj)
|
|
150
|
+
|
|
152
151
|
# Build message metadata and copy any metadata that would come from the message
|
|
153
|
-
metadata = _karafka_message_metadata_defaults(consumer_obj)
|
|
152
|
+
metadata = _karafka_message_metadata_defaults(consumer_obj, batch)
|
|
154
153
|
|
|
155
154
|
metadata.keys.each do |key|
|
|
156
155
|
message_key = METADATA_DISPATCH_MAPPINGS.fetch(key, key)
|
|
@@ -171,6 +170,7 @@ module Karafka
|
|
|
171
170
|
|
|
172
171
|
# Add this message to previously produced messages
|
|
173
172
|
@_karafka_consumer_messages << karafka_message
|
|
173
|
+
batch << karafka_message unless batch.equal?(@_karafka_consumer_messages)
|
|
174
174
|
# A real batch never mixes partitions, so the first message of this consumer decides
|
|
175
175
|
# the partition
|
|
176
176
|
partition = (@_karafka_batch_partitions ||= {})[consumer_obj] ||= karafka_message.partition
|
|
@@ -184,7 +184,7 @@ module Karafka
|
|
|
184
184
|
|
|
185
185
|
# Update batch metadata
|
|
186
186
|
batch_metadata = Karafka::Messages::Builders::BatchMetadata.call(
|
|
187
|
-
|
|
187
|
+
batch,
|
|
188
188
|
consumer_obj.topic,
|
|
189
189
|
partition,
|
|
190
190
|
Time.now
|
|
@@ -192,7 +192,7 @@ module Karafka
|
|
|
192
192
|
|
|
193
193
|
# Update consumer messages batch
|
|
194
194
|
consumer_obj.messages = Karafka::Messages::Messages.new(
|
|
195
|
-
|
|
195
|
+
batch,
|
|
196
196
|
batch_metadata
|
|
197
197
|
)
|
|
198
198
|
end
|
|
@@ -204,8 +204,9 @@ module Karafka
|
|
|
204
204
|
# offsets auto-increment from 0).
|
|
205
205
|
def _karafka_produce(payload, metadata = {})
|
|
206
206
|
# Extract offset before passing to WaterDrop since it is not a valid
|
|
207
|
-
# WaterDrop message attribute (Kafka assigns offsets, not producers)
|
|
208
|
-
|
|
207
|
+
# WaterDrop message attribute (Kafka assigns offsets, not producers). Kept per thread, as
|
|
208
|
+
# code under test may produce from many threads at once
|
|
209
|
+
Thread.current[:_karafka_next_offset] = metadata.delete(:offset)
|
|
209
210
|
|
|
210
211
|
topic_name = if metadata[:topic]
|
|
211
212
|
metadata[:topic]
|
|
@@ -222,7 +223,7 @@ module Karafka
|
|
|
222
223
|
}.merge(metadata)
|
|
223
224
|
)
|
|
224
225
|
ensure
|
|
225
|
-
|
|
226
|
+
Thread.current[:_karafka_next_offset] = nil
|
|
226
227
|
end
|
|
227
228
|
|
|
228
229
|
# @return [Array<Hash>] messages that were produced
|
|
@@ -236,6 +237,16 @@ module Karafka
|
|
|
236
237
|
@_karafka_consumer_messages
|
|
237
238
|
end
|
|
238
239
|
|
|
240
|
+
# @return [Array<Hash>] pauses requested by the consumers
|
|
241
|
+
def _karafka_pauses
|
|
242
|
+
@_karafka_consumer_client.pauses
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# @return [Array<Hash>] resumes of paused consumers
|
|
246
|
+
def _karafka_resumes
|
|
247
|
+
@_karafka_consumer_client.resumes
|
|
248
|
+
end
|
|
249
|
+
|
|
239
250
|
# Produces message to a specific consumer instance
|
|
240
251
|
# Use when testing multiple consumers for the same topic
|
|
241
252
|
#
|
|
@@ -260,6 +271,38 @@ module Karafka
|
|
|
260
271
|
|
|
261
272
|
private
|
|
262
273
|
|
|
274
|
+
# Messages of the batch of a given consumer. The first consumer that gets a message uses
|
|
275
|
+
# the shared buffer, so altering `consumer_messages` alters its batch. Once a second
|
|
276
|
+
# consumer gets one, each consumer gets its own batch, as a real batch holds one topic.
|
|
277
|
+
#
|
|
278
|
+
# @param consumer_obj [Karafka::BaseConsumer] consumer reference
|
|
279
|
+
# @return [Array<Karafka::Messages::Message>] messages of this consumer's batch
|
|
280
|
+
def _karafka_consumer_batch(consumer_obj)
|
|
281
|
+
batches = (@_karafka_consumer_batches ||= {})
|
|
282
|
+
|
|
283
|
+
batches.fetch(consumer_obj) do
|
|
284
|
+
if batches.empty?
|
|
285
|
+
batches[consumer_obj] = @_karafka_consumer_messages
|
|
286
|
+
else
|
|
287
|
+
first_consumer, first_batch = batches.first
|
|
288
|
+
|
|
289
|
+
if first_batch.equal?(@_karafka_consumer_messages)
|
|
290
|
+
own_batch = first_batch.dup
|
|
291
|
+
batches[first_consumer] = own_batch
|
|
292
|
+
|
|
293
|
+
if first_consumer.messages
|
|
294
|
+
first_consumer.messages = Karafka::Messages::Messages.new(
|
|
295
|
+
own_batch,
|
|
296
|
+
first_consumer.messages.metadata
|
|
297
|
+
)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
batches[consumer_obj] = []
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
263
306
|
# Finds a consumer for the given message with backward-compatible fallback
|
|
264
307
|
# @param message [Hash] the message being routed
|
|
265
308
|
# @return [Object, nil] the consumer instance or nil
|
|
@@ -283,14 +326,15 @@ module Karafka
|
|
|
283
326
|
end
|
|
284
327
|
|
|
285
328
|
# @param consumer_obj [Karafka::BaseConsumer] consumer reference
|
|
329
|
+
# @param batch [Array<Karafka::Messages::Message>] messages already given to this consumer
|
|
286
330
|
# @return [Hash] message default options
|
|
287
|
-
def _karafka_message_metadata_defaults(consumer_obj)
|
|
331
|
+
def _karafka_message_metadata_defaults(consumer_obj, batch)
|
|
288
332
|
{
|
|
289
333
|
deserializers: consumer_obj.topic.deserializers,
|
|
290
334
|
timestamp: Time.now,
|
|
291
335
|
raw_headers: {},
|
|
292
336
|
raw_key: nil,
|
|
293
|
-
offset:
|
|
337
|
+
offset: Thread.current[:_karafka_next_offset] || batch.size,
|
|
294
338
|
partition: 0,
|
|
295
339
|
received_at: Time.now,
|
|
296
340
|
topic: consumer_obj.topic.name
|
|
@@ -314,6 +358,7 @@ module Karafka
|
|
|
314
358
|
@consumer.instance_variable_set(:@used, true)
|
|
315
359
|
expansions = processing_cfg.consumer_groups.expansions_selector.find(topic)
|
|
316
360
|
expansions.each { |expansion| @consumer.singleton_class.include(expansion) }
|
|
361
|
+
@consumer.singleton_class.prepend(Testing::SpecConsumerResume)
|
|
317
362
|
@_karafka_consumer_mappings[topic.id] = @consumer
|
|
318
363
|
@consumer
|
|
319
364
|
end
|
|
@@ -39,6 +39,17 @@ module Karafka
|
|
|
39
39
|
def consumer_messages
|
|
40
40
|
@minitest_example._karafka_consumer_messages
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# @return [Array<Hash>] pauses requested by the consumers, each with the `topic`,
|
|
44
|
+
# `partition`, `offset` and `timeout`
|
|
45
|
+
def pauses
|
|
46
|
+
@minitest_example._karafka_pauses
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Array<Hash>] resumes of paused consumers, each with the `topic` and `partition`
|
|
50
|
+
def resumes
|
|
51
|
+
@minitest_example._karafka_resumes
|
|
52
|
+
end
|
|
42
53
|
end
|
|
43
54
|
end
|
|
44
55
|
end
|
|
@@ -5,6 +5,7 @@ require "karafka/testing"
|
|
|
5
5
|
require "karafka/testing/errors"
|
|
6
6
|
require "karafka/testing/helpers"
|
|
7
7
|
require "karafka/testing/spec_consumer_client"
|
|
8
|
+
require "karafka/testing/spec_consumer_resume"
|
|
8
9
|
require "karafka/testing/spec_producer_client"
|
|
9
10
|
require "karafka/testing/rspec/proxy"
|
|
10
11
|
|
|
@@ -55,12 +56,23 @@ module Karafka
|
|
|
55
56
|
# that patches are available because some users have Mocha as part of their
|
|
56
57
|
# supply chain, but do not use it when running Karafka specs. In such cases, without
|
|
57
58
|
# such check `karafka-testing` would falsely assume, that Mocha is in use.
|
|
59
|
+
# With Mocha we define the method instead of stubbing it: Mocha records every call in
|
|
60
|
+
# an unsynchronized array, which breaks when code under test produces from many threads
|
|
58
61
|
if Object.const_defined?("Mocha", false) && Karafka.producer.respond_to?(:stubs)
|
|
59
|
-
|
|
62
|
+
producer_client = _karafka_producer_client
|
|
63
|
+
Karafka.producer.define_singleton_method(:client) { producer_client }
|
|
64
|
+
@_karafka_producer_client_stubbed = true
|
|
60
65
|
else
|
|
61
66
|
allow(Karafka.producer).to receive(:client).and_return(_karafka_producer_client)
|
|
62
67
|
end
|
|
63
68
|
end
|
|
69
|
+
|
|
70
|
+
base.after do
|
|
71
|
+
next unless @_karafka_producer_client_stubbed
|
|
72
|
+
|
|
73
|
+
Karafka.producer.singleton_class.remove_method(:client)
|
|
74
|
+
@_karafka_producer_client_stubbed = false
|
|
75
|
+
end
|
|
64
76
|
end
|
|
65
77
|
end
|
|
66
78
|
|
|
@@ -128,8 +140,10 @@ module Karafka
|
|
|
128
140
|
return if message[:consumer_group] &&
|
|
129
141
|
message[:consumer_group].to_s != consumer_obj.topic.consumer_group.name
|
|
130
142
|
|
|
143
|
+
batch = _karafka_consumer_batch(consumer_obj)
|
|
144
|
+
|
|
131
145
|
# Build message metadata and copy any metadata that would come from the message
|
|
132
|
-
metadata = _karafka_message_metadata_defaults(consumer_obj)
|
|
146
|
+
metadata = _karafka_message_metadata_defaults(consumer_obj, batch)
|
|
133
147
|
|
|
134
148
|
metadata.keys.each do |key|
|
|
135
149
|
message_key = METADATA_DISPATCH_MAPPINGS.fetch(key, key)
|
|
@@ -150,6 +164,7 @@ module Karafka
|
|
|
150
164
|
|
|
151
165
|
# Add this message to previously produced messages
|
|
152
166
|
_karafka_consumer_messages << karafka_message
|
|
167
|
+
batch << karafka_message unless batch.equal?(_karafka_consumer_messages)
|
|
153
168
|
|
|
154
169
|
# A real batch never mixes partitions, so the first message of this consumer decides
|
|
155
170
|
# the partition
|
|
@@ -164,7 +179,7 @@ module Karafka
|
|
|
164
179
|
|
|
165
180
|
# Update batch metadata
|
|
166
181
|
batch_metadata = Karafka::Messages::Builders::BatchMetadata.call(
|
|
167
|
-
|
|
182
|
+
batch,
|
|
168
183
|
consumer_obj.topic,
|
|
169
184
|
partition,
|
|
170
185
|
Time.now
|
|
@@ -172,7 +187,7 @@ module Karafka
|
|
|
172
187
|
|
|
173
188
|
# Update consumer messages batch
|
|
174
189
|
consumer_obj.messages = Karafka::Messages::Messages.new(
|
|
175
|
-
|
|
190
|
+
batch,
|
|
176
191
|
batch_metadata
|
|
177
192
|
)
|
|
178
193
|
end
|
|
@@ -184,8 +199,9 @@ module Karafka
|
|
|
184
199
|
# offsets auto-increment from 0).
|
|
185
200
|
def _karafka_produce(payload, metadata = {})
|
|
186
201
|
# Extract offset before passing to WaterDrop since it is not a valid
|
|
187
|
-
# WaterDrop message attribute (Kafka assigns offsets, not producers)
|
|
188
|
-
|
|
202
|
+
# WaterDrop message attribute (Kafka assigns offsets, not producers). Kept per thread, as
|
|
203
|
+
# code under test may produce from many threads at once
|
|
204
|
+
Thread.current[:_karafka_next_offset] = metadata.delete(:offset)
|
|
189
205
|
|
|
190
206
|
topic_name = if metadata[:topic]
|
|
191
207
|
metadata[:topic]
|
|
@@ -204,7 +220,7 @@ module Karafka
|
|
|
204
220
|
}.merge(metadata)
|
|
205
221
|
)
|
|
206
222
|
ensure
|
|
207
|
-
|
|
223
|
+
Thread.current[:_karafka_next_offset] = nil
|
|
208
224
|
end
|
|
209
225
|
|
|
210
226
|
# @return [Array<Hash>] messages that were produced
|
|
@@ -212,6 +228,16 @@ module Karafka
|
|
|
212
228
|
_karafka_producer_client.messages
|
|
213
229
|
end
|
|
214
230
|
|
|
231
|
+
# @return [Array<Hash>] pauses requested by the consumers
|
|
232
|
+
def _karafka_pauses
|
|
233
|
+
_karafka_consumer_client.pauses
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @return [Array<Hash>] resumes of paused consumers
|
|
237
|
+
def _karafka_resumes
|
|
238
|
+
_karafka_consumer_client.resumes
|
|
239
|
+
end
|
|
240
|
+
|
|
215
241
|
# Produces message to a specific consumer instance
|
|
216
242
|
# Use when testing multiple consumers for the same topic
|
|
217
243
|
#
|
|
@@ -239,6 +265,38 @@ module Karafka
|
|
|
239
265
|
|
|
240
266
|
private
|
|
241
267
|
|
|
268
|
+
# Messages of the batch of a given consumer. The first consumer that gets a message uses
|
|
269
|
+
# the shared buffer, so altering `consumer_messages` alters its batch. Once a second
|
|
270
|
+
# consumer gets one, each consumer gets its own batch, as a real batch holds one topic.
|
|
271
|
+
#
|
|
272
|
+
# @param consumer_obj [Karafka::BaseConsumer] consumer reference
|
|
273
|
+
# @return [Array<Karafka::Messages::Message>] messages of this consumer's batch
|
|
274
|
+
def _karafka_consumer_batch(consumer_obj)
|
|
275
|
+
batches = (@_karafka_consumer_batches ||= {})
|
|
276
|
+
|
|
277
|
+
batches.fetch(consumer_obj) do
|
|
278
|
+
if batches.empty?
|
|
279
|
+
batches[consumer_obj] = _karafka_consumer_messages
|
|
280
|
+
else
|
|
281
|
+
first_consumer, first_batch = batches.first
|
|
282
|
+
|
|
283
|
+
if first_batch.equal?(_karafka_consumer_messages)
|
|
284
|
+
own_batch = first_batch.dup
|
|
285
|
+
batches[first_consumer] = own_batch
|
|
286
|
+
|
|
287
|
+
if first_consumer.messages
|
|
288
|
+
first_consumer.messages = Karafka::Messages::Messages.new(
|
|
289
|
+
own_batch,
|
|
290
|
+
first_consumer.messages.metadata
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
batches[consumer_obj] = []
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
242
300
|
# Checks if the RSpec described_class is a Karafka consumer class.
|
|
243
301
|
# When true, the implicit RSpec subject can be used as a consumer fallback.
|
|
244
302
|
#
|
|
@@ -288,14 +346,15 @@ module Karafka
|
|
|
288
346
|
end
|
|
289
347
|
|
|
290
348
|
# @param consumer_obj [Karafka::BaseConsumer] consumer reference
|
|
349
|
+
# @param batch [Array<Karafka::Messages::Message>] messages already given to this consumer
|
|
291
350
|
# @return [Hash] message default options
|
|
292
|
-
def _karafka_message_metadata_defaults(consumer_obj)
|
|
351
|
+
def _karafka_message_metadata_defaults(consumer_obj, batch)
|
|
293
352
|
{
|
|
294
353
|
deserializers: consumer_obj.topic.deserializers,
|
|
295
354
|
timestamp: Time.now,
|
|
296
355
|
raw_headers: {},
|
|
297
356
|
raw_key: nil,
|
|
298
|
-
offset:
|
|
357
|
+
offset: Thread.current[:_karafka_next_offset] || batch.size,
|
|
299
358
|
partition: 0,
|
|
300
359
|
received_at: Time.now,
|
|
301
360
|
topic: consumer_obj.topic.name
|
|
@@ -319,6 +378,7 @@ module Karafka
|
|
|
319
378
|
consumer.instance_variable_set(:@used, true)
|
|
320
379
|
expansions = processing_cfg.consumer_groups.expansions_selector.find(topic)
|
|
321
380
|
expansions.each { |expansion| consumer.singleton_class.include(expansion) }
|
|
381
|
+
consumer.singleton_class.prepend(Testing::SpecConsumerResume)
|
|
322
382
|
|
|
323
383
|
@_karafka_consumer_mappings[topic.id] = consumer
|
|
324
384
|
consumer
|
|
@@ -39,6 +39,17 @@ module Karafka
|
|
|
39
39
|
def consumer_messages
|
|
40
40
|
@rspec_example._karafka_consumer_messages
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# @return [Array<Hash>] pauses requested by the consumers, each with the `topic`,
|
|
44
|
+
# `partition`, `offset` and `timeout`
|
|
45
|
+
def pauses
|
|
46
|
+
@rspec_example._karafka_pauses
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Array<Hash>] resumes of paused consumers, each with the `topic` and `partition`
|
|
50
|
+
def resumes
|
|
51
|
+
@rspec_example._karafka_resumes
|
|
52
|
+
end
|
|
42
53
|
end
|
|
43
54
|
end
|
|
44
55
|
end
|
|
@@ -6,6 +6,20 @@ module Karafka
|
|
|
6
6
|
# For specs we do not mark anything as consumed, nor do we really send heartbeats.
|
|
7
7
|
# Those things are tested in the framework itself
|
|
8
8
|
class SpecConsumerClient
|
|
9
|
+
# @return [Array<Hash>] pauses requested on this client, each with the `topic`,
|
|
10
|
+
# `partition`, `offset` and `timeout`
|
|
11
|
+
attr_reader :pauses
|
|
12
|
+
|
|
13
|
+
# @return [Array<Hash>] resumes requested on this client, each with the `topic` and
|
|
14
|
+
# `partition`
|
|
15
|
+
attr_reader :resumes
|
|
16
|
+
|
|
17
|
+
# Builds a client with no pauses or resumes recorded
|
|
18
|
+
def initialize
|
|
19
|
+
@pauses = []
|
|
20
|
+
@resumes = []
|
|
21
|
+
end
|
|
22
|
+
|
|
9
23
|
%i[
|
|
10
24
|
mark_as_consumed
|
|
11
25
|
mark_as_consumed!
|
|
@@ -17,6 +31,28 @@ module Karafka
|
|
|
17
31
|
define_method(caught_delegator) { |*| true }
|
|
18
32
|
end
|
|
19
33
|
|
|
34
|
+
# Records the pause instead of pausing the partition
|
|
35
|
+
#
|
|
36
|
+
# @param topic [String] topic name
|
|
37
|
+
# @param partition [Integer] partition
|
|
38
|
+
# @param offset [Integer, nil] offset to continue from or nil to keep the current position
|
|
39
|
+
# @param timeout [Integer] pause time in milliseconds
|
|
40
|
+
# @return [true]
|
|
41
|
+
def pause(topic, partition, offset = nil, timeout = 0)
|
|
42
|
+
@pauses << { topic: topic, partition: partition, offset: offset, timeout: timeout }
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Records the resume instead of resuming the partition
|
|
47
|
+
#
|
|
48
|
+
# @param topic [String] topic name
|
|
49
|
+
# @param partition [Integer] partition
|
|
50
|
+
# @return [true]
|
|
51
|
+
def resume(topic, partition)
|
|
52
|
+
@resumes << { topic: topic, partition: partition }
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
20
56
|
# @return [Boolean] assignments are never lost for specs
|
|
21
57
|
def assignment_lost?
|
|
22
58
|
false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Karafka
|
|
4
|
+
module Testing
|
|
5
|
+
# Resumes the partition on the client when a paused consumer resumes. In a real run the
|
|
6
|
+
# listener does it before the next poll, which never happens in specs.
|
|
7
|
+
module SpecConsumerResume
|
|
8
|
+
# Expires the pause and, if it was paused, resumes the partition as the listener does
|
|
9
|
+
def resume
|
|
10
|
+
pause_tracker = coordinator.pause_tracker
|
|
11
|
+
paused = pause_tracker.paused?
|
|
12
|
+
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
return unless paused
|
|
16
|
+
|
|
17
|
+
pause_tracker.resume
|
|
18
|
+
client.resume(topic.name, partition)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -3,21 +3,47 @@
|
|
|
3
3
|
module Karafka
|
|
4
4
|
module Testing
|
|
5
5
|
# Spec producer client used to buffer messages that we send out in specs
|
|
6
|
+
#
|
|
7
|
+
# @note Code under test may produce from many threads at once, so every operation that touches
|
|
8
|
+
# the buffers runs under one lock
|
|
6
9
|
class SpecProducerClient < ::WaterDrop::Clients::Buffered
|
|
7
10
|
# @param example [RSpec::Core::ExampleGroup, Minitest::Test] test example we need to hold to
|
|
8
11
|
# trigger actions on it that are test context aware
|
|
9
12
|
def initialize(example)
|
|
10
13
|
super(nil)
|
|
11
14
|
@example = example
|
|
15
|
+
@mutex = Mutex.new
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
# "Produces" message to Kafka. That is, it acknowledges it locally, adds it to the internal
|
|
15
19
|
# buffer and adds it (if needed) into the current consumer messages buffer
|
|
16
20
|
# @param message [Hash] `Karafka.producer.produce_sync` message hash
|
|
17
21
|
def produce(message)
|
|
18
|
-
@
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
@example._karafka_add_message_to_consumer_if_needed(message)
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Starts the transaction on a given level
|
|
30
|
+
def begin_transaction
|
|
31
|
+
@mutex.synchronize { super }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Finishes given level of transaction
|
|
35
|
+
def commit_transaction
|
|
36
|
+
@mutex.synchronize { super }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Aborts the transaction
|
|
40
|
+
def abort_transaction
|
|
41
|
+
@mutex.synchronize { super }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Clears internal buffer. Used in between specs so messages do not leak out
|
|
45
|
+
def reset
|
|
46
|
+
@mutex.synchronize { super }
|
|
21
47
|
end
|
|
22
48
|
end
|
|
23
49
|
end
|
data/package-lock.json
CHANGED
|
@@ -286,9 +286,9 @@
|
|
|
286
286
|
}
|
|
287
287
|
},
|
|
288
288
|
"node_modules/smol-toml": {
|
|
289
|
-
"version": "1.9.
|
|
290
|
-
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.
|
|
291
|
-
"integrity": "sha512-
|
|
289
|
+
"version": "1.9.1",
|
|
290
|
+
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz",
|
|
291
|
+
"integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==",
|
|
292
292
|
"dev": true,
|
|
293
293
|
"license": "BSD-3-Clause",
|
|
294
294
|
"engines": {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: karafka-testing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.6.
|
|
4
|
+
version: 2.6.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- lib/karafka/testing/rspec/helpers.rb
|
|
83
83
|
- lib/karafka/testing/rspec/proxy.rb
|
|
84
84
|
- lib/karafka/testing/spec_consumer_client.rb
|
|
85
|
+
- lib/karafka/testing/spec_consumer_resume.rb
|
|
85
86
|
- lib/karafka/testing/spec_producer_client.rb
|
|
86
87
|
- lib/karafka/testing/version.rb
|
|
87
88
|
- package-lock.json
|