Skip to main content

darkbio_clock/
lib.rs

1// clock-rs: virtual clock for testing blocking code
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7// Pull in the README as the package doc
8#![doc = include_str!("../README.md")]
9// Enable the experimental doc_cfg feature
10#![cfg_attr(docsrs, feature(doc_cfg))]
11// Allow excluding test code from coverage measurements on nightly
12#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
13// The crate only builds on safe synchronization and never needs unsafe
14#![forbid(unsafe_code)]
15
16pub mod sync;
17
18// Every internal lock comes from here, so loom can swap in its own when model
19// checking the waits
20mod primitives;
21
22#[cfg(feature = "crossbeam")]
23mod timers;
24
25/// The crossbeam-channel crate that the clock's timers use, for naming its
26/// types at the same version.
27#[cfg(feature = "crossbeam")]
28#[cfg_attr(docsrs, doc(cfg(feature = "crossbeam")))]
29pub use crossbeam_channel;
30
31// Test clocks exist only in tests, so a build without the test-clock feature
32// cannot stop its own time
33#[cfg(any(test, feature = "test-clock"))]
34mod paused;
35
36#[cfg(feature = "test-clock")]
37pub use paused::TestClock;
38#[cfg(all(test, not(feature = "test-clock")))]
39use paused::TestClock;
40
41use std::fmt;
42use std::sync::Arc;
43use std::time::{Duration, Instant, SystemTime};
44
45use primitives::{Condvar, Mutex, MutexGuard};
46
47/// A clock that reads real time, or a test's time that moves only on command.
48///
49/// Clones share one clock. Equality compares identity, so all real clocks are
50/// equal and a test clock equals only the handles of its own `TestClock`.
51#[derive(Clone)]
52pub struct Clock {
53    /// Shared state of a test clock, or nothing for the real clock.
54    #[cfg(any(test, feature = "test-clock"))]
55    paused: Option<Arc<paused::Paused>>,
56}
57
58impl Clock {
59    /// Returns the real clock, which reads the system's monotonic and wall times.
60    pub const fn real() -> Self {
61        Self {
62            #[cfg(any(test, feature = "test-clock"))]
63            paused: None,
64        }
65    }
66
67    /// Returns the clock's current monotonic time.
68    pub fn now(&self) -> Instant {
69        #[cfg(any(test, feature = "test-clock"))]
70        if let Some(paused) = &self.paused {
71            return paused.now();
72        }
73        Instant::now()
74    }
75
76    /// Returns the time since `since`, or zero if it is later than now.
77    pub fn elapsed(&self, since: Instant) -> Duration {
78        self.now().saturating_duration_since(since)
79    }
80
81    /// Returns the clock's current wall time.
82    pub fn system_time(&self) -> SystemTime {
83        #[cfg(any(test, feature = "test-clock"))]
84        if let Some(paused) = &self.paused {
85            return paused.system_time();
86        }
87        SystemTime::now()
88    }
89
90    /// Blocks the thread until `duration` has passed on this clock.
91    ///
92    /// On a test clock, only its advances end the sleep.
93    ///
94    /// # Panics
95    ///
96    /// Panics on a test clock if the deadline overflows [`Instant`].
97    pub fn sleep(&self, duration: Duration) {
98        #[cfg(any(test, feature = "test-clock"))]
99        if self.paused.is_some() {
100            self.sleep_until(self.now() + duration);
101            return;
102        }
103        std::thread::sleep(duration);
104    }
105
106    /// Blocks the thread until this clock reaches `deadline`, returning at once
107    /// if it already has.
108    ///
109    /// On a test clock, only its advances end the sleep.
110    pub fn sleep_until(&self, deadline: Instant) {
111        self.waiter().wait_until(Some(deadline), || None::<()>);
112    }
113
114    /// Returns the shared state's address, or nothing for the real clock.
115    fn identity(&self) -> Option<*const ()> {
116        #[cfg(any(test, feature = "test-clock"))]
117        if let Some(paused) = &self.paused {
118            return Some(Arc::as_ptr(paused).cast());
119        }
120        None
121    }
122
123    /// Creates a waiter that measures deadlines on this clock.
124    fn waiter(&self) -> Waiter {
125        // Register the signal before any wait can observe the clock
126        let signal = Arc::new(Signal::default());
127        #[cfg(any(test, feature = "test-clock"))]
128        if let Some(paused) = &self.paused {
129            paused.register(&signal);
130        }
131
132        // Keep the registration alive for the waiter's lifetime
133        Waiter {
134            clock: self.clone(),
135            signal,
136        }
137    }
138}
139
140// A production clock carries no state, so passing one around costs nothing
141#[cfg(not(any(test, feature = "test-clock")))]
142const _: () = assert!(size_of::<Clock>() == 0);
143
144impl PartialEq for Clock {
145    /// Compares identity, with all real clocks equal to each other.
146    fn eq(&self, other: &Self) -> bool {
147        self.identity() == other.identity()
148    }
149}
150
151impl Eq for Clock {}
152
153impl fmt::Debug for Clock {
154    /// Shows whether the clock is paused, its advance and its wall time.
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        let mut clock = f.debug_struct("Clock");
157        #[cfg(any(test, feature = "test-clock"))]
158        if let Some(paused) = &self.paused {
159            return clock
160                .field("paused", &true)
161                .field("advanced", &paused.advanced())
162                .field("system_time", &paused.system_time())
163                .finish();
164        }
165        clock.field("paused", &false).finish()
166    }
167}
168
169/// Blocks threads until a condition holds or a deadline passes on a clock.
170struct Waiter {
171    /// Clock that decides when deadlines pass.
172    clock: Clock,
173    /// Wakeups registered with a test clock until this waiter drops.
174    signal: Arc<Signal>,
175}
176
177impl Waiter {
178    /// Blocks until `ready` returns a value or the clock reaches `deadline`.
179    ///
180    /// A ready value wins over an expired deadline. Without a deadline, only
181    /// a ready value ends the wait. The callback runs without crate locks held.
182    fn wait_until<T>(
183        &self,
184        deadline: Option<Instant>,
185        mut ready: impl FnMut() -> Option<T>,
186    ) -> Option<T> {
187        // Pick the real timer once, so the test seam reports what every park uses
188        let timer = self.timer(deadline);
189
190        // Check and park in turns, until a value arrives or the deadline passes
191        loop {
192            // Take the generation before checking, so a change after the check cuts the park short
193            let seen = self.signal.generation();
194            if let Some(value) = ready() {
195                return Some(value);
196            }
197            if deadline.is_some_and(|deadline| self.clock.now() >= deadline) {
198                return None;
199            }
200            drop(self.park(self.signal.lock(), seen, deadline, timer));
201        }
202    }
203
204    /// Returns the deadline as a real timer on a real clock, and no timer on a
205    /// test clock, whose advances end its waits.
206    fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
207        #[cfg(any(test, feature = "test-clock"))]
208        if self.clock.paused.is_some() {
209            return None;
210        }
211        deadline
212    }
213
214    /// Parks until the generation moves on from `seen` or the clock reaches
215    /// `deadline`, and returns the state locked again.
216    ///
217    /// Only a real clock's park uses a real timer. In tests, one-shot hooks run
218    /// before its first wait and before a wait after a spurious wakeup, with
219    /// the signal unlocked.
220    fn park<'a>(
221        &'a self,
222        mut state: MutexGuard<'a, SignalState>,
223        seen: u64,
224        deadline: Option<Instant>,
225        timer: Option<Instant>,
226    ) -> MutexGuard<'a, SignalState> {
227        // Count the park once, keeping it counted through spurious wakeups
228        #[cfg(any(test, feature = "test-clock"))]
229        let mut blocked = None;
230
231        loop {
232            // Stop once the generation moves or the clock reaches the deadline
233            let now = self.clock.now();
234            if state.generation != seen || deadline.is_some_and(|deadline| now >= deadline) {
235                break;
236            }
237
238            // Let tests act before a wait, then check again for any change they made
239            #[cfg(test)]
240            if let Some(hook) = self
241                .clock
242                .paused
243                .as_ref()
244                .and_then(|paused| paused.take_hook(blocked.is_some()))
245            {
246                drop(state);
247                hook(timer);
248                state = self.signal.lock();
249                continue;
250            }
251
252            // Count the park while the signal is locked, so that an advance seen after
253            // the count still wakes it
254            #[cfg(any(test, feature = "test-clock"))]
255            if blocked.is_none() {
256                blocked = self
257                    .clock
258                    .paused
259                    .as_ref()
260                    .map(|paused| paused.block(deadline));
261            }
262            #[cfg(test)]
263            {
264                state.parks += 1;
265                self.signal.parked.notify_all();
266            }
267
268            // Wait for a wakeup or the real timer, leaving expiry to the check above
269            state = match timer {
270                None => self
271                    .signal
272                    .changed
273                    .wait(state)
274                    .expect("waiter signal not poisoned"),
275                Some(timer) => {
276                    self.signal
277                        .changed
278                        .wait_timeout(state, timer - now)
279                        .expect("waiter signal not poisoned")
280                        .0
281                }
282            };
283        }
284
285        // Uncount the park before the caller acts on its wakeup
286        #[cfg(any(test, feature = "test-clock"))]
287        drop(blocked);
288        state
289    }
290}
291
292#[cfg(any(test, feature = "test-clock"))]
293impl Drop for Waiter {
294    /// Removes the waiter's registration as soon as it is no longer live.
295    fn drop(&mut self) {
296        if let Some(paused) = &self.clock.paused {
297            paused.unregister(&self.signal);
298        }
299    }
300}
301
302impl fmt::Debug for Waiter {
303    /// Shows the clock, never the wakeup bookkeeping.
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        f.debug_struct("Waiter")
306            .field("clock", &self.clock)
307            .finish_non_exhaustive()
308    }
309}
310
311/// Wakes threads through a generation read before each condition check.
312#[derive(Default)]
313struct Signal {
314    /// Counters and the test park count, guarded together.
315    state: Mutex<SignalState>,
316    /// Wakes parked threads when the generation moves.
317    changed: Condvar,
318    /// Reports new parks to tests, without waking parked threads.
319    #[cfg(test)]
320    parked: Condvar,
321}
322
323/// Mutable part of a signal.
324#[derive(Default)]
325struct SignalState {
326    /// Number of notifications and clock advances, wrapping on overflow.
327    generation: u64,
328    /// Number of notifications alone, which end condvar waits, wrapping on overflow.
329    notifications: u64,
330    /// Total parks that found the generation unchanged, for test synchronization.
331    #[cfg(test)]
332    parks: usize,
333}
334
335impl Signal {
336    /// Locks the state, which no code panics under.
337    fn lock(&self) -> MutexGuard<'_, SignalState> {
338        self.state.lock().expect("waiter signal not poisoned")
339    }
340
341    /// Returns the current generation, to compare against when parking.
342    fn generation(&self) -> u64 {
343        self.lock().generation
344    }
345
346    /// Counts a notification and wakes one parked thread.
347    ///
348    /// Every waiting thread sees the count, so another one may return too,
349    /// as a spurious wakeup, the next time it wakes.
350    fn notify_one(&self) {
351        let mut state = self.lock();
352        state.generation = state.generation.wrapping_add(1);
353        state.notifications = state.notifications.wrapping_add(1);
354        self.changed.notify_one();
355    }
356
357    /// Counts a notification and wakes every parked thread.
358    fn notify_all(&self) {
359        let mut state = self.lock();
360        state.generation = state.generation.wrapping_add(1);
361        state.notifications = state.notifications.wrapping_add(1);
362        self.changed.notify_all();
363    }
364
365    /// Wakes every parked thread to recheck the time, without counting a notification.
366    #[cfg(any(test, feature = "test-clock"))]
367    fn advance(&self) {
368        let mut state = self.lock();
369        state.generation = state.generation.wrapping_add(1);
370        self.changed.notify_all();
371    }
372}
373
374// The tests live in src/tests, apart from the code they check
375#[cfg(test)]
376#[cfg_attr(coverage_nightly, coverage(off))]
377mod tests;