ReceiveTimeout: use cats-effect Clock instead of System.currentTimeMillis (TestControl support)#44
ReceiveTimeout: use cats-effect Clock instead of System.currentTimeMillis (TestControl support)#44rssh wants to merge 5 commits into
Conversation
…imeout ReceiveTimeout recorded timestamps via System.currentTimeMillis(), which cats.effect.testkit.TestControl cannot advance. As a result actor receive-timeouts never fire under virtual time, forcing actor tests onto the real runtime (and real wall-clock waits). Switch to Clock[F].realTime — the class already requires Sync[F], which extends Clock[F], so no new constraint is added. On the real runtime Clock[F].realTime is the wall clock, so behaviour is unchanged; under TestControl it now advances with virtual time and timeouts fire deterministically and instantly. Generated by Claude Opus 4.7
…virtual time
Adds a regression test driving the existing constantFlowActor under
cats.effect.testkit.TestControl: IO.sleep is advanced virtually (no real
wait) and the receive timeout must fire. Verified to fail before the
Clock[F] change ("0 was not greater than or equal to 1") and pass after.
Adds cats-effect-testkit as a Test dependency.
Generated by Claude Opus 4.7
Per Copilot review: align with dungeon/engine files that import cats.syntax.all._ rather than the narrower flatMap/functor imports. Generated by Claude Opus 4.7
A scenario that was impossible to test before the Clock[F] change: a 1-hour receive timeout. With a wall clock this could only be verified by waiting an hour; under TestControl the hour of IO.sleep is advanced virtually, so the test runs in milliseconds and deterministically. Generated by Claude Opus 4.7
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates receive-timeout tracking to use the cats-effect clock (supporting virtual time under cats.effect.testkit.TestControl) and adds regression tests to ensure timeouts fire deterministically without real waiting.
Changes:
- Switch receive-timeout timestamping from
System.currentTimeMillis()toClock[F].realTime. - Add
cats-effect-testkitas a test dependency. - Add
TestControl-based regression tests for short and long receive timeouts.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| shared/src/main/scala/com/suprnation/actor/dungeon/ReceiveTimeout.scala | Uses Clock[F] for time measurement so timeouts work under virtual time. |
| shared/src/test/scala/com/suprnation/actor/timeout/ReceiveTimeoutSpec.scala | Adds TestControl regression tests to validate virtual-time timeout behavior. |
| build.sbt | Adds the cats-effect-testkit dependency for tests. |
Comments suppressed due to low confidence (1)
shared/src/main/scala/com/suprnation/actor/dungeon/ReceiveTimeout.scala:34
nowMillissummonsClock[F], but the class only requiresSync[F]. This will fail to compile unlessClock[F]evidence is available in the class scope. Add an explicitClock[F]constraint (e.g., a context bound or an implicit constructor parameter) soClock[F]is guaranteed to be resolvable here.
class ReceiveTimeout[F[_]: Sync, Request](
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Use the cats-effect clock instead of System.currentTimeMillis so that receive-timeouts honour | ||
| // virtual time under cats.effect.testkit.TestControl (deterministic, instant tests). On the real | ||
| // runtime Clock[F].realTime is the wall clock, so behaviour is unchanged. | ||
| private def nowMillis: F[Long] = Clock[F].realTime.map(_.toMillis) |
| // Use the cats-effect clock instead of System.currentTimeMillis so that receive-timeouts honour | ||
| // virtual time under cats.effect.testkit.TestControl (deterministic, instant tests). On the real | ||
| // runtime Clock[F].realTime is the wall clock, so behaviour is unchanged. | ||
| private def nowMillis: F[Long] = Clock[F].realTime.map(_.toMillis) |
| // executeEmbed runs the whole program in simulated time and fails on non-termination. | ||
| TestControl.executeEmbed(program).map { case (timeouts, msgs) => | ||
| timeouts should be >= 1 | ||
| msgs should be(List.empty) | ||
| } |
| TestControl.executeEmbed(program).map { case (timeouts, _) => | ||
| timeouts should be >= 1 | ||
| } |
Per review: a receive timeout measures *elapsed* time, so monotonic is the correct clock (immune to NTP / wall-clock jumps) rather than realTime. The elapsed comparison (timestamp + timeout <= now) is origin-independent, and both clocks are TestControl-controllable, so the virtual-time tests still pass. Generated by Claude Opus 4.7
|
Thanks for the review. Going through the points: Adopted — The other three appear to be from a static read (the note says the full agentic suite didn't run) — they don't hold up against a compile + test run:
The new TestControl test was also verified to fail before this change ( |
|
btw, this fix #43 |
Problem
ReceiveTimeoutrecords and compares timestamps withSystem.currentTimeMillis()(4 call sites).cats.effect.testkit.TestControlcannot advance the JVM wall clock, so actor receive-timeouts never fire under virtual time — forcing any actor test that depends on a receive timeout onto the real runtime with real wall-clock waits.Change
Use
Clock[F].realTimeinstead.ReceiveTimeoutalready requiresF[_]: Sync, andSync[F]extendsClock[F], so no new type-class constraint is introduced.Clock[F].realTimeis the wall clock → behaviour unchanged.TestControlit advances with virtual time → receive-timeouts fire deterministically and instantly.The actor
Scheduleralready schedules viadelayBy/Temporal.sleep(virtual-time safe), so this was the remaining wall-clock dependency for receive-timeouts.Tests
ReceiveTimeoutSpecgains two tests that the previous implementation made impossible:TestControl(virtual time, no real waiting) — verified to fail before this change (0 was not greater than or equal to 1) and pass after;Adds
cats-effect-testkitas aTestdependency.🤖 Generated with Claude Code
Reviewed by @rssh (human)