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 times the suspended window
11//! on the plane's clock rather than its own, so a window isn't measured on one clock while
12//! the polls run on another. One trait, with tokio's clock in production, keeps that property
13//! for everything that reads time. The trait is re-exported at `control::Clock` and at
14//! `session::Clock`, the 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: the suspended window is a *duration*, and a wall clock that steps
28//! backward would reopen a closed one. [`Clock::unix_now`] is the wall reading, for the values
29//! that are dates rather than durations: a name record's timestamp, an exec id, the cost
30//! engine's today. Deriving one from the other would make a stepped wall clock move a
31//! 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            readings.elapsed += duration;
115            readings.unix += duration;
116        }
117
118        /// The current elapsed reading.
119        pub fn now(&self) -> Duration {
120            self.readings
121                .lock()
122                .unwrap_or_else(PoisonError::into_inner)
123                .elapsed
124        }
125
126        /// Sets the wall reading, leaving the monotonic one where it is.
127        pub fn set_unix_now(&self, unix: Duration) {
128            self.readings
129                .lock()
130                .unwrap_or_else(PoisonError::into_inner)
131                .unix = unix;
132        }
133    }
134
135    impl Clock for TestClock {
136        fn elapsed(&self) -> Duration {
137            self.now()
138        }
139
140        fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
141            self.advance(duration);
142            Box::pin(std::future::ready(()))
143        }
144
145        fn unix_now(&self) -> Duration {
146            self.readings
147                .lock()
148                .unwrap_or_else(PoisonError::into_inner)
149                .unix
150        }
151    }
152
153    /// A [`TestClock`] whose `sleep` also **yields** once.
154    ///
155    /// A `sleep` that only advanced would let a poll loop run every iteration inside one
156    /// `poll` of its future, so a `select!` racing it against an interrupt, or a second caller
157    /// sharing its fake, would never get a turn. Yielding returns `Pending` once per sleep,
158    /// which is that turn. Time still passes only when slept through.
159    #[derive(Debug, Default)]
160    pub struct YieldingClock {
161        inner: TestClock,
162    }
163
164    impl YieldingClock {
165        pub fn new() -> Self {
166            Self::default()
167        }
168    }
169
170    impl std::ops::Deref for YieldingClock {
171        type Target = TestClock;
172
173        fn deref(&self) -> &TestClock {
174            &self.inner
175        }
176    }
177
178    impl Clock for YieldingClock {
179        fn elapsed(&self) -> Duration {
180            self.inner.elapsed()
181        }
182
183        fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
184            self.inner.advance(duration);
185            Box::pin(tokio::task::yield_now())
186        }
187
188        fn unix_now(&self) -> Duration {
189            self.inner.unix_now()
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::testing::TestClock;
197    use super::*;
198
199    /// The provided `today_utc` reads the clock's wall time, not the host's.
200    ///
201    /// **Falsification**: make `today_utc` read `SystemTime::now()` and the date below is
202    /// the day the test runs instead.
203    #[test]
204    fn today_comes_from_the_clock_s_wall_reading() {
205        let clock = TestClock::new();
206        // 2024-02-29T12:00:00Z, a leap day, so an off-by-one in the day arithmetic shows.
207        clock.set_unix_now(Duration::from_secs(1_709_208_000));
208        assert_eq!(
209            clock.today_utc().expect("a representable date"),
210            CalendarDate::from_ymd(2024, 2, 29)
211        );
212    }
213
214    /// A reading past the domain's last date is an error, not a wrapped date.
215    #[test]
216    fn a_wall_reading_past_9999_is_refused() {
217        let clock = TestClock::new();
218        clock.set_unix_now(Duration::from_secs(253_402_300_800));
219        assert!(clock.today_utc().is_err());
220    }
221
222    /// Advancing moves both readings, so a test that sleeps through a day sees a new date.
223    #[test]
224    fn advancing_the_test_clock_moves_the_wall_reading_too() {
225        let clock = TestClock::new();
226        let (elapsed, unix) = (clock.elapsed(), clock.unix_now());
227        clock.advance(Duration::from_secs(86_400));
228        assert_eq!(clock.elapsed() - elapsed, Duration::from_secs(86_400));
229        assert_eq!(clock.unix_now() - unix, Duration::from_secs(86_400));
230    }
231}