Skip to main content

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}