Skip to main content

spate_core/
clock.rs

1//! Injectable time, so a test controls the clock that time-dependent code
2//! reads.
3//!
4//! Code whose behaviour depends on time reads it from an injected clock:
5//! [`Clock`] on OS threads, and [`tokio::Clock`] on the tokio runtime, whose
6//! deadlines run on the tokio timer. A read that must stay on the real clock
7//! names [`SystemClock`] at the site and says why. A duration that is only
8//! measured goes through the `Stopwatch` of the clock module the code reads,
9//! [`Stopwatch`] or [`tokio::Stopwatch`], and a wall-clock timestamp through
10//! [`wall_now`].
11
12pub mod tokio;
13
14#[cfg(any(test, feature = "testing"))]
15use std::sync::{
16    Arc,
17    atomic::{AtomicU64, Ordering},
18};
19use std::time::{Duration, Instant, SystemTime};
20
21/// A monotonic time source.
22pub trait Clock {
23    /// Current monotonic instant.
24    fn now(&self) -> Instant;
25}
26
27/// A clock shared by the types that hold one.
28///
29/// `RefUnwindSafe`, so a public type holding it stays `UnwindSafe` and
30/// `RefUnwindSafe`.
31#[cfg(not(loom))]
32pub type SharedClock = std::sync::Arc<dyn Clock + Send + Sync + std::panic::RefUnwindSafe>;
33
34/// A [`SharedClock`] that implements `Debug`, which [`Clock`] does not
35/// require, so a type that holds one can derive it.
36#[cfg(not(loom))]
37#[derive(Clone)]
38pub struct SyncClock(SharedClock);
39
40#[cfg(not(loom))]
41impl SyncClock {
42    /// Holds `clock`.
43    #[must_use]
44    pub fn new(clock: SharedClock) -> Self {
45        SyncClock(clock)
46    }
47
48    /// Holds the production clock, [`SystemClock`].
49    #[must_use]
50    pub fn system() -> Self {
51        SyncClock(std::sync::Arc::new(SystemClock))
52    }
53}
54
55#[cfg(not(loom))]
56impl std::ops::Deref for SyncClock {
57    type Target = dyn Clock + Send + Sync + std::panic::RefUnwindSafe;
58
59    fn deref(&self) -> &Self::Target {
60        &*self.0
61    }
62}
63
64#[cfg(not(loom))]
65impl std::fmt::Debug for SyncClock {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.write_str("SyncClock")
68    }
69}
70
71/// The production clock, over [`Instant::now`].
72#[derive(Clone, Copy, Debug, Default)]
73pub struct SystemClock;
74
75impl Clock for SystemClock {
76    #[inline]
77    fn now(&self) -> Instant {
78        Instant::now()
79    }
80}
81
82/// Measures how far a [`Clock`] moves between two reads.
83///
84/// It exposes no [`Instant`], so a measurement cannot feed a deadline. Pass
85/// the same clock to [`start`](Self::start) and [`elapsed`](Self::elapsed).
86#[derive(Clone, Copy, Debug)]
87pub struct Stopwatch {
88    started: Instant,
89}
90
91impl Stopwatch {
92    /// Starts measuring at `clock`'s current instant.
93    #[inline]
94    #[must_use]
95    pub fn start<C: Clock + ?Sized>(clock: &C) -> Self {
96        Self {
97            started: clock.now(),
98        }
99    }
100
101    /// Time `clock` has moved since [`start`](Self::start), zero if it reads
102    /// earlier.
103    #[inline]
104    #[must_use]
105    pub fn elapsed<C: Clock + ?Sized>(&self, clock: &C) -> Duration {
106        clock.now().saturating_duration_since(self.started)
107    }
108}
109
110/// The system's wall-clock time.
111///
112/// For timestamps and identifiers only: wall-clock time can step backwards,
113/// so it never feeds a deadline.
114#[must_use]
115pub fn wall_now() -> SystemTime {
116    SystemTime::now()
117}
118
119/// A clock that only moves when a test moves it.
120#[cfg(any(test, feature = "testing"))]
121#[doc(hidden)]
122#[derive(Debug)]
123pub struct TestClock {
124    base: Instant,
125    offset_nanos: AtomicU64,
126}
127
128#[cfg(any(test, feature = "testing"))]
129impl TestClock {
130    /// A clock frozen at construction.
131    #[must_use]
132    pub fn frozen() -> Arc<TestClock> {
133        Arc::new(TestClock {
134            base: Instant::now(),
135            offset_nanos: AtomicU64::new(0),
136        })
137    }
138
139    /// Move the clock forward.
140    pub fn advance(&self, by: Duration) {
141        let nanos = u64::try_from(by.as_nanos()).expect("test clock advance fits in u64 nanos");
142        self.offset_nanos.fetch_add(nanos, Ordering::Relaxed);
143    }
144
145    /// Advance by `total` in `step`-sized increments, running `between`
146    /// after each one.
147    ///
148    /// `between` is where the test lets the code under test react before
149    /// the next step. Panics if `step` is zero.
150    pub fn advance_stepped(&self, total: Duration, step: Duration, mut between: impl FnMut()) {
151        assert!(!step.is_zero(), "advance_stepped needs a non-zero step");
152        let mut moved = Duration::ZERO;
153        while moved < total {
154            let chunk = step.min(total - moved);
155            self.advance(chunk);
156            moved += chunk;
157            between();
158        }
159    }
160}
161
162#[cfg(any(test, feature = "testing"))]
163impl Clock for TestClock {
164    fn now(&self) -> Instant {
165        self.base + Duration::from_nanos(self.offset_nanos.load(Ordering::Relaxed))
166    }
167}
168
169#[cfg(any(test, feature = "testing"))]
170impl Clock for &TestClock {
171    fn now(&self) -> Instant {
172        (**self).now()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    /// Pins that a holder's derived `Debug` names the clock field without
181    /// the trait object behind it.
182    #[cfg(not(loom))]
183    #[test]
184    fn a_sync_clock_debugs_as_its_name() {
185        assert_eq!(format!("{:?}", SyncClock::system()), "SyncClock");
186    }
187
188    /// `advance_stepped` lands exactly on `total`, with a short last step,
189    /// and runs `between` once per step.
190    #[test]
191    fn advance_stepped_reaches_total_in_steps() {
192        let clock = TestClock::frozen();
193        let start = clock.now();
194        let mut seen = Vec::new();
195        clock.advance_stepped(Duration::from_millis(25), Duration::from_millis(10), || {
196            seen.push(clock.now() - start);
197        });
198        assert_eq!(
199            seen,
200            [10, 20, 25].map(Duration::from_millis),
201            "one reading per step, the last one short"
202        );
203    }
204
205    /// A stopwatch reports exactly how far its clock moved.
206    #[test]
207    fn a_stopwatch_reports_the_time_its_clock_moved() {
208        let clock = TestClock::frozen();
209        let watch = Stopwatch::start(&*clock);
210        clock.advance(Duration::from_millis(250));
211        assert_eq!(watch.elapsed(&*clock), Duration::from_millis(250));
212    }
213
214    #[test]
215    #[should_panic(expected = "non-zero step")]
216    fn advance_stepped_rejects_a_zero_step() {
217        TestClock::frozen().advance_stepped(Duration::from_secs(1), Duration::ZERO, || {});
218    }
219}