qubit-clock 0.12.1

Injectable wall and monotonic clocks with deterministic timers
Documentation

Qubit Clock

Rust CI Coverage Crates.io Rust License 中文文档

Time is a hidden dependency. Code that calls SystemTime::now(), Instant::now(), or a sleep function directly is tied to the machine clock: tests must really wait, boundary cases are hard to reach, and clock changes can make results nondeterministic.

qubit-clock turns time into an injectable dependency. Application components depend on small clock and timer traits; the composition root supplies standard implementations in production and fixed or manually advanced implementations in tests. The same business code runs in both environments, with no test-only branch and no real delay.

Detailed documentation:

A first example

This session records a monotonic deadline instead of reading the global clock. Its constructor accepts Arc<dyn MonotonicClock>, so the caller chooses how time progresses:

use qubit_clock::{
    ManualMonotonicClock, MonotonicClock, MonotonicInstant, StdMonotonicClock,
    TimeError,
};
use std::{sync::Arc, time::Duration};

struct Session {
    clock: Arc<dyn MonotonicClock>,
    expires_at: MonotonicInstant,
}

impl Session {
    fn new(
        clock: Arc<dyn MonotonicClock>,
        ttl: Duration,
    ) -> Result<Self, TimeError> {
        let expires_at = clock.deadline_after(ttl)?;
        Ok(Self { clock, expires_at })
    }

    fn is_expired(&self) -> bool {
        self.clock.now() >= self.expires_at
    }
}

fn main() -> Result<(), TimeError> {
    // Production assembly uses the operating system's monotonic clock.
    let _production = Session::new(
        Arc::new(StdMonotonicClock::new()),
        Duration::from_secs(30),
    )?;

    // A test injects manual time and reaches the boundary immediately.
    let clock = ManualMonotonicClock::new_shared();
    let session = Session::new(clock.clone(), Duration::from_secs(30))?;
    assert!(!session.is_expired());

    clock.advance(Duration::from_secs(30))?;
    assert!(session.is_expired());
    Ok(())
}

The test covers an exact 30-second boundary without sleeping for 30 seconds. Only the assembly changes; Session contains no mock flag or test-specific logic.

Components at a glance

Need Trait Real time Deterministic tests
Externally meaningful timestamps WallClock StdWallClock FixedWallClock, ManualWallClock
Elapsed time and deadlines MonotonicClock StdMonotonicClock, TokioMonotonicClock ManualMonotonicClock
Async deadlines Timer StdTimer, TokioTimer ManualTimer
Blocking waits BlockingSleeper adapter compose a timer with independent progress compose a manually driven timer

Wall-clock time may jump and is intended for externally meaningful timestamps. Monotonic time never moves backward within one clock domain and is intended for elapsed time, retries, and timeouts. Every clock creates a same-domain timer directly with clock.new_timer().

Installation

[dependencies]
qubit-clock = "0.12"

Enable the tokio feature when you need Tokio-backed clock and timer types and their runtime-related errors:

[dependencies]
qubit-clock = { version = "0.12", features = ["tokio"] }

The tokio feature exposes TokioMonotonicClock, TokioTimer, and their runtime-related errors. Manual time is executor-neutral and does not require this feature. Tests that need deterministic timer failures can enable the default-off test-util feature in a development dependency.

Timers and waits

Inject Arc<dyn Timer> when a component must await a deadline instead of only checking the current time. Timer::after creates a relative deadline and Timer::at accepts an absolute MonotonicInstant. A ManualMonotonicClock creates a same-domain manual timer, so tests can advance logical time instead of waiting for a scheduler or the operating system.

BlockingSleeper adapts a timer for synchronous code. Its timer backend must be able to progress independently while the calling thread is parked. The user guide covers manual-time coordination, Tokio runtime ownership, wall-clock projection, cancellation, and error handling.

Use in related libraries

The same injection model is used by rs-command to enforce command timeouts without real waits and by rs-id to separate ID timestamps from allocation waits. rs-lock uses it to test timeout-aware waits, while rs-retry uses it to test retry delays, attempt timeouts, and elapsed-time budgets. These libraries inject the narrow capability they need (WallClock, Timer, or MonotonicClock); their production code does not contain a separate mock waiting algorithm.

Testing

# Run tests with the default feature set
cargo test

# Run tests with all declared features
cargo test --all-features

# Project CI checks
./ci-check.sh

# Check code coverage
./coverage.sh

License

Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.

Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.

Contributing

Contributions are welcome. Please follow the Rust API guidelines, keep public API documentation and tests current, and run ./align-ci.sh to format code and ./ci-check.sh to satisfy CI requirements before submitting a pull request.

Author

Haixing Hu - Qubit Co. Ltd.

Repository: https://github.com/qubit-ltd/rs-clock