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 waits 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 {
166                #[cfg(any(test, feature = "test-clock"))]
167                paused: self.paused.as_ref().map(Arc::downgrade),
168                ..Signal::default()
169            }),
170        }
171    }
172}
173
174// A production clock carries no state, so passing one around costs nothing
175#[cfg(not(any(test, feature = "test-clock")))]
176const _: () = assert!(size_of::<Clock>() == 0);
177
178// A production clock still has a destructor, as a test build's does
179#[cfg(not(any(test, feature = "test-clock")))]
180const _: () = assert!(std::mem::needs_drop::<Clock>());
181
182impl Drop for Clock {
183    /// Does nothing, but gives every build a destructor, so enabling
184    /// `test-clock` does not change which const contexts accept a clock.
185    fn drop(&mut self) {}
186}
187
188impl PartialEq for Clock {
189    /// Compares identity, with all real clocks equal to each other.
190    fn eq(&self, other: &Self) -> bool {
191        self.identity() == other.identity()
192    }
193}
194
195impl Eq for Clock {}
196
197impl fmt::Debug for Clock {
198    /// Shows whether the clock is paused, its advance and its wall time.
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        // Read both times under one lock before calling the formatter's writer
201        #[cfg(any(test, feature = "test-clock"))]
202        let snapshot = self.paused.as_ref().map(|paused| paused.snapshot());
203
204        // Format without the clock lock, since the writer may read this clock
205        let mut clock = f.debug_struct("Clock");
206        #[cfg(any(test, feature = "test-clock"))]
207        if let Some((advanced, system_time)) = snapshot {
208            return clock
209                .field("paused", &true)
210                .field("advanced", &advanced)
211                .field("system_time", &system_time)
212                .finish();
213        }
214        clock.field("paused", &false).finish()
215    }
216}
217
218/// Blocks threads until notified or a deadline passes on a clock.
219struct Waiter {
220    /// Clock that decides when deadlines pass.
221    clock: Clock,
222    /// Notifications for this waiter's waits, and the wakeups that deliver them.
223    signal: Arc<Signal>,
224}
225
226impl Waiter {
227    /// Starts a wait and blocks until a notification ends it or the clock
228    /// reaches `deadline`, returning whether a notification ended it.
229    #[cfg(any(test, feature = "test-clock"))]
230    fn wait(&self, deadline: Option<Instant>) -> bool {
231        let mut state = self.signal.lock();
232        let start = state.start();
233        self.wait_locked(state, start, deadline).1
234    }
235
236    /// Returns the deadline as a real timer on a real clock, and no timer on a
237    /// test clock, whose advances end its waits.
238    fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
239        #[cfg(any(test, feature = "test-clock"))]
240        if self.clock.paused.is_some() {
241            return None;
242        }
243        deadline
244    }
245
246    /// Waits, with the signal state locked on entry and return, until a
247    /// notification ends the wait that started at `start` or the clock
248    /// reaches `deadline`, returning whether a notification ended it.
249    ///
250    /// On a test clock, the wait stays registered with its deadline until it
251    /// returns, including after a wake. Only a real clock uses a real timer.
252    /// Test hooks run with the signal unlocked before registration and before
253    /// a repeated wait after a spurious wake.
254    fn wait_locked<'a>(
255        &'a self,
256        mut state: MutexGuard<'a, SignalState>,
257        start: u64,
258        deadline: Option<Instant>,
259    ) -> (MutexGuard<'a, SignalState>, bool) {
260        // Only a real clock times its waits, since a test clock's advances end them
261        let timer = self.timer(deadline);
262
263        // Keep the wait registered through every wakeup until it returns
264        #[cfg(any(test, feature = "test-clock"))]
265        let mut registration = None;
266
267        let notified = loop {
268            // Take a notification first, so that one wins over a reached deadline
269            if state.claim(start) {
270                break true;
271            }
272            let remaining =
273                deadline.map(|deadline| deadline.saturating_duration_since(self.clock.now()));
274            if remaining.is_some_and(|remaining| remaining.is_zero()) {
275                break false;
276            }
277
278            // Let tests act before a wait, then check again for any change they made
279            #[cfg(test)]
280            if let Some(hook) = self
281                .clock
282                .paused
283                .as_ref()
284                .and_then(|paused| paused.take_hook(registration.is_some()))
285            {
286                drop(state);
287                hook(timer);
288                state = self.signal.lock();
289                continue;
290            }
291
292            // Register under the clock lock, so an advance either finds the wait or has
293            // already passed its deadline
294            #[cfg(any(test, feature = "test-clock"))]
295            if registration.is_none() {
296                if let Some(paused) = &self.clock.paused {
297                    registration = paused.register(deadline, &self.signal);
298                    if registration.is_none() {
299                        break false;
300                    }
301                }
302            }
303            #[cfg(test)]
304            {
305                state.waits += 1;
306                self.signal.entered.notify_all();
307            }
308
309            // Wait for a wakeup or the real timer, leaving expiry to the check above
310            state = match timer.and(remaining) {
311                None => self
312                    .signal
313                    .changed
314                    .wait(state)
315                    .expect("waiter signal not poisoned"),
316                Some(remaining) => {
317                    self.signal
318                        .changed
319                        .wait_timeout(state, remaining.min(MAX_REAL_WAIT))
320                        .expect("waiter signal not poisoned")
321                        .0
322                }
323            };
324        };
325
326        // Retire and unlist the wait under the signal lock before its caller acts
327        state.retire(start);
328        #[cfg(any(test, feature = "test-clock"))]
329        drop(registration);
330        #[cfg(any(test, feature = "test-clock"))]
331        self.signal.publish(&mut state);
332        (state, notified)
333    }
334}
335
336/// Hands notifications to registered waits and wakes threads to check
337/// for one or for their deadline.
338#[derive(Default)]
339struct Signal {
340    /// Clock observing outstanding notifications for its parked wait barrier.
341    #[cfg(any(test, feature = "test-clock"))]
342    paused: Option<std::sync::Weak<paused::Paused>>,
343    /// Notification bookkeeping and the test wait count, guarded together.
344    state: Mutex<SignalState>,
345    /// Wakes waiting threads for a notification or a reached deadline.
346    changed: Condvar,
347    /// Reports new physical waits to tests without waking the waiting threads.
348    #[cfg(test)]
349    entered: Condvar,
350    /// Clock wakes delivered, so tests can check which waits an advance reached.
351    #[cfg(test)]
352    wakes: AtomicUsize,
353}
354
355/// Mutable part of a signal.
356///
357/// Notifications are numbered in the order they are sent, and a wait can only
358/// take one sent after it started. Each queued `notify_one` can be matched to
359/// a distinct counted wait that started before it and will check again, so
360/// none is ever left behind. A `u64` count cannot wrap within any run.
361#[derive(Default)]
362struct SignalState {
363    /// Number of the latest notification, one-shot or broadcast.
364    notifications: u64,
365    /// Number of the latest `notify_all`, which ends every wait started before it.
366    broadcast: u64,
367    /// Waits started and neither finished nor ended by a broadcast.
368    waiting: usize,
369    /// Numbers of the `notify_one` calls no wait has taken yet, oldest first.
370    pending: VecDeque<u64>,
371    /// Waits ended by broadcasts that have not yet returned.
372    #[cfg(any(test, feature = "test-clock"))]
373    ended: usize,
374    /// Notification obligations last published to the test clock.
375    #[cfg(any(test, feature = "test-clock"))]
376    published: usize,
377    /// Total physical waits entered, for test synchronization.
378    #[cfg(test)]
379    waits: usize,
380}
381
382impl SignalState {
383    /// Counts a new wait, returning its start, the number after which it
384    /// can take notifications.
385    fn start(&mut self) -> u64 {
386        self.waiting += 1;
387        self.notifications
388    }
389
390    /// Returns whether a notification ends the wait that started at `start`,
391    /// taking the oldest queued one sent after it unless a broadcast ended it.
392    fn claim(&mut self, start: u64) -> bool {
393        // A broadcast ends every wait started before it, and takes no queued notification
394        if self.broadcast > start {
395            return true;
396        }
397
398        // Take the oldest notification this wait can, leaving earlier ones to earlier waits
399        if let Some(index) = self.pending.iter().position(|&sent| sent > start) {
400            self.pending.remove(index);
401            return true;
402        }
403        false
404    }
405
406    /// Uncounts a finished wait, unless the broadcast that ended it already did.
407    fn retire(&mut self, start: u64) {
408        if self.broadcast <= start {
409            self.waiting -= 1;
410        } else {
411            #[cfg(any(test, feature = "test-clock"))]
412            {
413                self.ended -= 1;
414            }
415        }
416    }
417}
418
419impl Signal {
420    /// Locks the state, which no code panics under.
421    fn lock(&self) -> MutexGuard<'_, SignalState> {
422        self.state.lock().expect("waiter signal not poisoned")
423    }
424
425    /// Queues a notification for one of the waits already started, and wakes a
426    /// waiting thread to take it. Queues none once every counted wait has one.
427    fn notify_one(&self) {
428        let mut state = self.lock();
429        state.notifications += 1;
430        if state.pending.len() < state.waiting {
431            let sent = state.notifications;
432            state.pending.push_back(sent);
433            #[cfg(any(test, feature = "test-clock"))]
434            self.publish(&mut state);
435            self.changed.notify_one();
436        }
437    }
438
439    /// Ends every wait started so far, dropping their queued notifications,
440    /// and wakes every waiting thread.
441    fn notify_all(&self) {
442        // Uncount the ended waits at once, so that no later notify_one queues one for them
443        let mut state = self.lock();
444        state.notifications += 1;
445        state.broadcast = state.notifications;
446        state.pending.clear();
447        #[cfg(any(test, feature = "test-clock"))]
448        {
449            state.ended += state.waiting;
450        }
451        state.waiting = 0;
452        #[cfg(any(test, feature = "test-clock"))]
453        self.publish(&mut state);
454        self.changed.notify_all();
455    }
456
457    /// Publishes this signal's outstanding notifications to its clock while the
458    /// signal stays locked.
459    ///
460    /// Claiming a credit alone publishes nothing. A wait publishes only after
461    /// it retires and drops its registration, so the parked wait barrier never
462    /// counts a wait whose credit it has already consumed.
463    #[cfg(any(test, feature = "test-clock"))]
464    fn publish(&self, state: &mut SignalState) {
465        if let Some(paused) = self.paused.as_ref().and_then(std::sync::Weak::upgrade) {
466            let pending = state.ended + state.pending.len();
467            if pending != state.published {
468                paused.notifications(state.published, pending);
469                state.published = pending;
470            }
471        }
472    }
473
474    /// Wakes every waiting thread to recheck the time without counting a notification.
475    #[cfg(any(test, feature = "test-clock"))]
476    fn wake(&self) {
477        #[cfg(test)]
478        self.wakes.fetch_add(1, Ordering::SeqCst);
479
480        // Take the lock, so a thread that read the time before the advance is waiting by now
481        let _state = self.lock();
482        self.changed.notify_all();
483    }
484}
485
486// The tests live in src/tests, apart from the code they check
487#[cfg(test)]
488#[cfg_attr(coverage_nightly, coverage(off))]
489mod tests;