Skip to content

Commit 6f76364

Browse files
authored
Make PartitionFlowSpec deterministic under the poll gates (evolution-gaming#886)
Five tests failed intermittently, 113-168 times per 200 iterations, while passing when run in isolation. Both poll gates compare strictly (clock isAfter <at>, PartitionFlow.scala:275 and :279) against refs seeded with the flow's acquisition instant, read from a millisecond-truncated clock. A poll takes a few hundred microseconds, so the opening polls finish inside that same millisecond and skip the gated work - no timers triggered, no commit offset computed - and the assertions that follow see no scheduled commit, or a committed offset that never advanced. Records still fold and later polls evaluate, so the fault is in the tests. Running those bodies under TestControl and stepping virtual time by 1 ms before the first poll opens the gates deterministically, at no wall-clock cost. All five measure 0/200 afterwards, with no assertion changed.
1 parent bd96f8f commit 6f76364

1 file changed

Lines changed: 16 additions & 5 deletions

File tree

core/src/test/scala/com/evolutiongaming/kafka/flow/PartitionFlowSpec.scala

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.evolutiongaming.kafka.flow
22

3+
import cats.effect.testkit.TestControl
34
import cats.effect.unsafe.IORuntime
45
import cats.effect.{IO, Ref, Resource}
56
import cats.syntax.all.*
@@ -67,14 +68,16 @@ class PartitionFlowSpec extends FunSuite {
6768

6869
val flow = f.flow use { flow =>
6970
for {
71+
// step past the acquisition ms: poll gates are strict `isAfter`
72+
_ <- IO.sleep(1.milli)
7073
// When("exactly 3 messages come")
7174
_ <- flow(f.records("key1", 100, List("event1", "event2", "event3")))
7275
offset <- f.pendingOffset.get
7376
// Then("offset of first message is returned")
7477
_ <- IO { assertEquals(offset, Some(Offset.unsafe(103))) }
7578
} yield ()
7679
}
77-
flow.unsafeRunSync()
80+
TestControl.executeEmbed(flow).unsafeRunSync()
7881
}
7982

8083
test("PartitionFlow does not allow commit until working with key is finished") {
@@ -84,6 +87,8 @@ class PartitionFlowSpec extends FunSuite {
8487

8588
val flow = f.flow use { flow =>
8689
for {
90+
// step past the acquisition ms: poll gates are strict `isAfter`
91+
_ <- IO.sleep(1.milli)
8792
// When("2 messages come for the key1")
8893
_ <- flow(f.records("key1", 100, List("event1", "event2")))
8994
// And("2 messages come for the key2")
@@ -102,7 +107,7 @@ class PartitionFlowSpec extends FunSuite {
102107

103108
} yield ()
104109
}
105-
flow.unsafeRunSync()
110+
TestControl.executeEmbed(flow).unsafeRunSync()
106111

107112
}
108113

@@ -192,6 +197,8 @@ class PartitionFlowSpec extends FunSuite {
192197

193198
val flow = f.flow.use { flow =>
194199
for {
200+
// step past the acquisition ms: poll gates are strict `isAfter`
201+
_ <- IO.sleep(1.milli)
195202
// The first two events for each state are handled without errors, offset is committed
196203
_ <- flow(f.records("key1", 100, List("event1", "event2")) ++ f.records("key2", 102, List("event3", "event4")))
197204
_ <- f.pendingOffset.get.map(offset => assertEquals(offset, Some(Offset.unsafe(104))))
@@ -204,7 +211,7 @@ class PartitionFlowSpec extends FunSuite {
204211
} yield ()
205212
}
206213

207-
flow.unsafeRunSync()
214+
TestControl.executeEmbed(flow).unsafeRunSync()
208215
}
209216

210217
test("PartitionFlow filters out events but commits offsets") {
@@ -247,6 +254,8 @@ class PartitionFlowSpec extends FunSuite {
247254

248255
val flow = f.flow.use { flow =>
249256
for {
257+
// step past the acquisition ms: poll gates are strict `isAfter`
258+
_ <- IO.sleep(1.milli)
250259
// Only one key is processed and persisted, the second one is not, but the latest offset is committed nonetheless
251260
_ <- flow(f.records(processedKey, 100, List("event1")) ++ f.records(skippedKey, 101, List("event2")))
252261
_ <- f.pendingOffset.get.map(offset => assertEquals(offset, Some(Offset.unsafe(102))))
@@ -265,7 +274,7 @@ class PartitionFlowSpec extends FunSuite {
265274
} yield ()
266275
}
267276

268-
flow.unsafeRunSync()
277+
TestControl.executeEmbed(flow).unsafeRunSync()
269278
}
270279

271280
test("RemapKeys derives keys correctly and updates them before applying filters and folds") {
@@ -294,6 +303,8 @@ class PartitionFlowSpec extends FunSuite {
294303
)
295304

296305
for {
306+
// step past the acquisition ms: poll gates are strict `isAfter`
307+
_ <- IO.sleep(1.milli)
297308
// Ensure pre-existing data is loaded correctly from the storage
298309
_ <- cache.keys.map(keys => assertEquals(keys.size, 1))
299310
_ <- keys.get.map(keys => assertEquals(keys, Set(initialKey)))
@@ -319,7 +330,7 @@ class PartitionFlowSpec extends FunSuite {
319330

320331
}
321332

322-
test.unsafeRunSync()
333+
TestControl.executeEmbed(test).unsafeRunSync()
323334
}
324335

325336
test(

0 commit comments

Comments
 (0)