spate_core/clock/tokio.rs
1//! Injectable time on the tokio timer: [`Clock`] reads
2//! [`tokio::time::Instant`] and sleeps until a deadline.
3//!
4//! [`SystemClock`] follows the runtime's clock, so paused tokio time applies
5//! to it. `TestClock` moves only when a test advances it, for code on a
6//! multi-thread runtime, where tokio time cannot be paused. A duration that
7//! is only measured on this timeline goes through [`Stopwatch`].
8
9use std::future::Future;
10use std::pin::Pin;
11use std::time::Duration;
12#[cfg(any(test, feature = "testing"))]
13use tokio::sync::watch;
14use tokio::time::Instant;
15
16/// A future that resolves when a [`Clock`] reaches some instant.
17///
18/// Boxed and `'static`: implementations must not borrow the clock, so a
19/// caller can hold the future across a `select!` arm whose body needs
20/// `&mut self`.
21pub type Sleep = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
22
23/// A monotonic clock on [`tokio::time::Instant`].
24pub trait Clock: Send + Sync + std::fmt::Debug {
25 /// The current instant on this clock's timeline.
26 fn now(&self) -> Instant;
27
28 /// Resolve once this clock reaches `deadline`.
29 ///
30 /// Returns immediately if `deadline` is already past. On a frozen
31 /// clock this parks until the test advances past `deadline`. It must
32 /// never fall back to real time, or the freeze leaks.
33 fn sleep_until(&self, deadline: Instant) -> Sleep;
34}
35
36/// The production clock, on the runtime's tokio time.
37#[derive(Debug, Default, Clone, Copy)]
38pub struct SystemClock;
39
40impl Clock for SystemClock {
41 fn now(&self) -> Instant {
42 Instant::now()
43 }
44
45 fn sleep_until(&self, deadline: Instant) -> Sleep {
46 Box::pin(tokio::time::sleep_until(deadline))
47 }
48}
49
50/// Measures how far a [`Clock`] moves between two reads.
51///
52/// It exposes no [`Instant`], so a measurement cannot feed a deadline. Pass
53/// the same clock to [`start`](Self::start) and [`elapsed`](Self::elapsed).
54#[derive(Clone, Copy, Debug)]
55pub struct Stopwatch {
56 started: Instant,
57}
58
59impl Stopwatch {
60 /// Starts measuring at `clock`'s current instant.
61 #[inline]
62 #[must_use]
63 pub fn start<C: Clock + ?Sized>(clock: &C) -> Self {
64 Self {
65 started: clock.now(),
66 }
67 }
68
69 /// Time `clock` has moved since [`start`](Self::start), zero if it reads
70 /// earlier.
71 #[inline]
72 #[must_use]
73 pub fn elapsed<C: Clock + ?Sized>(&self, clock: &C) -> Duration {
74 clock.now().saturating_duration_since(self.started)
75 }
76}
77
78/// A clock that only moves when a test moves it.
79///
80/// Wired into production code, it stops every timer drawn from it.
81#[cfg(any(test, feature = "testing"))]
82#[doc(hidden)]
83#[derive(Debug)]
84pub struct TestClock {
85 /// The instant this clock was created at. Fixed for its lifetime.
86 base: Instant,
87 /// Nanoseconds elapsed on this clock's timeline. The watch channel is
88 /// the single source of truth for "now" *and* the wakeup for parked
89 /// [`Clock::sleep_until`] futures, so the two can never disagree.
90 offset_nanos: watch::Sender<u64>,
91 /// Every deadline passed to [`Clock::sleep_until`], so a test can wait
92 /// until code under test has armed a timer.
93 slept_until: std::sync::Mutex<Vec<Instant>>,
94}
95
96#[cfg(any(test, feature = "testing"))]
97impl TestClock {
98 /// A clock frozen at construction. Build it after the runtime exists
99 /// so `tokio::time::Instant::now()` reads the runtime's clock.
100 #[must_use]
101 pub fn frozen() -> std::sync::Arc<TestClock> {
102 std::sync::Arc::new(TestClock {
103 base: Instant::now(),
104 offset_nanos: watch::Sender::new(0),
105 slept_until: std::sync::Mutex::new(Vec::new()),
106 })
107 }
108
109 /// Whether anything has called [`Clock::sleep_until`] for `deadline`.
110 #[must_use]
111 pub fn has_slept_until(&self, deadline: Instant) -> bool {
112 self.slept_until
113 .lock()
114 .expect("test clock lock")
115 .contains(&deadline)
116 }
117
118 /// Move the clock forward, waking everything parked on a deadline this
119 /// jump passes.
120 ///
121 /// One jump fires every deadline it passes at the same instant. Use
122 /// [`advance_stepped`](TestClock::advance_stepped) when code under test
123 /// must react between them.
124 pub fn advance(&self, by: Duration) {
125 let nanos = u64::try_from(by.as_nanos()).expect("test clock advance fits in u64 nanos");
126 self.offset_nanos.send_modify(|n| *n += nanos);
127 }
128
129 /// Advance by `total` in `step`-sized increments, running `between`
130 /// after each one.
131 ///
132 /// `between` is where the test lets the code under test react before
133 /// the next step. Panics if `step` is zero.
134 pub fn advance_stepped(&self, total: Duration, step: Duration, mut between: impl FnMut()) {
135 assert!(!step.is_zero(), "advance_stepped needs a non-zero step");
136 let mut moved = Duration::ZERO;
137 while moved < total {
138 let chunk = step.min(total - moved);
139 self.advance(chunk);
140 moved += chunk;
141 between();
142 }
143 }
144}
145
146#[cfg(any(test, feature = "testing"))]
147impl Clock for TestClock {
148 fn now(&self) -> Instant {
149 self.base + Duration::from_nanos(*self.offset_nanos.borrow())
150 }
151
152 fn sleep_until(&self, deadline: Instant) -> Sleep {
153 self.slept_until
154 .lock()
155 .expect("test clock lock")
156 .push(deadline);
157 let base = self.base;
158 let mut rx = self.offset_nanos.subscribe();
159 Box::pin(async move {
160 loop {
161 if base + Duration::from_nanos(*rx.borrow_and_update()) >= deadline {
162 return;
163 }
164 if rx.changed().await.is_err() {
165 // The clock is gone, so it can never reach `deadline`.
166 // Park rather than return: waking would fire a timer
167 // whose time never came.
168 std::future::pending::<()>().await;
169 }
170 }
171 })
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 /// A stopwatch reports exactly how far its clock moved, and nothing before
180 /// it moves.
181 #[test]
182 fn a_tokio_stopwatch_reports_the_time_its_clock_moved() {
183 let clock = TestClock::frozen();
184 let watch = Stopwatch::start(&*clock);
185 assert_eq!(watch.elapsed(&*clock), Duration::ZERO);
186 clock.advance(Duration::from_millis(250));
187 assert_eq!(watch.elapsed(&*clock), Duration::from_millis(250));
188 }
189}