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}