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