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 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}