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}