Skip to content

ReceiveTimeout: use cats-effect Clock instead of System.currentTimeMillis (TestControl support)#44

Open
rssh wants to merge 5 commits into
cloudmark:mainfrom
rssh:fix/receive-timeout-virtual-clock
Open

ReceiveTimeout: use cats-effect Clock instead of System.currentTimeMillis (TestControl support)#44
rssh wants to merge 5 commits into
cloudmark:mainfrom
rssh:fix/receive-timeout-virtual-clock

Conversation

@rssh
Copy link
Copy Markdown

@rssh rssh commented May 26, 2026

Problem

ReceiveTimeout records and compares timestamps with System.currentTimeMillis() (4 call sites). cats.effect.testkit.TestControl cannot 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].realTime instead. ReceiveTimeout already requires F[_]: Sync, and Sync[F] extends Clock[F], so no new type-class constraint is introduced.

  • On the real runtime Clock[F].realTime is the wall clock → behaviour unchanged.
  • Under TestControl it advances with virtual time → receive-timeouts fire deterministically and instantly.

The actor Scheduler already schedules via delayBy/Temporal.sleep (virtual-time safe), so this was the remaining wall-clock dependency for receive-timeouts.

Tests

ReceiveTimeoutSpec gains two tests that the previous implementation made impossible:

  • receive timeout fires under TestControl (virtual time, no real waiting) — verified to fail before this change (0 was not greater than or equal to 1) and pass after;
  • a 1-hour receive timeout fires in virtual time (a real-clock test would have to wait an hour) — runs in milliseconds, deterministically.

Adds cats-effect-testkit as a Test dependency.

🤖 Generated with Claude Code
Reviewed by @rssh (human)

rssh added 4 commits May 26, 2026 16:26
…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
Copilot AI review requested due to automatic review settings May 26, 2026 14:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() to Clock[F].realTime.
  • Add cats-effect-testkit as 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

  • nowMillis summons Clock[F], but the class only requires Sync[F]. This will fail to compile unless Clock[F] evidence is available in the class scope. Add an explicit Clock[F] constraint (e.g., a context bound or an implicit constructor parameter) so Clock[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)
Comment on lines +223 to +227
// 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)
}
Comment on lines +249 to +251
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
@rssh
Copy link
Copy Markdown
Author

rssh commented May 26, 2026

Thanks for the review. Going through the points:

Adopted — Clock[F].monotonic (the clock-choice comment). A receive timeout measures elapsed time, so monotonic is the correct source (immune to NTP / wall-clock jumps); switched in the latest commit. The elapsed comparison timestamp + timeout <= now is origin-independent, and TestControl controls monotonic too, so the virtual-time tests still pass.

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:

  • Clock[F] not resolvable from Sync[F]Sync[F] extends Clock[F] in cats-effect 3, so it resolves with no extra constraint. The module compiles (and is used downstream).
  • executeEmbed(...).map won't compileTestControl.executeEmbed returns IO[A] (it's .execute that returns IO[TestControl[A]]), so .map is valid. ReceiveTimeoutSpec compiles and passes 6/6, including the two new virtual-time tests.

The new TestControl test was also verified to fail before this change (0 was not greater than or equal to 1), confirming it actually guards the behaviour.

@rssh
Copy link
Copy Markdown
Author

rssh commented May 26, 2026

btw, this fix #43

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants