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::collections::VecDeque;
42use std::fmt;
43use std::sync::Arc;
44#[cfg(test)]
45use std::sync::atomic::{AtomicUsize, Ordering};
46use std::time::{Duration, Instant, SystemTime};
47
48use primitives::{Condvar, Mutex, MutexGuard};
49
50/// Longest real wait handed to the OS in one call, since Windows waits forever
51/// on timeouts of about 49.7 days or more.
52const MAX_REAL_WAIT: Duration = Duration::from_secs(24 * 60 * 60);
53
54/// A clock that reads real time, or a test's time that moves only on command.
55///
56/// Clones share one clock. Equality compares identity, so all real clocks are
57/// equal and a test clock equals only the handles of its own `TestClock`.
58///
59/// It cannot be built from a struct literal and it has a destructor in every
60/// build, so code that compiles without `test-clock` compiles with it too.
61#[derive(Clone)]
62#[non_exhaustive]
63pub struct Clock {
64    /// Shared state of a test clock, or nothing for the real clock.
65    #[cfg(any(test, feature = "test-clock"))]
66    paused: Option<Arc<paused::Paused>>,
67}
68
69impl Clock {
70    /// Returns the real clock, which reads the system's monotonic and wall times.
71    pub const fn real() -> Self {
72        Self {
73            #[cfg(any(test, feature = "test-clock"))]
74            paused: None,
75        }
76    }
77
78    /// Returns the clock's current monotonic time.
79    #[must_use]
80    pub fn now(&self) -> Instant {
81        #[cfg(any(test, feature = "test-clock"))]
82        if let Some(paused) = &self.paused {
83            return paused.now();
84        }
85        Instant::now()
86    }
87
88    /// Returns the time since `since`, or zero if it is later than now.
89    #[must_use]
90    pub fn elapsed(&self, since: Instant) -> Duration {
91        self.now().saturating_duration_since(since)
92    }
93
94    /// Returns the clock's current wall time.
95    #[must_use]
96    pub fn system_time(&self) -> SystemTime {
97        #[cfg(any(test, feature = "test-clock"))]
98        if let Some(paused) = &self.paused {
99            return paused.system_time();
100        }
101        SystemTime::now()
102    }
103
104    /// Blocks the thread until `duration` has passed on this clock.
105    ///
106    /// On a test clock, only its advances end the sleep.
107    ///
108    /// # Panics
109    ///
110    /// Panics on a test clock if the deadline overflows [`Instant`].
111    pub fn sleep(&self, duration: Duration) {
112        // A test clock's sleep waits for the deadline this duration gives
113        #[cfg(any(test, feature = "test-clock"))]
114        if self.paused.is_some() {
115            self.sleep_until(self.now() + duration);
116            return;
117        }
118
119        // A real sleep waits in capped slices, rechecking the time slept after each
120        let start = Instant::now();
121        loop {
122            let elapsed = start.elapsed();
123            if elapsed >= duration {
124                return;
125            }
126            std::thread::sleep((duration - elapsed).min(MAX_REAL_WAIT));
127        }
128    }
129
130    /// Blocks the thread until this clock reaches `deadline`, returning at once
131    /// if it already has.
132    ///
133    /// On a test clock, only its advances end the sleep.
134    pub fn sleep_until(&self, deadline: Instant) {
135        // A test clock's sleep parks until an advance reaches its deadline
136        #[cfg(any(test, feature = "test-clock"))]
137        if self.paused.is_some() {
138            self.waiter().wait(Some(deadline));
139            return;
140        }
141
142        // A real sleep waits in capped slices, rechecking the deadline after each
143        loop {
144            let now = Instant::now();
145            if now >= deadline {
146                return;
147            }
148            std::thread::sleep((deadline - now).min(MAX_REAL_WAIT));
149        }
150    }
151
152    /// Returns the shared state's address, or nothing for the real clock.
153    fn identity(&self) -> Option<*const ()> {
154        #[cfg(any(test, feature = "test-clock"))]
155        if let Some(paused) = &self.paused {
156            return Some(Arc::as_ptr(paused).cast());
157        }
158        None
159    }
160
161    /// Creates a waiter that measures deadlines on this clock.
162    fn waiter(&self) -> Waiter {
163        Waiter {
164            clock: self.clone(),
165            signal: Arc::new(Signal::default()),
166        }
167    }
168}
169
170// A production clock carries no state, so passing one around costs nothing
171#[cfg(not(any(test, feature = "test-clock")))]
172const _: () = assert!(size_of::<Clock>() == 0);
173
174// A production clock still has a destructor, as a test build's does
175#[cfg(not(any(test, feature = "test-clock")))]
176const _: () = assert!(std::mem::needs_drop::<Clock>());
177
178impl Drop for Clock {
179    /// Does nothing, but gives every build a destructor, so enabling
180    /// `test-clock` does not change which const contexts accept a clock.
181    fn drop(&mut self) {}
182}
183
184impl PartialEq for Clock {
185    /// Compares identity, with all real clocks equal to each other.
186    fn eq(&self, other: &Self) -> bool {
187        self.identity() == other.identity()
188    }
189}
190
191impl Eq for Clock {}
192
193impl fmt::Debug for Clock {
194    /// Shows whether the clock is paused, its advance and its wall time.
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        // Read both times under one lock before calling the formatter's writer
197        #[cfg(any(test, feature = "test-clock"))]
198        let snapshot = self.paused.as_ref().map(|paused| paused.snapshot());
199
200        // Format without the clock lock, since the writer may read this clock
201        let mut clock = f.debug_struct("Clock");
202        #[cfg(any(test, feature = "test-clock"))]
203        if let Some((advanced, system_time)) = snapshot {
204            return clock
205                .field("paused", &true)
206                .field("advanced", &advanced)
207                .field("system_time", &system_time)
208                .finish();
209        }
210        clock.field("paused", &false).finish()
211    }
212}
213
214/// Blocks threads until notified or a deadline passes on a clock.
215struct Waiter {
216    /// Clock that decides when deadlines pass.
217    clock: Clock,
218    /// Notifications for this waiter's waits, and the wakeups that deliver them.
219    signal: Arc<Signal>,
220}
221
222impl Waiter {
223    /// Starts a wait and parks until a notification ends it or the clock
224    /// reaches `deadline`, returning whether a notification ended it.
225    #[cfg(any(test, feature = "test-clock"))]
226    fn wait(&self, deadline: Option<Instant>) -> bool {
227        let mut state = self.signal.lock();
228        let start = state.start();
229        self.park(state, start, deadline).1
230    }
231
232    /// Returns the deadline as a real timer on a real clock, and no timer on a
233    /// test clock, whose advances end its waits.
234    fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
235        #[cfg(any(test, feature = "test-clock"))]
236        if self.clock.paused.is_some() {
237            return None;
238        }
239        deadline
240    }
241
242    /// Parks the wait that started at `start` until a notification ends it or
243    /// the clock reaches `deadline`. Returns the state locked again, and
244    /// whether a notification ended the wait.
245    ///
246    /// On a test clock, the park counts as blocked and lists its deadline from
247    /// its first wait until it returns, however often it wakes in between. Only
248    /// a real clock's park uses a real timer. In tests, one-shot hooks run
249    /// before its first wait and before a wait after a spurious wakeup, with
250    /// the signal unlocked.
251    fn park<'a>(
252        &'a self,
253        mut state: MutexGuard<'a, SignalState>,
254        start: u64,
255        deadline: Option<Instant>,
256    ) -> (MutexGuard<'a, SignalState>, bool) {
257        // Only a real clock times its waits, since a test clock's advances end them
258        let timer = self.timer(deadline);
259
260        // Count the park once, keeping it counted through every wakeup until it returns
261        #[cfg(any(test, feature = "test-clock"))]
262        let mut blocked = None;
263
264        let notified = loop {
265            // Take a notification first, so that one wins over a reached deadline
266            if state.claim(start) {
267                break true;
268            }
269            let remaining =
270                deadline.map(|deadline| deadline.saturating_duration_since(self.clock.now()));
271            if remaining.is_some_and(|remaining| remaining.is_zero()) {
272                break false;
273            }
274
275            // Let tests act before a wait, then check again for any change they made
276            #[cfg(test)]
277            if let Some(hook) = self
278                .clock
279                .paused
280                .as_ref()
281                .and_then(|paused| paused.take_hook(blocked.is_some()))
282            {
283                drop(state);
284                hook(timer);
285                state = self.signal.lock();
286                continue;
287            }
288
289            // Count the park under the clock lock, so an advance either finds it or has
290            // already passed its deadline
291            #[cfg(any(test, feature = "test-clock"))]
292            if blocked.is_none() {
293                if let Some(paused) = &self.clock.paused {
294                    blocked = paused.block(deadline, &self.signal);
295                    if blocked.is_none() {
296                        break false;
297                    }
298                }
299            }
300            #[cfg(test)]
301            {
302                state.parks += 1;
303                self.signal.parked.notify_all();
304            }
305
306            // Wait for a wakeup or the real timer, leaving expiry to the check above
307            state = match timer.and(remaining) {
308                None => self
309                    .signal
310                    .changed
311                    .wait(state)
312                    .expect("waiter signal not poisoned"),
313                Some(remaining) => {
314                    self.signal
315                        .changed
316                        .wait_timeout(state, remaining.min(MAX_REAL_WAIT))
317                        .expect("waiter signal not poisoned")
318                        .0
319                }
320            };
321        };
322
323        // Retire and unlist the wait under the signal lock before its caller acts
324        state.retire(start);
325        #[cfg(any(test, feature = "test-clock"))]
326        drop(blocked);
327        (state, notified)
328    }
329}
330
331/// Hands notifications to a waiter's waits and wakes parked threads to check
332/// for one or for their deadline.
333#[derive(Default)]
334struct Signal {
335    /// Notification bookkeeping and the test park count, guarded together.
336    state: Mutex<SignalState>,
337    /// Wakes parked threads for a notification or a reached deadline.
338    changed: Condvar,
339    /// Reports new parks to tests, without waking parked threads.
340    #[cfg(test)]
341    parked: Condvar,
342    /// Clock wakes delivered, so tests can check which waits an advance reached.
343    #[cfg(test)]
344    wakes: AtomicUsize,
345}
346
347/// Mutable part of a signal.
348///
349/// Notifications are numbered in the order they are sent, and a wait can only
350/// take one sent after it started. Each queued `notify_one` can be matched to
351/// a distinct counted wait that started before it and will check again, so
352/// none is ever left behind. A `u64` count cannot wrap within any run.
353#[derive(Default)]
354struct SignalState {
355    /// Number of the latest notification, one-shot or broadcast.
356    notifications: u64,
357    /// Number of the latest `notify_all`, which ends every wait started before it.
358    broadcast: u64,
359    /// Waits started and neither finished nor ended by a broadcast.
360    waiting: usize,
361    /// Numbers of the `notify_one` calls no wait has taken yet, oldest first.
362    pending: VecDeque<u64>,
363    /// Total physical waits entered, for test synchronization.
364    #[cfg(test)]
365    parks: usize,
366}
367
368impl SignalState {
369    /// Counts a new wait, returning its start, the number after which it
370    /// can take notifications.
371    fn start(&mut self) -> u64 {
372        self.waiting += 1;
373        self.notifications
374    }
375
376    /// Returns whether a notification ends the wait that started at `start`,
377    /// taking the oldest queued one sent after it unless a broadcast ended it.
378    fn claim(&mut self, start: u64) -> bool {
379        // A broadcast ends every wait started before it, and takes no queued notification
380        if self.broadcast > start {
381            return true;
382        }
383
384        // Take the oldest notification this wait can, leaving earlier ones to earlier waits
385        if let Some(index) = self.pending.iter().position(|&sent| sent > start) {
386            self.pending.remove(index);
387            return true;
388        }
389        false
390    }
391
392    /// Uncounts a finished wait, unless the broadcast that ended it already did.
393    fn retire(&mut self, start: u64) {
394        if self.broadcast <= start {
395            self.waiting -= 1;
396        }
397    }
398}
399
400impl Signal {
401    /// Locks the state, which no code panics under.
402    fn lock(&self) -> MutexGuard<'_, SignalState> {
403        self.state.lock().expect("waiter signal not poisoned")
404    }
405
406    /// Queues a notification for one of the waits already started, and wakes a
407    /// parked thread to take it. Queues none once every counted wait has one.
408    fn notify_one(&self) {
409        let mut state = self.lock();
410        state.notifications += 1;
411        if state.pending.len() < state.waiting {
412            let sent = state.notifications;
413            state.pending.push_back(sent);
414            self.changed.notify_one();
415        }
416    }
417
418    /// Ends every wait started so far, dropping their queued notifications,
419    /// and wakes every parked thread.
420    fn notify_all(&self) {
421        // Uncount the ended waits at once, so that no later notify_one queues one for them
422        let mut state = self.lock();
423        state.notifications += 1;
424        state.broadcast = state.notifications;
425        state.pending.clear();
426        state.waiting = 0;
427        self.changed.notify_all();
428    }
429
430    /// Wakes every parked thread to recheck the time, without counting a notification.
431    #[cfg(any(test, feature = "test-clock"))]
432    fn wake(&self) {
433        #[cfg(test)]
434        self.wakes.fetch_add(1, Ordering::SeqCst);
435
436        // Take the lock, so a park that read the time before the advance is waiting by now
437        let _state = self.lock();
438        self.changed.notify_all();
439    }
440}
441
442// The tests live in src/tests, apart from the code they check
443#[cfg(test)]
444#[cfg_attr(coverage_nightly, coverage(off))]
445mod tests;