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.
7
8use std::future::Future;
9use std::pin::Pin;
10// Only `TestClock` needs `Duration`; the trait and `SystemClock` do not.
11#[cfg(any(test, feature = "testing"))]
12use std::time::Duration;
13use tokio::time::Instant;
14
15/// A future that resolves when a [`Clock`] reaches some instant.
16///
17/// Boxed and `'static`: implementations must not borrow the clock, so a
18/// caller can hold the future across a `select!` arm whose body needs
19/// `&mut self`.
20pub type Sleep = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
21
22/// A monotonic clock on [`tokio::time::Instant`].
23pub trait Clock: Send + Sync + std::fmt::Debug {
24    /// The current instant on this clock's timeline.
25    fn now(&self) -> Instant;
26
27    /// Resolve once this clock reaches `deadline`.
28    ///
29    /// Returns immediately if `deadline` is already past. On a frozen
30    /// clock this parks until the test advances past `deadline`. It must
31    /// never fall back to real time, or the freeze leaks.
32    fn sleep_until(&self, deadline: Instant) -> Sleep;
33}
34
35/// The production clock, on the runtime's tokio time.
36#[derive(Debug, Default, Clone, Copy)]
37pub struct SystemClock;
38
39impl Clock for SystemClock {
40    fn now(&self) -> Instant {
41        Instant::now()
42    }
43
44    fn sleep_until(&self, deadline: Instant) -> Sleep {
45        Box::pin(tokio::time::sleep_until(deadline))
46    }
47}
48
49/// A clock that only moves when a test moves it.
50///
51/// Wired into production code, it stops every timer drawn from it.
52#[cfg(any(test, feature = "testing"))]
53#[doc(hidden)]
54#[derive(Debug)]
55pub struct TestClock {
56    /// The instant this clock was created at. Fixed for its lifetime.
57    base: Instant,
58    /// Nanoseconds elapsed on this clock's timeline. The watch channel is
59    /// the single source of truth for "now" *and* the wakeup for parked
60    /// [`Clock::sleep_until`] futures, so the two can never disagree.
61    offset_nanos: tokio::sync::watch::Sender<u64>,
62}
63
64#[cfg(any(test, feature = "testing"))]
65impl TestClock {
66    /// A clock frozen at construction. Build it after the runtime exists
67    /// so `tokio::time::Instant::now()` reads the runtime's clock.
68    #[must_use]
69    pub fn frozen() -> std::sync::Arc<TestClock> {
70        std::sync::Arc::new(TestClock {
71            base: Instant::now(),
72            offset_nanos: tokio::sync::watch::Sender::new(0),
73        })
74    }
75
76    /// Move the clock forward, waking everything parked on a deadline this
77    /// jump passes.
78    ///
79    /// One jump fires every deadline it passes at the same instant. Use
80    /// [`advance_stepped`](TestClock::advance_stepped) when code under test
81    /// must react between them.
82    pub fn advance(&self, by: Duration) {
83        let nanos = u64::try_from(by.as_nanos()).expect("test clock advance fits in u64 nanos");
84        self.offset_nanos.send_modify(|n| *n += nanos);
85    }
86
87    /// Advance by `total` in `step`-sized increments, running `between`
88    /// after each one.
89    ///
90    /// `between` is where the test lets the code under test react before
91    /// the next step. Panics if `step` is zero.
92    pub fn advance_stepped(&self, total: Duration, step: Duration, mut between: impl FnMut()) {
93        assert!(!step.is_zero(), "advance_stepped needs a non-zero step");
94        let mut moved = Duration::ZERO;
95        while moved < total {
96            let chunk = step.min(total - moved);
97            self.advance(chunk);
98            moved += chunk;
99            between();
100        }
101    }
102}
103
104#[cfg(any(test, feature = "testing"))]
105impl Clock for TestClock {
106    fn now(&self) -> Instant {
107        self.base + Duration::from_nanos(*self.offset_nanos.borrow())
108    }
109
110    fn sleep_until(&self, deadline: Instant) -> Sleep {
111        let base = self.base;
112        let mut rx = self.offset_nanos.subscribe();
113        Box::pin(async move {
114            loop {
115                if base + Duration::from_nanos(*rx.borrow_and_update()) >= deadline {
116                    return;
117                }
118                if rx.changed().await.is_err() {
119                    // The clock is gone, so it can never reach `deadline`.
120                    // Park rather than return: waking would fire a timer
121                    // whose time never came.
122                    std::future::pending::<()>().await;
123                }
124            }
125        })
126    }
127}