Skip to main content

autumn_web/
time.rs

1//! Deterministic, injectable wall-clock time.
2//!
3//! Autumn exposes a [`Clock`] extractor so handlers can read the current time
4//! through the framework's injected clock instead of calling
5//! [`chrono::Utc::now`] directly. In tests, replace the clock with
6//! [`FixedClock`] or [`TickingClock`] via [`crate::test::TestApp::with_clock`]
7//! to control time without sleeping.
8//!
9//! # Quick example
10//!
11//! ```rust,no_run
12//! use autumn_web::prelude::*;
13//! use autumn_web::time::Clock;
14//!
15//! #[get("/token-age")]
16//! async fn token_age(clock: Clock) -> String {
17//!     format!("now is {}", clock.now())
18//! }
19//! ```
20//!
21//! # Testing time-sensitive logic
22//!
23//! ```rust,no_run
24//! use autumn_web::prelude::*;
25//! use autumn_web::test::TestApp;
26//! use autumn_web::time::{Clock, TickingClock};
27//! use chrono::{TimeZone, Utc};
28//! use std::time::Duration;
29//!
30//! #[get("/token")]
31//! async fn check_token(clock: Clock) -> axum::http::StatusCode {
32//!     let issued = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
33//!     if clock.now() < issued + chrono::Duration::days(30) {
34//!         axum::http::StatusCode::OK
35//!     } else {
36//!         axum::http::StatusCode::UNAUTHORIZED
37//!     }
38//! }
39//!
40//! # #[tokio::main]
41//! # async fn main() {
42//! let issued = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
43//! let client = TestApp::new()
44//!     .routes(routes![check_token])
45//!     .with_clock(TickingClock::starting_at(issued))
46//!     .build();
47//!
48//! client.get("/token").send().await.assert_status(200); // valid
49//! client.advance_clock(Duration::from_secs(30 * 24 * 3600)); // advance 30 days
50//! client.get("/token").send().await.assert_status(401); // expired
51//! # }
52//! ```
53
54// autumn-determinism-gate: production code in this module must read time and
55// mint identifiers through the framework's injected seams (ClockSource /
56// Entropy), never `Instant::now()` / `Utc::now()` / `SystemTime::now()` /
57// `Uuid::new_v4()` directly. See CONTRIBUTING.md "Determinism seam gate"
58// (issue #1797). Justify exceptions with
59// #[allow(clippy::disallowed_methods, reason = "…")] at the narrowest scope.
60#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
61
62use std::sync::{Arc, LazyLock, Mutex};
63use std::time::Duration;
64
65use chrono::{DateTime, Utc};
66
67// ── Monotonic instant ─────────────────────────────────────────────────────────
68
69/// Process-wide origin for the real monotonic clock.
70///
71/// Sampled once, lazily, the first time [`ClockSource::monotonic`] runs on a
72/// source that uses the default (real) implementation. Every real monotonic
73/// reading is an offset from this single instant, which is what makes
74/// [`MonotonicInstant`] a plain [`Duration`] and therefore constructible at an
75/// arbitrary *virtual* point — something [`std::time::Instant`] can never be.
76#[allow(
77    clippy::disallowed_methods,
78    reason = "this IS the seam: the single process-monotonic origin every real \
79              MonotonicInstant is measured from. There is nothing further to \
80              inject it from."
81)]
82static MONOTONIC_ORIGIN: LazyLock<std::time::Instant> = LazyLock::new(std::time::Instant::now);
83
84/// A monotonic instant read from a [`ClockSource`], for measuring *elapsed
85/// time* the way [`ClockSource::now`] measures *wall-clock time*.
86///
87/// # Why this exists
88///
89/// [`ClockSource`] models only `DateTime<Utc>`, so framework code that needed a
90/// duration (request latency, query timings, task run times, uptime) reached
91/// for [`std::time::Instant`] directly — off the injected seam. `Instant` is
92/// opaque and cannot be constructed at an arbitrary point, and tokio's
93/// `start_paused` test runtime does **not** virtualize it (only
94/// [`tokio::time::Instant`] moves with the paused timer). Under a
95/// [`#[sim_test]`](crate::sim_test) those measurements silently came from the
96/// real machine clock: a 24-hour virtual advance read back as microseconds, and
97/// two runs of the same seed disagreed.
98///
99/// `MonotonicInstant` is an offset from its source's own origin, so a virtual
100/// clock can produce one at any point while a real clock keeps genuine
101/// monotonicity.
102///
103/// # Guarantees
104///
105/// - **Monotonic in production.** [`SystemClock`] derives it from a
106///   process-global [`std::time::Instant`], so a wall-clock/NTP jump — even a
107///   backwards one — can never make an elapsed duration negative or absurd.
108///   Comparing wall-clock timestamps has no such guarantee.
109/// - **Virtual under simulation.** [`TickingClock`] derives it from the virtual
110///   instant, so [`Sim::advance`](crate::sim::Sim::advance) moves it and the same
111///   seed replays the same durations byte-for-byte.
112/// - **Only comparable within one source.** Two `MonotonicInstant`s are relative
113///   to their own clock's origin; subtracting across different clocks is
114///   meaningless. [`saturating_duration_since`](Self::saturating_duration_since)
115///   never panics and never underflows.
116///
117/// # Taking two readings
118///
119/// Measuring elapsed time needs a *start* and an *end*. The [`Clock`] extractor
120/// is a **snapshot** taken when the extractor resolved, so
121/// [`Clock::monotonic`] gives you the request-start instant and never moves —
122/// calling it twice returns the same value. Take the closing reading from the
123/// live source, [`AppState::monotonic`](crate::state::AppState::monotonic):
124///
125/// ```rust,ignore
126/// use autumn_web::prelude::*;
127/// use autumn_web::time::Clock;
128///
129/// #[get("/work")]
130/// async fn handler(clock: Clock, state: AppState) -> String {
131///     let start = clock.monotonic();           // request start (a snapshot)
132///     // ... work ...
133///     let elapsed = state.monotonic()          // live reading
134///         .saturating_duration_since(start);
135///     format!("took {}ms", elapsed.as_millis())
136/// }
137/// ```
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub struct MonotonicInstant(Duration);
140
141impl MonotonicInstant {
142    /// The origin of a monotonic clock — the zero point every reading is
143    /// measured from.
144    pub const ORIGIN: Self = Self(Duration::ZERO);
145
146    /// Build a monotonic instant `since_origin` after its source's origin.
147    ///
148    /// Framework/clock-implementor helper: prefer [`ClockSource::monotonic`] to
149    /// *read* the current instant.
150    #[must_use]
151    pub const fn from_origin_elapsed(since_origin: Duration) -> Self {
152        Self(since_origin)
153    }
154
155    /// How far this instant sits after its source's origin.
156    #[must_use]
157    pub const fn since_origin(self) -> Duration {
158        self.0
159    }
160
161    /// The duration from `earlier` to `self`, saturating at
162    /// [`Duration::ZERO`] when `earlier` is later.
163    ///
164    /// This is the replacement for `end - start` / `start.elapsed()` on
165    /// [`std::time::Instant`]. It never panics, and (unlike subtracting two
166    /// wall-clock timestamps) it cannot go negative.
167    #[must_use]
168    pub const fn saturating_duration_since(self, earlier: Self) -> Duration {
169        self.0.saturating_sub(earlier.0)
170    }
171
172    /// The duration from `self` to `now`, saturating at [`Duration::ZERO`].
173    ///
174    /// Convenience mirror of [`std::time::Instant::elapsed`] for callers holding
175    /// a start instant and a freshly-read `now`.
176    #[must_use]
177    pub const fn elapsed_at(self, now: Self) -> Duration {
178        now.saturating_duration_since(self)
179    }
180
181    /// This instant advanced by `duration`, or `None` on overflow.
182    #[must_use]
183    pub const fn checked_add(self, duration: Duration) -> Option<Self> {
184        match self.0.checked_add(duration) {
185            Some(sum) => Some(Self(sum)),
186            None => None,
187        }
188    }
189
190    /// This instant advanced by `duration`, saturating at [`Duration::MAX`].
191    ///
192    /// The panic-free replacement for `Instant + Duration`, which panics when
193    /// the sum is not representable on the platform clock.
194    #[must_use]
195    pub const fn saturating_add(self, duration: Duration) -> Self {
196        Self(self.0.saturating_add(duration))
197    }
198}
199
200// ── Clock source trait ────────────────────────────────────────────────────────
201
202/// Source of the current wall-clock time used internally by the framework.
203///
204/// Production apps see [`SystemClock`] (the silent default). Tests swap it out
205/// via [`crate::test::TestApp::with_clock`].
206///
207/// Implement this trait to supply a custom clock (e.g. from an NTP client or a
208/// property-testing generator).
209pub trait ClockSource: Send + Sync + 'static {
210    /// Returns the current UTC instant.
211    fn now(&self) -> DateTime<Utc>;
212
213    /// Returns the current *monotonic* instant, for measuring elapsed time.
214    ///
215    /// The default implementation reads the real process-monotonic clock
216    /// ([`std::time::Instant`] offset from a process-global origin) — exactly
217    /// what framework code did before this method existed. That makes the method
218    /// **fully backward compatible**: an existing downstream `impl ClockSource`
219    /// keeps compiling and keeps its current behavior.
220    ///
221    /// **Override this whenever [`now`](Self::now) is virtual.** A clock that
222    /// pins or steps wall time but leaves this at the default reports real
223    /// elapsed time, which silently reintroduces nondeterminism — the exact gap
224    /// [`MonotonicInstant`] exists to close. The in-tree virtual clocks
225    /// ([`FixedClock`], [`TickingClock`]) override it; [`SystemClock`]
226    /// deliberately does not.
227    fn monotonic(&self) -> MonotonicInstant {
228        MonotonicInstant::from_origin_elapsed(MONOTONIC_ORIGIN.elapsed())
229    }
230}
231
232/// A shared clock handle is itself a clock.
233///
234/// Lets an already-`Arc`ed source — the one a capsule replay builds, or one a
235/// test keeps a handle on to inspect afterwards — be handed to APIs that take
236/// `impl ClockSource` by value, without a hand-written forwarding newtype at
237/// every call site.
238impl ClockSource for std::sync::Arc<dyn ClockSource> {
239    fn now(&self) -> DateTime<Utc> {
240        (**self).now()
241    }
242
243    fn monotonic(&self) -> MonotonicInstant {
244        (**self).monotonic()
245    }
246}
247
248// ── Extractor ─────────────────────────────────────────────────────────────────
249
250/// Axum extractor that resolves the current framework time.
251///
252/// Use as a handler argument to get the current time through the injected clock
253/// instead of calling [`chrono::Utc::now`] directly. This lets tests control
254/// time via [`crate::test::TestApp::with_clock`] and
255/// [`crate::test::TestClient::advance_clock`].
256///
257/// ```rust,ignore
258/// use autumn_web::time::Clock;
259///
260/// async fn handler(clock: Clock) -> String {
261///     format!("Current time: {}", clock.now())
262/// }
263/// ```
264#[derive(Debug, Clone, Copy)]
265pub struct Clock(DateTime<Utc>, MonotonicInstant);
266
267impl Clock {
268    /// Returns the UTC instant captured when this extractor was resolved.
269    #[must_use]
270    pub const fn now(&self) -> DateTime<Utc> {
271        self.0
272    }
273
274    /// Returns the *monotonic* instant captured when this extractor was
275    /// resolved — i.e. the **request-start** instant, and the deterministic
276    /// replacement for [`std::time::Instant::now`] as the start of an elapsed
277    /// measurement.
278    ///
279    /// Like [`now`](Self::now) this is a **snapshot**, not a live handle:
280    /// calling it twice on the same `Clock` returns the same value. Take the
281    /// closing reading from the live source —
282    /// [`AppState::monotonic`](crate::state::AppState::monotonic) — and subtract
283    /// with [`MonotonicInstant::saturating_duration_since`]. See
284    /// [`MonotonicInstant`]'s "Taking two readings" for a worked example.
285    ///
286    /// Under a [`#[sim_test]`](crate::sim_test) the underlying clock moves only
287    /// when [`Sim::advance`](crate::sim::Sim::advance) steps virtual time, so the
288    /// resulting measurement is reproducible from the seed.
289    #[must_use]
290    pub const fn monotonic(&self) -> MonotonicInstant {
291        self.1
292    }
293}
294
295impl std::ops::Deref for Clock {
296    type Target = DateTime<Utc>;
297
298    fn deref(&self) -> &Self::Target {
299        &self.0
300    }
301}
302
303impl axum::extract::FromRequestParts<crate::state::AppState> for Clock {
304    type Rejection = std::convert::Infallible;
305
306    async fn from_request_parts(
307        _parts: &mut axum::http::request::Parts,
308        state: &crate::state::AppState,
309    ) -> Result<Self, Self::Rejection> {
310        let clock = state.clock();
311        Ok(Self(clock.now(), clock.monotonic()))
312    }
313}
314
315// ── System (real) clock ───────────────────────────────────────────────────────
316
317/// Real wall-clock implementation of [`ClockSource`].
318///
319/// This is the default when no custom clock is configured. It delegates to
320/// [`chrono::Utc::now`] and carries zero overhead compared to calling
321/// `Utc::now()` directly.
322///
323/// It deliberately does **not** override [`ClockSource::monotonic`]: the trait's
324/// default body already reads the real process-monotonic clock, so an elapsed
325/// measurement taken through this clock is derived from the same
326/// [`std::time::Instant`] reading it replaced — the identical monotonicity
327/// guarantee, plus one relaxed atomic load (the process-origin
328/// [`std::sync::LazyLock`]) and one [`Duration`] subtraction. That is a few
329/// nanoseconds on top of the `Instant::now()` call itself, and it is *not* on
330/// the per-request path unless a caller asks for elapsed time.
331#[derive(Debug, Clone, Copy, Default)]
332pub struct SystemClock;
333
334impl ClockSource for SystemClock {
335    #[allow(
336        clippy::disallowed_methods,
337        reason = "this IS the seam: SystemClock is the production ClockSource, \
338                  the one place the real wall clock is allowed to be read."
339    )]
340    fn now(&self) -> DateTime<Utc> {
341        Utc::now()
342    }
343}
344
345// ── Fixed clock ───────────────────────────────────────────────────────────────
346
347/// A test clock that stays pinned to a fixed point in time.
348///
349/// Every call to [`ClockSource::now`] returns the same instant. Use when you
350/// need a stable reference time but don't need [`crate::test::TestClient::advance_clock`].
351///
352/// Calling `advance_clock` when this clock is active is a safe no-op.
353///
354/// ```rust,ignore
355/// use autumn_web::time::FixedClock;
356/// use chrono::{TimeZone, Utc};
357///
358/// let clock = FixedClock::at(Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap());
359/// ```
360#[derive(Debug, Clone, Copy)]
361pub struct FixedClock(DateTime<Utc>);
362
363impl FixedClock {
364    /// Create a clock pinned to `dt`.
365    #[must_use]
366    pub const fn at(dt: DateTime<Utc>) -> Self {
367        Self(dt)
368    }
369}
370
371impl ClockSource for FixedClock {
372    fn now(&self) -> DateTime<Utc> {
373        self.0
374    }
375
376    /// Pinned, exactly like [`now`](ClockSource::now): a clock whose wall time
377    /// never moves must not report elapsed time either, or a test that pins the
378    /// clock would still see real durations tick past.
379    fn monotonic(&self) -> MonotonicInstant {
380        MonotonicInstant::ORIGIN
381    }
382}
383
384// ── Ticking clock ─────────────────────────────────────────────────────────────
385
386/// A test clock that starts at a given time and can be stepped forward.
387///
388/// Cloning produces a handle that shares the same internal instant — a clone
389/// passed to [`crate::test::TestApp::with_clock`] and a clone kept by the test
390/// both observe the same time.
391///
392/// Advance the clock between requests via
393/// [`crate::test::TestClient::advance_clock`]:
394///
395/// ```rust,ignore
396/// use autumn_web::time::TickingClock;
397/// use chrono::{TimeZone, Utc};
398/// use std::time::Duration;
399///
400/// let clock = TickingClock::starting_at(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap());
401/// let client = TestApp::new().with_clock(clock.clone()).build();
402///
403/// client.advance_clock(Duration::from_secs(3600)); // advance 1 hour
404/// ```
405#[derive(Clone, Debug)]
406pub struct TickingClock {
407    /// Shared current virtual instant, stepped by [`advance`](Self::advance).
408    current: Arc<Mutex<DateTime<Utc>>>,
409    /// The instant this clock started at. Copied (not shared) on clone, which is
410    /// harmless because it is immutable — every clone agrees on the origin, so
411    /// every clone reports the same [`monotonic`](ClockSource::monotonic).
412    start: DateTime<Utc>,
413}
414
415impl TickingClock {
416    /// Create a ticking clock starting at `dt`.
417    #[must_use]
418    pub fn starting_at(dt: DateTime<Utc>) -> Self {
419        Self {
420            current: Arc::new(Mutex::new(dt)),
421            start: dt,
422        }
423    }
424
425    /// Step this clock forward by `duration`.
426    ///
427    /// Sub-millisecond durations are truncated to zero (chrono's minimum resolution
428    /// is microseconds). This method never panics.
429    pub fn advance(&self, duration: std::time::Duration) {
430        let mut guard = self
431            .current
432            .lock()
433            .unwrap_or_else(std::sync::PoisonError::into_inner);
434        if let Ok(delta) = chrono::Duration::from_std(duration) {
435            *guard += delta;
436        }
437    }
438}
439
440impl ClockSource for TickingClock {
441    fn now(&self) -> DateTime<Utc> {
442        *self
443            .current
444            .lock()
445            .unwrap_or_else(std::sync::PoisonError::into_inner)
446    }
447
448    /// Virtual, derived from the distance the clock has been stepped from its
449    /// starting instant — so elapsed time moves *only* when
450    /// [`advance`](Self::advance) moves it, in exact lockstep with the wall
451    /// clock.
452    ///
453    /// Because `advance` only ever adds, the difference is non-negative; a
454    /// (currently impossible) backwards step saturates to the origin rather than
455    /// panicking.
456    fn monotonic(&self) -> MonotonicInstant {
457        let elapsed = self
458            .now()
459            .signed_duration_since(self.start)
460            .to_std()
461            .unwrap_or(Duration::ZERO);
462        MonotonicInstant::from_origin_elapsed(elapsed)
463    }
464}
465
466// ── Helpers for internal framework code ──────────────────────────────────────
467
468/// The current instant on the **real** process-monotonic timeline.
469///
470/// The sanctioned replacement for [`std::time::Instant::now`] in framework code
471/// that genuinely has no [`ClockSource`] handle in scope (a constructor that
472/// runs before the clock is installed, a free function with no state argument).
473/// Equivalent to `SystemClock.monotonic()`.
474///
475/// Prefer a real seam wherever one is reachable —
476/// [`AppState::monotonic`](crate::state::AppState::monotonic),
477/// [`Clock::monotonic`], or `clock.monotonic()` on a threaded-in
478/// `Arc<dyn ClockSource>` — because only those follow a virtual clock under a
479/// [`#[sim_test]`](crate::sim_test). This function never does; for a reading
480/// that follows a running `Sim`, use [`ambient_monotonic`].
481#[must_use]
482pub fn monotonic_now() -> MonotonicInstant {
483    SystemClock.monotonic()
484}
485
486// ── Ambient clock ─────────────────────────────────────────────────────────────
487//
488// Framework code with no clock handle in scope used to read the OS clock
489// directly, which a `Sim` cannot control (issue #2967). The ambient clock is
490// the fix: a `Sim` installs its virtual clock for its own thread while it
491// lives, and the `ambient_*` helpers read it. The sim runtime is a
492// current-thread runtime, so every task it polls runs on that thread. Other
493// threads, and code that runs with no `Sim`, see the system clock.
494
495thread_local! {
496    /// The clocks installed on this thread, newest last, with a liveness flag.
497    static AMBIENT: std::cell::RefCell<Vec<AmbientEntry>> =
498        const { std::cell::RefCell::new(Vec::new()) };
499}
500
501/// One installed ambient clock.
502struct AmbientEntry {
503    /// Cleared when the guard drops, on any thread.
504    alive: Arc<std::sync::atomic::AtomicBool>,
505    clock: Arc<dyn ClockSource>,
506    /// Where this clock's [`ambient_instant`]s start, after
507    /// `MONOTONIC_ORIGIN`.
508    floor: Duration,
509    /// The clock's own monotonic reading when it was installed.
510    start: Duration,
511}
512
513/// The latest [`ambient_instant`] given out in this process, in nanoseconds
514/// after `MONOTONIC_ORIGIN`.
515///
516/// A clock installed later starts its instants here, so a new timeline never
517/// starts before an earlier one's instants. State that outlives one `Sim` then
518/// sees the next `Sim`'s instants as later, as real time would, and its TTLs
519/// and idle ages run on (issue #2967).
520static INSTANT_HIGH_WATER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
521
522/// Where a clock installed now starts its instants: the system clock or the
523/// latest instant given out, whichever is later.
524fn instant_floor() -> Duration {
525    let high_water =
526        Duration::from_nanos(INSTANT_HIGH_WATER.load(std::sync::atomic::Ordering::Relaxed));
527    SystemClock.monotonic().since_origin().max(high_water)
528}
529
530/// Removes its clock from the ambient stack when dropped.
531///
532/// The guard may drop on another thread (a `Sim` can move). The cleared flag
533/// then makes the install thread skip the entry, and prune it on its next
534/// read.
535#[derive(Debug)]
536pub(crate) struct AmbientGuard {
537    alive: Arc<std::sync::atomic::AtomicBool>,
538}
539
540impl Drop for AmbientGuard {
541    fn drop(&mut self) {
542        self.alive
543            .store(false, std::sync::atomic::Ordering::Release);
544        // `try_with`: a guard dropped during thread teardown finds no stack.
545        let _ = AMBIENT.try_with(|stack| {
546            if let Ok(mut stack) = stack.try_borrow_mut() {
547                stack.retain(|entry| !Arc::ptr_eq(&entry.alive, &self.alive));
548            }
549        });
550    }
551}
552
553/// Make `clock` the ambient clock of this thread until the guard drops. The
554/// newest installed clock wins, so a nested `Sim` shadows an outer one.
555pub(crate) fn install_ambient(clock: Arc<dyn ClockSource>) -> AmbientGuard {
556    // Read before the stack is borrowed: a sim clock's read touches its own
557    // state, not this stack.
558    let start = clock.monotonic().since_origin();
559    install_ambient_at(clock, instant_floor(), start)
560}
561
562/// Install `clock` with a given instant floor and start reading, so its
563/// instants on this thread match those it gives on another.
564fn install_ambient_at(
565    clock: Arc<dyn ClockSource>,
566    floor: Duration,
567    start: Duration,
568) -> AmbientGuard {
569    let alive = Arc::new(std::sync::atomic::AtomicBool::new(true));
570    AMBIENT.with(|stack| {
571        stack.borrow_mut().push(AmbientEntry {
572            alive: Arc::clone(&alive),
573            clock,
574            floor,
575            start,
576        });
577    });
578    AmbientGuard { alive }
579}
580
581/// The newest live ambient clock on this thread, with its instant floor and
582/// start reading.
583fn ambient_entry() -> Option<(Arc<dyn ClockSource>, Duration, Duration)> {
584    AMBIENT
585        .try_with(|stack| {
586            let mut stack = stack.try_borrow_mut().ok()?;
587            // Prune entries whose guard dropped on another thread.
588            stack.retain(|entry| entry.alive.load(std::sync::atomic::Ordering::Acquire));
589            stack
590                .last()
591                .map(|entry| (Arc::clone(&entry.clock), entry.floor, entry.start))
592        })
593        .ok()
594        .flatten()
595}
596
597/// Run `f` on tokio's blocking pool with this thread's ambient clock.
598///
599/// The ambient clock belongs to one thread. Under a plain
600/// `tokio::task::spawn_blocking`, time read in `f` is the system clock, even
601/// inside a `Sim`. This carries the running sim's clock into the blocking
602/// thread, with the same instants, so `f` reads the sim's time. With no
603/// ambient clock it is a plain `spawn_blocking` (issue #2967).
604pub(crate) fn spawn_blocking<F, R>(f: F) -> tokio::task::JoinHandle<R>
605where
606    F: FnOnce() -> R + Send + 'static,
607    R: Send + 'static,
608{
609    let carried = ambient_entry();
610    #[allow(
611        clippy::disallowed_methods,
612        reason = "the sanctioned spawn_blocking: it carries the ambient clock"
613    )]
614    tokio::task::spawn_blocking(move || {
615        let _guard = carried.map(|(clock, floor, start)| install_ambient_at(clock, floor, start));
616        // On a sim runtime, run only in a turn of the gate, as a query does
617        // (issue #3067). Elsewhere this does nothing.
618        let _turn = crate::sim::gate::Scope::enter();
619        f()
620    })
621}
622
623thread_local! {
624    /// The latest instant an ambient clock gave out on this thread, as an
625    /// offset after `MONOTONIC_ORIGIN`.
626    static THREAD_LATEST_INSTANT: std::cell::Cell<Duration> =
627        const { std::cell::Cell::new(Duration::ZERO) };
628}
629
630/// Keep this thread's instants from going back.
631///
632/// A nested `Sim` starts its instants after the outer one's, and runs ahead.
633/// When it ends, the outer `Sim` would give out instants before the ones the
634/// nested `Sim` gave. Instead the outer clock's floor moves up, so it goes on
635/// from where the nested one stopped. Within one `Sim` the clock never goes
636/// back, so this changes nothing there.
637fn keep_thread_instants_forward(offset: Duration) -> Duration {
638    let latest = THREAD_LATEST_INSTANT
639        .try_with(std::cell::Cell::get)
640        .unwrap_or(Duration::ZERO);
641    let offset = if offset < latest {
642        let behind = latest.saturating_sub(offset);
643        let _ = AMBIENT.try_with(|stack| {
644            if let Ok(mut stack) = stack.try_borrow_mut()
645                && let Some(entry) = stack.last_mut()
646            {
647                entry.floor = entry.floor.saturating_add(behind);
648            }
649        });
650        latest
651    } else {
652        offset
653    };
654    let _ = THREAD_LATEST_INSTANT.try_with(|cell| cell.set(offset));
655    offset
656}
657
658/// Run `read` on the ambient clock, or on [`SystemClock`] when none is set.
659fn with_ambient<T>(read: impl FnOnce(&dyn ClockSource) -> T) -> T {
660    match ambient_entry() {
661        Some((clock, _, _)) => read(clock.as_ref()),
662        None => read(&SystemClock),
663    }
664}
665
666/// A [`ClockSource`] that reads the ambient clock: the running
667/// [`Sim`](crate::sim::Sim)'s virtual clock on this thread, else the system
668/// clock.
669///
670/// Under a `Sim`, wall time is the sim clock, and elapsed time
671/// ([`ambient_monotonic`], [`ambient_instant`]) follows tokio's paused clock.
672/// So an ambient deadline and a `tokio::time::sleep` stay on one timeline.
673///
674/// Each `Sim` has its own timeline, and the system clock is another one.
675/// [`ambient_monotonic`] readings are comparable only within one timeline.
676/// [`ambient_instant`] joins the timelines one after the other: a clock
677/// installed later starts its instants at the latest instant already given
678/// out. So state that outlives one `Sim` sees the next one's instants as
679/// later, and its TTLs and idle ages run on. After a nested `Sim` ends, the
680/// outer one's instants go on from where the nested one stopped. So on one
681/// thread an ambient instant never goes back, and no instant stored there is
682/// in the future. Instants from code outside the sim, or from a `Sim` on
683/// another thread, are not ordered this way. Do not share state that stores
684/// ambient instants (a cache, a presence map) with those. Process-global
685/// state reads the system clock instead (the global circuit-breaker registry
686/// does).
687///
688/// Use it where framework code needs a clock but has none in scope. Prefer
689/// the app's injected clock ([`AppState::clock`](crate::state::AppState::clock),
690/// the [`Clock`] extractor) wherever one is reachable.
691#[derive(Debug, Clone, Copy, Default)]
692pub struct AmbientClock;
693
694impl ClockSource for AmbientClock {
695    fn now(&self) -> DateTime<Utc> {
696        ambient_now()
697    }
698
699    fn monotonic(&self) -> MonotonicInstant {
700        ambient_monotonic()
701    }
702}
703
704/// The ambient wall-clock time. Replaces `Utc::now()` where no clock is in
705/// scope. See [`AmbientClock`].
706#[must_use]
707pub fn ambient_now() -> DateTime<Utc> {
708    with_ambient(ClockSource::now)
709}
710
711/// The ambient monotonic instant. Replaces `Instant::now()` for elapsed time
712/// where no clock is in scope. See [`AmbientClock`].
713#[must_use]
714pub fn ambient_monotonic() -> MonotonicInstant {
715    with_ambient(ClockSource::monotonic)
716}
717
718/// The ambient monotonic instant as a [`std::time::Instant`], for code that
719/// stores or passes `Instant`s. See [`AmbientClock`].
720///
721/// Measure with `ambient_instant().saturating_duration_since(start)`, not
722/// `start.elapsed()`: `elapsed` reads the OS clock.
723///
724/// Under a `Sim`, the instant is the sim's elapsed time after a floor set
725/// when the sim started. The floor is never before an instant given out
726/// earlier, so instants from one `Sim` to the next go forward. It moves up
727/// after a nested `Sim` ends, so the outer one's instants do not go back. See
728/// [`AmbientClock`].
729#[must_use]
730pub fn ambient_instant() -> std::time::Instant {
731    let offset = match ambient_entry() {
732        Some((clock, floor, start)) => keep_thread_instants_forward(
733            floor.saturating_add(clock.monotonic().since_origin().saturating_sub(start)),
734        ),
735        None => SystemClock.monotonic().since_origin(),
736    };
737    INSTANT_HIGH_WATER.fetch_max(
738        u64::try_from(offset.as_nanos()).unwrap_or(u64::MAX),
739        std::sync::atomic::Ordering::Relaxed,
740    );
741    *MONOTONIC_ORIGIN + offset
742}
743
744/// The system monotonic clock as a [`std::time::Instant`], never a `Sim`'s.
745///
746/// For instants kept in process-global state, which outlives any `Sim`: a
747/// virtual instant stored there would later be compared with real time.
748pub(crate) fn system_instant() -> std::time::Instant {
749    *MONOTONIC_ORIGIN + SystemClock.monotonic().since_origin()
750}
751
752/// The ambient wall-clock time as a [`std::time::SystemTime`]. Replaces
753/// `SystemTime::now()` where no clock is in scope. See [`AmbientClock`].
754#[must_use]
755pub fn ambient_system_time() -> std::time::SystemTime {
756    std::time::UNIX_EPOCH + clock_unix_duration(&AmbientClock)
757}
758
759/// Compute the current Unix timestamp in seconds from the given clock.
760///
761/// Used by scheduler and storage internals instead of
762/// `SystemTime::now().duration_since(UNIX_EPOCH)`.
763#[must_use]
764pub fn clock_unix_secs(clock: &dyn ClockSource) -> u64 {
765    clock_unix_duration(clock).as_secs()
766}
767
768/// Compute the elapsed duration since the Unix epoch from the given clock.
769#[must_use]
770pub fn clock_unix_duration(clock: &dyn ClockSource) -> std::time::Duration {
771    let now = clock.now();
772    let ts = now.timestamp();
773    if ts >= 0 {
774        std::time::Duration::new(ts.cast_unsigned(), now.timestamp_subsec_nanos())
775    } else {
776        std::time::Duration::ZERO
777    }
778}
779
780// ── Module-level unit tests ───────────────────────────────────────────────────
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use chrono::TimeZone;
786
787    #[test]
788    fn system_clock_returns_time_close_to_utc_now() {
789        let clock = SystemClock;
790        let a = clock.now();
791        let b = Utc::now();
792        assert!(
793            (b - a).num_seconds().abs() < 1,
794            "SystemClock should be within 1s of Utc::now()"
795        );
796    }
797
798    #[test]
799    fn fixed_clock_always_returns_same_time() {
800        let pinned = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
801        let clock = FixedClock::at(pinned);
802        assert_eq!(clock.now(), pinned);
803        assert_eq!(clock.now(), pinned);
804    }
805
806    #[test]
807    fn ticking_clock_starts_at_given_time() {
808        let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
809        let clock = TickingClock::starting_at(start);
810        assert_eq!(clock.now(), start);
811    }
812
813    #[test]
814    fn ticking_clock_advances_correctly() {
815        let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
816        let clock = TickingClock::starting_at(start);
817        clock.advance(std::time::Duration::from_secs(3600));
818        assert_eq!(clock.now(), start + chrono::Duration::hours(1));
819    }
820
821    #[test]
822    fn ticking_clock_clone_shares_state() {
823        let start = Utc.with_ymd_and_hms(2025, 6, 1, 12, 0, 0).unwrap();
824        let clock = TickingClock::starting_at(start);
825        let clone = clock.clone();
826
827        clock.advance(std::time::Duration::from_secs(86400));
828        assert_eq!(clone.now(), start + chrono::Duration::days(1));
829    }
830
831    #[test]
832    fn clock_unix_secs_uses_clock_timestamp() {
833        let pinned = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
834        let clock = FixedClock::at(pinned);
835        let secs = clock_unix_secs(&clock);
836        assert_eq!(secs, pinned.timestamp().cast_unsigned());
837    }
838
839    // ── Monotonic seam (issue #1797) ─────────────────────────────────────
840
841    #[test]
842    fn monotonic_saturates_instead_of_underflowing_on_inversion() {
843        // The one behaviour every caller depends on: subtracting a LATER
844        // instant from an earlier one yields zero rather than panicking or
845        // wrapping. `Duration` has no negative representation, so an unchecked
846        // subtraction here would be an arithmetic panic in production.
847        let early = MonotonicInstant::from_origin_elapsed(Duration::from_secs(1));
848        let late = MonotonicInstant::from_origin_elapsed(Duration::from_secs(5));
849        assert_eq!(
850            late.saturating_duration_since(early),
851            Duration::from_secs(4)
852        );
853        assert_eq!(early.saturating_duration_since(late), Duration::ZERO);
854        assert_eq!(early.elapsed_at(late), Duration::from_secs(4));
855    }
856
857    #[test]
858    fn monotonic_add_saturates_instead_of_panicking() {
859        // `Instant + Duration` panics when the sum is not representable; the
860        // replacement must clamp, because these TTLs are app-supplied.
861        let base = MonotonicInstant::ORIGIN;
862        assert_eq!(
863            base.saturating_add(Duration::from_secs(30)).since_origin(),
864            Duration::from_secs(30)
865        );
866        assert_eq!(
867            base.checked_add(Duration::MAX),
868            Some(MonotonicInstant::from_origin_elapsed(Duration::MAX))
869        );
870        let high = MonotonicInstant::from_origin_elapsed(Duration::MAX);
871        assert_eq!(high.checked_add(Duration::from_secs(1)), None);
872        assert_eq!(high.saturating_add(Duration::from_secs(1)), high);
873    }
874
875    #[test]
876    fn ticking_clock_monotonic_moves_in_lockstep_with_wall_time() {
877        let start = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
878        let clock = TickingClock::starting_at(start);
879        assert_eq!(clock.monotonic(), MonotonicInstant::ORIGIN);
880
881        clock.advance(Duration::from_secs(90));
882        assert_eq!(clock.monotonic().since_origin(), Duration::from_secs(90));
883        assert_eq!(clock.now(), start + chrono::Duration::seconds(90));
884
885        // A clone shares the instant, so it reports the same elapsed time — the
886        // property `TestApp::with_clock` and `Sim::advance` both rely on.
887        let clone = clock.clone();
888        clock.advance(Duration::from_secs(10));
889        assert_eq!(clone.monotonic().since_origin(), Duration::from_secs(100));
890    }
891
892    #[test]
893    fn ticking_clock_monotonic_is_reproducible_across_instances() {
894        // Two clocks constructed identically and stepped identically must agree
895        // exactly — this is what makes a sim run replay from its seed.
896        let start = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
897        let a = TickingClock::starting_at(start);
898        let b = TickingClock::starting_at(start);
899        for step in [1u64, 60, 3_600, 86_400] {
900            a.advance(Duration::from_secs(step));
901            b.advance(Duration::from_secs(step));
902        }
903        assert_eq!(a.monotonic(), b.monotonic());
904        assert_eq!(a.monotonic().since_origin(), Duration::from_secs(90_061));
905    }
906
907    #[test]
908    fn fixed_clock_monotonic_never_advances() {
909        // A clock whose wall time is pinned must not let elapsed time tick past,
910        // or a test that pins the clock would still see real durations.
911        let clock = FixedClock::at(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap());
912        let first = clock.monotonic();
913        std::thread::yield_now();
914        assert_eq!(first, MonotonicInstant::ORIGIN);
915        assert_eq!(clock.monotonic(), first);
916    }
917
918    #[test]
919    fn system_clock_monotonic_is_real_and_non_decreasing() {
920        let clock = SystemClock;
921        let first = clock.monotonic();
922        let second = clock.monotonic();
923        assert!(
924            second >= first,
925            "the real monotonic clock must never go backwards"
926        );
927        // And it is the same timeline the free helper reads.
928        assert!(monotonic_now() >= second);
929
930        // The default body must read a REAL clock. Without this, a body that
931        // simply returned `ORIGIN` would keep every other test in this file
932        // green while silently making production elapsed-time measurements
933        // always zero. Bounded spin rather than a sleep, so the test stays fast
934        // and never depends on scheduler behaviour.
935        let base = clock.monotonic();
936        let mut advanced = base;
937        for _ in 0..50_000_000u64 {
938            advanced = clock.monotonic();
939            if advanced > base {
940                break;
941            }
942        }
943        assert!(
944            advanced > base,
945            "ClockSource::monotonic's default body must advance with real time"
946        );
947    }
948
949    #[test]
950    fn a_custom_clock_keeps_compiling_and_gets_real_monotonic_by_default() {
951        // Backward-compatibility guard: `monotonic()` ships with a default body,
952        // so a downstream `impl ClockSource` that predates it still compiles and
953        // still reports real process-monotonic time. If someone ever removes the
954        // default body, this test stops compiling — which is the point.
955        #[derive(Debug)]
956        struct LegacyClock;
957        impl ClockSource for LegacyClock {
958            fn now(&self) -> DateTime<Utc> {
959                Utc.with_ymd_and_hms(1999, 12, 31, 23, 59, 59).unwrap()
960            }
961        }
962        let clock = LegacyClock;
963        let first = clock.monotonic();
964        assert!(clock.monotonic() >= first);
965    }
966
967    #[test]
968    fn a_backwards_wall_clock_cannot_produce_a_negative_elapsed() {
969        // A clock whose `now()` runs backwards (a crude NTP-jump model) must not
970        // be able to corrupt an elapsed measurement taken through the seam.
971        // `TickingClock` derives monotonic from its own start, and its
972        // subtraction saturates, so the worst case is zero — never a panic.
973        let start = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
974        let clock = TickingClock::starting_at(start);
975        clock.advance(Duration::from_secs(10));
976        let later = clock.monotonic();
977        let earlier = MonotonicInstant::from_origin_elapsed(Duration::from_secs(100));
978        assert_eq!(later.saturating_duration_since(earlier), Duration::ZERO);
979    }
980
981    #[test]
982    fn clock_unix_secs_exact_epoch() {
983        let epoch = Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap();
984        let clock = FixedClock::at(epoch);
985        assert_eq!(clock_unix_secs(&clock), 0);
986    }
987
988    #[test]
989    fn clock_unix_duration_zero_for_pre_epoch() {
990        // Chrono timestamps before the epoch should not underflow.
991        let pre_epoch = Utc.with_ymd_and_hms(1969, 12, 31, 23, 59, 59).unwrap();
992        let clock = FixedClock::at(pre_epoch);
993        assert_eq!(clock_unix_duration(&clock), std::time::Duration::ZERO);
994    }
995
996    #[test]
997    fn clock_unix_duration_exact_epoch_with_nanos() {
998        // Test exact epoch with some nanos to ensure the true branch logic is exercised
999        let epoch_with_nanos =
1000            Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap() + chrono::Duration::nanoseconds(100);
1001        let clock = FixedClock::at(epoch_with_nanos);
1002        assert_eq!(
1003            clock_unix_duration(&clock),
1004            std::time::Duration::from_nanos(100)
1005        );
1006    }
1007
1008    #[test]
1009    fn clock_unix_duration_post_epoch() {
1010        let post_epoch = Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 1).unwrap();
1011        let clock = FixedClock::at(post_epoch);
1012        assert_eq!(
1013            clock_unix_duration(&clock),
1014            std::time::Duration::from_secs(1)
1015        );
1016    }
1017
1018    #[test]
1019    fn ambient_clock_is_installed_nested_and_removed() {
1020        let pinned = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
1021        let later = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
1022        let outer = install_ambient(Arc::new(FixedClock::at(pinned)));
1023        assert_eq!(ambient_now(), pinned);
1024        let inner = install_ambient(Arc::new(FixedClock::at(later)));
1025        assert_eq!(ambient_now(), later, "the newest clock wins");
1026        drop(inner);
1027        assert_eq!(ambient_now(), pinned);
1028        drop(outer);
1029        assert!(ambient_now() > later, "the system clock again");
1030    }
1031
1032    #[test]
1033    fn a_later_timeline_starts_after_earlier_instants() {
1034        let epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
1035        let hour = std::time::Duration::from_secs(3600);
1036        let first = TickingClock::starting_at(epoch);
1037        let guard = install_ambient(Arc::new(first.clone()));
1038        let start = ambient_instant();
1039        first.advance(hour);
1040        let stored = ambient_instant();
1041        assert_eq!(stored.saturating_duration_since(start), hour);
1042        drop(guard);
1043
1044        // A second clock at the same reading starts after the first's hour.
1045        let second = TickingClock::starting_at(epoch);
1046        let _guard = install_ambient(Arc::new(second.clone()));
1047        let fresh = ambient_instant();
1048        assert!(fresh >= stored, "a new timeline does not go back");
1049        second.advance(hour);
1050        assert_eq!(ambient_instant().saturating_duration_since(fresh), hour);
1051    }
1052
1053    #[test]
1054    fn framework_code_spawns_blocking_work_through_the_ambient_helper() {
1055        // A plain `tokio::task::spawn_blocking` reads the system clock inside
1056        // a `Sim`. Clippy bans it only in the modules the determinism gate
1057        // covers, so this checks the rest of the crate (issue #2967).
1058        fn visit(dir: &std::path::Path, offenders: &mut Vec<String>) {
1059            for entry in std::fs::read_dir(dir).unwrap() {
1060                let path = entry.unwrap().path();
1061                if path.is_dir() {
1062                    visit(&path, offenders);
1063                } else if path.extension().is_some_and(|ext| ext == "rs")
1064                    && !path.ends_with("src/time.rs")
1065                    && std::fs::read_to_string(&path)
1066                        .unwrap()
1067                        .contains("task::spawn_blocking")
1068                {
1069                    offenders.push(path.display().to_string());
1070                }
1071            }
1072        }
1073        let mut offenders = Vec::new();
1074        visit(
1075            &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
1076            &mut offenders,
1077        );
1078        assert!(
1079            offenders.is_empty(),
1080            "use crate::time::spawn_blocking instead of tokio::task::spawn_blocking in {offenders:?}"
1081        );
1082    }
1083
1084    #[tokio::test]
1085    async fn spawn_blocking_reads_the_callers_ambient_clock() {
1086        let epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
1087        let clock = TickingClock::starting_at(epoch);
1088        let guard = install_ambient(Arc::new(clock.clone()));
1089        clock.advance(std::time::Duration::from_secs(3600));
1090        let here = (ambient_now(), ambient_instant());
1091        let there = spawn_blocking(|| (ambient_now(), ambient_instant()))
1092            .await
1093            .unwrap();
1094        assert_eq!(there, here, "the blocking thread reads the same clock");
1095        drop(guard);
1096
1097        // With no ambient clock, the blocking thread reads the system clock.
1098        let there = spawn_blocking(ambient_now).await.unwrap();
1099        assert!((Utc::now() - there).num_seconds().abs() < 5);
1100    }
1101
1102    #[test]
1103    fn an_outer_timeline_goes_on_after_a_nested_one() {
1104        let epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
1105        let hour = std::time::Duration::from_secs(3600);
1106        let outer = TickingClock::starting_at(epoch);
1107        let _outer = install_ambient(Arc::new(outer.clone()));
1108        let before = ambient_instant();
1109
1110        let inner = TickingClock::starting_at(epoch);
1111        let guard = install_ambient(Arc::new(inner.clone()));
1112        inner.advance(hour);
1113        let stored = ambient_instant();
1114        assert!(stored.saturating_duration_since(before) >= hour);
1115        drop(guard);
1116
1117        // Back on the outer clock, instants go on from the nested one's.
1118        let back = ambient_instant();
1119        assert!(back >= stored, "the outer timeline does not go back");
1120        outer.advance(hour);
1121        assert_eq!(ambient_instant().saturating_duration_since(back), hour);
1122    }
1123
1124    #[test]
1125    fn ambient_guard_dropped_on_another_thread_uninstalls() {
1126        let pinned = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
1127        let guard = install_ambient(Arc::new(FixedClock::at(pinned)));
1128        assert_eq!(ambient_now(), pinned);
1129        std::thread::spawn(move || drop(guard)).join().unwrap();
1130        assert_ne!(
1131            ambient_now(),
1132            pinned,
1133            "the entry is skipped once its guard drops"
1134        );
1135    }
1136}