Skip to main content

microvms_app/
clock.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The one clock port: monotonic time, sleeping, and the wall time since the epoch.
3//!
4//! # One trait rather than one per module
5//!
6//! The control plane and the session each had a `Clock` of their own, over different time
7//! sources: the control plane's read `std::time::Instant` and the session's read tokio's.
8//! Under a deterministic simulator those disagree. tokio's clock is virtual there and `std`'s
9//! isn't, so a control-plane wait whose sleeps advanced virtual time measured its deadline
10//! on the host's real time and never reached it. `Sandbox` already waits on the plane's clock
11//! rather than one of its own, so a deadline isn't measured on one clock while the polls sleep
12//! on another. One trait, with tokio's clock in production, keeps that property for everything
13//! that reads time. The trait is re-exported at `control::Clock` and at `session::Clock`, the
14//! paths each half had.
15//!
16//! # Why a trait rather than `tokio::time::pause`
17//!
18//! The waits are driven by *elapsed* comparisons against a deadline and a stall grace, and a
19//! test has to be able to say "now 300 seconds have passed" between two polls of a fake
20//! control plane. A paused tokio clock can do that, but only for code that sleeps on tokio,
21//! and it makes every test in a file share one global clock state. A [`Clock`] parameter
22//! makes the dependency visible in the signature.
23//!
24//! # Monotonic and wall time are separate readings
25//!
26//! [`Clock::elapsed`] is monotonic, the same reasoning the Python client's injectable
27//! `time.monotonic` carried: a wait's deadline and a proxy token's age are *durations*, and a
28//! wall clock that steps backward would hold a passed deadline open. [`Clock::unix_now`] is the
29//! wall reading, for the values that are dates rather than durations: a name record's
30//! timestamp, an exec id, the cost engine's today. Deriving one from the other would make a
31//! stepped wall clock move a deadline.
32
33use std::fmt;
34use std::future::Future;
35use std::pin::Pin;
36use std::time::Duration;
37
38use crate::cost::CalendarDate;
39use crate::error::Error;
40
41/// Time, injectable so a wait or a refresh boundary is a test rather than a wait.
42pub trait Clock: Send + Sync + fmt::Debug {
43    /// Monotonic time since this clock was created. Never decreases.
44    fn elapsed(&self) -> Duration;
45
46    /// Sleeps for `duration`.
47    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
48
49    /// Wall time since the Unix epoch.
50    ///
51    /// Zero if the host clock is set before 1970, which is a machine whose dates are already
52    /// meaningless; a reading that can't be negative is what every caller wants.
53    fn unix_now(&self) -> Duration;
54
55    /// Today, UTC, for the cost engine's dates.
56    ///
57    /// Fails only for a wall reading past 9999-12-31, the last date the domain represents.
58    fn today_utc(&self) -> Result<CalendarDate, Error> {
59        CalendarDate::from_unix_secs(self.unix_now().as_secs())
60    }
61}
62
63#[cfg(any(test, feature = "test-support"))]
64#[cfg_attr(test, mutants::skip)] // a test double; see .cargo/mutants.toml
65pub(crate) mod testing {
66    //! The clocks the tests share, so the control-plane tests, the session tests and the
67    //! CLI's guards don't each keep a near-copy that can drift.
68
69    use std::sync::{Mutex, PoisonError};
70
71    use super::*;
72
73    /// 2026-01-01T00:00:00Z. A fixed, plausible wall time, so a test's dates don't depend on
74    /// the day it runs and don't read as the epoch fallback.
75    const START_UNIX_SECS: u64 = 1_767_225_600;
76
77    #[derive(Debug)]
78    struct Readings {
79        elapsed: Duration,
80        unix: Duration,
81    }
82
83    /// A clock a test drives by hand.
84    ///
85    /// `sleep` **advances** it rather than waiting, so a 45-minute build wait runs instantly
86    /// and the code under test still sees time pass exactly as it would have. That is what
87    /// makes the TRAP-2 stall test possible without a four-minute test, and a sixty-minute
88    /// token boundary costs no wall time and no simulator. The wall reading moves with every
89    /// advance, the way a real clock's does.
90    #[derive(Debug)]
91    pub struct TestClock {
92        readings: Mutex<Readings>,
93    }
94
95    impl Default for TestClock {
96        fn default() -> Self {
97            Self {
98                readings: Mutex::new(Readings {
99                    elapsed: Duration::ZERO,
100                    unix: Duration::from_secs(START_UNIX_SECS),
101                }),
102            }
103        }
104    }
105
106    impl TestClock {
107        pub fn new() -> Self {
108            Self::default()
109        }
110
111        /// Moves the clock forward by `duration`, as if a sleep had happened.
112        pub fn advance(&self, duration: Duration) {
113            let mut readings = self.readings.lock().unwrap_or_else(PoisonError::into_inner);
114            // Deadline-boundary tests can advance to MAX; a later sleep must remain
115            // observable without overflowing the test clock before an assertion.
116            readings.elapsed = readings.elapsed.saturating_add(duration);
117            readings.unix = readings.unix.saturating_add(duration);
118        }
119
120        /// The current elapsed reading.
121        pub fn now(&self) -> Duration {
122            self.readings
123                .lock()
124                .unwrap_or_else(PoisonError::into_inner)
125                .elapsed
126        }
127
128        /// Sets the wall reading, leaving the monotonic one where it is.
129        pub fn set_unix_now(&self, unix: Duration) {
130            self.readings
131                .lock()
132                .unwrap_or_else(PoisonError::into_inner)
133                .unix = unix;
134        }
135    }
136
137    impl Clock for TestClock {
138        fn elapsed(&self) -> Duration {
139            self.now()
140        }
141
142        fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
143            self.advance(duration);
144            Box::pin(std::future::ready(()))
145        }
146
147        fn unix_now(&self) -> Duration {
148            self.readings
149                .lock()
150                .unwrap_or_else(PoisonError::into_inner)
151                .unix
152        }
153    }
154
155    /// A [`TestClock`] whose `sleep` also **yields** once.
156    ///
157    /// A `sleep` that only advanced would let a poll loop run every iteration inside one
158    /// `poll` of its future, so a `select!` racing it against an interrupt, or a second caller
159    /// sharing its fake, would never get a turn. Yielding returns `Pending` once per sleep,
160    /// which is that turn. Time still passes only when slept through.
161    #[derive(Debug, Default)]
162    pub struct YieldingClock {
163        inner: TestClock,
164    }
165
166    impl YieldingClock {
167        pub fn new() -> Self {
168            Self::default()
169        }
170    }
171
172    impl std::ops::Deref for YieldingClock {
173        type Target = TestClock;
174
175        fn deref(&self) -> &TestClock {
176            &self.inner
177        }
178    }
179
180    impl Clock for YieldingClock {
181        fn elapsed(&self) -> Duration {
182            self.inner.elapsed()
183        }
184
185        fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
186            self.inner.advance(duration);
187            Box::pin(tokio::task::yield_now())
188        }
189
190        fn unix_now(&self) -> Duration {
191            self.inner.unix_now()
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::testing::TestClock;
199    use super::*;
200
201    /// The provided `today_utc` reads the clock's wall time, not the host's.
202    ///
203    /// **Falsification**: make `today_utc` read `SystemTime::now()` and the date below is
204    /// the day the test runs instead.
205    #[test]
206    fn today_comes_from_the_clock_s_wall_reading() {
207        let clock = TestClock::new();
208        // 2024-02-29T12:00:00Z, a leap day, so an off-by-one in the day arithmetic shows.
209        clock.set_unix_now(Duration::from_secs(1_709_208_000));
210        assert_eq!(
211            clock.today_utc().expect("a representable date"),
212            CalendarDate::from_ymd(2024, 2, 29)
213        );
214    }
215
216    /// A reading past the domain's last date is an error, not a wrapped date.
217    #[test]
218    fn a_wall_reading_past_9999_is_refused() {
219        let clock = TestClock::new();
220        clock.set_unix_now(Duration::from_secs(253_402_300_800));
221        assert!(clock.today_utc().is_err());
222    }
223
224    /// Advancing moves both readings, so a test that sleeps through a day sees a new date.
225    #[test]
226    fn advancing_the_test_clock_moves_the_wall_reading_too() {
227        let clock = TestClock::new();
228        let (elapsed, unix) = (clock.elapsed(), clock.unix_now());
229        clock.advance(Duration::from_secs(86_400));
230        assert_eq!(clock.elapsed() - elapsed, Duration::from_secs(86_400));
231        assert_eq!(clock.unix_now() - unix, Duration::from_secs(86_400));
232    }
233}