Overview
Getting started
Pin the crate version, construct a SystemClock, and read time through one injected Clock — then swap in a VirtualClock for tests with no other code changes.
Add the dependency
Add FerroReplay from the approved MorphIQ package source for your workspace. Consumers pin a version and upgrade deliberately:
[dependencies]
ferro-replay = { version = "0.1" }Tags are semver. Breaking the Clockcontract is a major bump; the determinism laws (monotonicity, wakes-before-further-advance) are load-bearing for every consumer's replay correctness, so upgrades are deliberate.
Construct the live clock
Build one SystemClock at the composition root and hand it around as Arc<dyn Clock>. It anchors the epoch timeline to a monotonic instant once at construction, so now_ns never goes backwards across NTP steps and costs no syscall per call.
use std::sync::Arc;
use std::time::Duration;
use ferro_replay::{Clock, SystemClock};
let clock: Arc<dyn Clock> = Arc::new(SystemClock::new());
// Read the current time, nanoseconds since the UNIX epoch.
let now_ns = clock.now_ns();
// Sleep relative to now — e.g. an I/O-timeout race or retry backoff.
clock.sleep(Duration::from_millis(250)).await;
// Or wait until an absolute deadline on the same timeline.
clock.sleep_until(now_ns + 1_000_000_000).await; // +1sSwap in virtual time for tests
Because every component already depends on Arc<dyn Clock>, a test simply constructs a VirtualClock instead. Advancing time wakes the sleeper, which observes now equal to its own deadline — and advancing before the sleeper registers still wakes it, so the test is race-free with no real waiting.
use std::sync::Arc;
use ferro_replay::{Clock, VirtualClock};
#[tokio::test]
async fn wakes_at_its_deadline() {
let clock = Arc::new(VirtualClock::new(1_000));
let c = clock.clone();
let task = tokio::spawn(async move {
c.sleep_until(2_000).await;
c.now_ns()
});
clock.advance_to(2_000).await;
assert_eq!(task.await.unwrap(), 2_000);
}The strict ordering guarantees hold on a current-thread runtime — the default #[tokio::test] flavor. A multi-thread runtime would reintroduce wake-order races, so a replay binary structurally selects the current-thread scheduler.
Where to go next
- Core concepts — the timeline, the two clocks, intervals, and the determinism laws.
- The wallclock gate — wire the discipline that keeps your repo deterministic.
- Inject the clock — the wiring pattern, end to end.