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 std's safe synchronization and never needs unsafe
14#![forbid(unsafe_code)]
15
16// Pausing is test-only, so a build without the test-clock feature cannot stop its own time
17#[cfg(any(test, feature = "test-clock"))]
18mod paused;
19
20use std::fmt;
21use std::sync::{Arc, Condvar, Mutex, MutexGuard};
22use std::time::Instant;
23
24/// A monotonic clock, either the real one or a paused one that a test
25/// advances by hand.
26///
27/// Clones share one clock. Equality is identity, not the current time, so
28/// all real clocks are equal and a paused clock equals only its own clones.
29#[derive(Clone)]
30// Without pausing every clock is the real one, so all are equal; paused.rs compares identity
31#[cfg_attr(not(any(test, feature = "test-clock")), derive(PartialEq, Eq))]
32pub struct Clock {
33    /// Shared state of a paused clock, or nothing for the real clock.
34    #[cfg(any(test, feature = "test-clock"))]
35    paused: Option<Arc<paused::Paused>>,
36}
37
38impl Clock {
39    /// Returns the real monotonic clock, which reads [`Instant::now`].
40    pub fn real() -> Self {
41        Self {
42            #[cfg(any(test, feature = "test-clock"))]
43            paused: None,
44        }
45    }
46
47    /// Returns the clock's current time.
48    pub fn now(&self) -> Instant {
49        #[cfg(any(test, feature = "test-clock"))]
50        if let Some(paused) = &self.paused {
51            return paused.now();
52        }
53        Instant::now()
54    }
55
56    /// Creates a waiter that measures deadlines on this clock. A paused clock
57    /// wakes it on every advance.
58    pub fn waiter(&self) -> Waiter {
59        let signal = Arc::new(Signal::default());
60        #[cfg(any(test, feature = "test-clock"))]
61        if let Some(paused) = &self.paused {
62            paused.register(&signal);
63        }
64        Waiter {
65            clock: self.clone(),
66            signal,
67        }
68    }
69}
70
71impl fmt::Debug for Clock {
72    /// Shows whether the clock is paused and, if so, how far it has advanced.
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        let mut clock = f.debug_struct("Clock");
75        #[cfg(any(test, feature = "test-clock"))]
76        if let Some(paused) = &self.paused {
77            return clock
78                .field("paused", &true)
79                .field("advanced", &paused.advanced())
80                .finish();
81        }
82        clock.field("paused", &false).finish()
83    }
84}
85
86/// Blocks threads until a condition holds or a deadline passes on a clock.
87///
88/// Clones share one waiter, so threads can wait on it while others notify it.
89/// Code that changes the state a condition reads calls [`Waiter::notify_all`]
90/// after the change.
91///
92/// ```
93/// use darkbio_clock::Clock;
94/// use std::sync::{Arc, Mutex};
95/// use std::thread;
96///
97/// let waiter = Clock::real().waiter();
98/// let slot = Arc::new(Mutex::new(None));
99/// let taker = thread::spawn({
100///     let (waiter, slot) = (waiter.clone(), slot.clone());
101///     move || waiter.wait_until(None, || slot.lock().unwrap().take())
102/// });
103/// *slot.lock().unwrap() = Some(42);
104/// waiter.notify_all();
105/// assert_eq!(taker.join().unwrap(), Some(42));
106/// ```
107#[derive(Clone)]
108pub struct Waiter {
109    /// Clock that decides when deadlines pass.
110    clock: Clock,
111    /// Wakeups shared by all clones, also registered with a paused clock.
112    signal: Arc<Signal>,
113}
114
115impl Waiter {
116    /// Wakes every thread waiting on this waiter to check its condition again.
117    /// Call it after each change that can make a condition ready, with or
118    /// without holding the lock that change took.
119    pub fn notify_all(&self) {
120        self.signal.notify_all();
121    }
122
123    /// Blocks until `ready` returns a value or the clock reaches `deadline`.
124    ///
125    /// Every check calls `ready` first and compares the deadline after, so a
126    /// value that is ready wins over an expired deadline. The wait returns
127    /// `None` when a check finds `ready` empty and the deadline reached. Without
128    /// a deadline, only a value ends the wait. A plain delay passes a `ready`
129    /// that always returns `None`.
130    ///
131    /// A check follows each notification and each advance of a paused clock,
132    /// and sometimes a wakeup where nothing changed. Changes that land together
133    /// share one check, so `ready` can run fewer times than there were changes.
134    ///
135    /// `ready` runs on the calling thread with no lock of the clock or the waiter
136    /// held. It may take its own locks, notify, or advance a clock, and should
137    /// return promptly.
138    pub fn wait_until<T>(
139        &self,
140        deadline: Option<Instant>,
141        mut ready: impl FnMut() -> Option<T>,
142    ) -> Option<T> {
143        // A paused clock wakes its waiters when it moves, so only real time needs a timer
144        #[cfg(any(test, feature = "test-clock"))]
145        let timer = deadline.filter(|_| self.clock.paused.is_none());
146        #[cfg(not(any(test, feature = "test-clock")))]
147        let timer = deadline;
148        loop {
149            // Take the generation before checking, so a change after the check cuts the park short
150            let seen = self.signal.generation();
151            if let Some(value) = ready() {
152                return Some(value);
153            }
154            if deadline.is_some_and(|deadline| self.clock.now() >= deadline) {
155                return None;
156            }
157            self.signal.park(seen, timer);
158        }
159    }
160}
161
162impl fmt::Debug for Waiter {
163    /// Shows the clock, never the wakeup bookkeeping.
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.debug_struct("Waiter")
166            .field("clock", &self.clock)
167            .finish_non_exhaustive()
168    }
169}
170
171/// Wakes the threads parked on one waiter. Each notification bumps the
172/// generation. A thread parks only while the generation it read before its
173/// check is current, so no wakeup is lost in between.
174#[derive(Default)]
175struct Signal {
176    /// Generation and parking count, guarded together.
177    state: Mutex<SignalState>,
178    /// Wakes parked threads when the generation moves.
179    changed: Condvar,
180}
181
182/// Mutable part of a signal.
183#[derive(Default)]
184struct SignalState {
185    /// Number of notifications and clock advances so far.
186    generation: u64,
187    /// Parks that found the generation unchanged, for tests to wait on. An
188    /// untimed park counted here blocks until the next notification.
189    #[cfg(test)]
190    parks: usize,
191}
192
193impl Signal {
194    /// Locks the state, which no code panics under.
195    fn lock(&self) -> MutexGuard<'_, SignalState> {
196        self.state.lock().expect("waiter signal not poisoned")
197    }
198
199    /// Returns the current generation, to compare against when parking.
200    fn generation(&self) -> u64 {
201        self.lock().generation
202    }
203
204    /// Bumps the generation and wakes every parked thread.
205    fn notify_all(&self) {
206        self.lock().generation += 1;
207        self.changed.notify_all();
208    }
209
210    /// Blocks until the generation moves past `seen`, or until real time
211    /// reaches `timer`. The caller rechecks its condition and deadline after.
212    fn park(&self, seen: u64, timer: Option<Instant>) {
213        let mut state = self.lock();
214        #[cfg(test)]
215        if state.generation == seen {
216            state.parks += 1;
217            self.changed.notify_all();
218        }
219        while state.generation == seen {
220            state = match timer {
221                None => self
222                    .changed
223                    .wait(state)
224                    .expect("waiter signal not poisoned"),
225                Some(timer) => {
226                    // Decide expiry by the time, since a wait's own timeout flag is unreliable
227                    let now = Instant::now();
228                    if now >= timer {
229                        break;
230                    }
231                    self.changed
232                        .wait_timeout(state, timer - now)
233                        .expect("waiter signal not poisoned")
234                        .0
235                }
236            };
237        }
238    }
239}
240
241/// Checks paused time, and that waits wake for every change they depend on.
242#[cfg(test)]
243#[cfg_attr(coverage_nightly, coverage(off))]
244mod tests {
245    use super::*;
246    use std::panic::{self, AssertUnwindSafe};
247    use std::sync::atomic::{AtomicBool, Ordering};
248    use std::thread;
249    use std::time::Duration;
250
251    /// Blocks until parks on the waiter have found the generation unchanged
252    /// `count` times in total.
253    fn parked(waiter: &Waiter, count: usize) {
254        let mut state = waiter.signal.lock();
255        while state.parks < count {
256            state = waiter.signal.changed.wait(state).unwrap();
257        }
258    }
259
260    // Clones share one paused clock, which moves only by its own advances.
261    // Another paused clock keeps its own time, and equality follows sharing
262    // even when the two clocks show the same time.
263    #[test]
264    fn test_paused_clock_sharing() {
265        let other = Clock::paused();
266        let clock = Clock::paused();
267        let clone = clock.clone();
268        let other_start = other.now();
269        let start = clock.now();
270
271        clone.advance(Duration::from_secs(1));
272        assert_eq!(clock.now(), start + Duration::from_secs(1));
273        clock.advance_to(start + Duration::from_secs(3));
274        assert_eq!(clone.now(), start + Duration::from_secs(3));
275        clock.advance(Duration::ZERO);
276        clock.advance_to(start + Duration::from_secs(3));
277        assert_eq!(clock.now(), start + Duration::from_secs(3));
278        assert_eq!(other.now(), other_start);
279
280        other.advance_to(clock.now());
281        assert_eq!(other.now(), clock.now());
282        assert_eq!(clock, clone);
283        assert_ne!(clock, other);
284        assert_ne!(clock, Clock::real());
285        assert_eq!(Clock::real(), Clock::real());
286    }
287
288    // Advancing a real clock, going backwards and overflowing all panic without
289    // moving the paused clock, which stays usable afterwards.
290    #[test]
291    fn test_failed_advance_keeps_time() {
292        let clock = Clock::paused();
293        let real = Clock::real();
294        clock.advance(Duration::from_secs(1));
295        let now = clock.now();
296
297        let misuses: [(&str, &dyn Fn()); 3] = [
298            ("real", &|| real.advance(Duration::from_secs(1))),
299            ("backwards", &|| {
300                clock.advance_to(now - Duration::from_secs(1))
301            }),
302            ("overflow", &|| clock.advance(Duration::MAX)),
303        ];
304        for (case, misuse) in misuses {
305            assert!(
306                panic::catch_unwind(AssertUnwindSafe(misuse)).is_err(),
307                "{case}"
308            );
309            assert_eq!(clock.now(), now, "{case}");
310        }
311        clock.advance(Duration::from_secs(1));
312        assert_eq!(clock.now(), now + Duration::from_secs(1));
313    }
314
315    // A ready value wins over an expired deadline, and an expired deadline with
316    // nothing ready returns at once. A deadline equal to the current time counts.
317    #[test]
318    fn test_wait_checks_ready_first() {
319        for clock in [Clock::real(), Clock::paused()] {
320            let waiter = clock.waiter();
321            let now = clock.now();
322            assert_eq!(
323                waiter.wait_until(Some(now), || Some(1)),
324                Some(1),
325                "{clock:?}"
326            );
327            assert_eq!(
328                waiter.wait_until(Some(now), || None::<u8>),
329                None,
330                "{clock:?}"
331            );
332        }
333    }
334
335    // A notification or an advance landing between a failed check and the park
336    // is not lost. The wait rechecks each time and returns at its deadline.
337    #[test]
338    fn test_wait_sees_changes_after_its_check() {
339        let clock = Clock::paused();
340        let waiter = clock.waiter();
341        let deadline = clock.now() + Duration::from_secs(2);
342        let mut checks = 0;
343        let result = waiter.wait_until(Some(deadline), || {
344            checks += 1;
345            if checks == 1 {
346                waiter.notify_all();
347            } else if clock.now() < deadline {
348                clock.advance(Duration::from_secs(1));
349            }
350            None::<()>
351        });
352        assert_eq!(result, None);
353        assert_eq!(clock.now(), deadline);
354    }
355
356    // Advancing wakes every parked waiter of the clock, whichever clone made it
357    // and whatever waiters were dropped around it. An advance short of the
358    // deadline parks them again, and the one reaching the deadline ends them.
359    #[test]
360    fn test_advance_wakes_parked_waiters() {
361        let clock = Clock::paused();
362        let deadline = clock.now() + Duration::from_secs(60);
363        // Interleave live and dropped waiters in the clock's registrations
364        let mut waiters: Vec<_> = (0..4).map(|_| clock.waiter()).collect();
365        waiters.truncate(1);
366        waiters.push(clock.clone().waiter());
367        drop(clock.waiter());
368
369        let threads: Vec<_> = waiters
370            .iter()
371            .map(|waiter| {
372                let waiter = waiter.clone();
373                thread::spawn(move || waiter.wait_until(Some(deadline), || None::<()>))
374            })
375            .collect();
376        for waiter in &waiters {
377            parked(waiter, 1);
378        }
379        clock.advance(Duration::from_secs(30));
380        for waiter in &waiters {
381            parked(waiter, 2);
382        }
383        clock.advance(Duration::from_secs(30));
384        for thread in threads {
385            assert_eq!(thread.join().unwrap(), None);
386        }
387    }
388
389    // A value that arrives while the waiter is parked wins over the deadline,
390    // even when the advance past that deadline is what wakes the waiter.
391    #[test]
392    fn test_ready_value_survives_advance() {
393        let clock = Clock::paused();
394        let waiter = clock.waiter();
395        let deadline = clock.now() + Duration::from_secs(1);
396        let ready = Arc::new(AtomicBool::new(false));
397        let waiting = thread::spawn({
398            let (waiter, ready) = (waiter.clone(), ready.clone());
399            move || {
400                waiter.wait_until(Some(deadline), || {
401                    ready.load(Ordering::SeqCst).then_some(())
402                })
403            }
404        });
405        parked(&waiter, 1);
406        ready.store(true, Ordering::SeqCst);
407        clock.advance(Duration::from_secs(1));
408        assert_eq!(waiting.join().unwrap(), Some(()));
409    }
410
411    // One notification wakes every thread parked on the waiter or its clones,
412    // on either clock, once the condition they check holds.
413    #[test]
414    fn test_notify_wakes_parked_waiters() {
415        for clock in [Clock::real(), Clock::paused()] {
416            let waiter = clock.waiter();
417            let open = Arc::new(AtomicBool::new(false));
418            let threads: Vec<_> = (0..2)
419                .map(|_| {
420                    let (waiter, open) = (waiter.clone(), open.clone());
421                    thread::spawn(move || {
422                        waiter.wait_until(None, || open.load(Ordering::SeqCst).then_some(()))
423                    })
424                })
425                .collect();
426            parked(&waiter, 2);
427            open.store(true, Ordering::SeqCst);
428            waiter.notify_all();
429            for thread in threads {
430                assert_eq!(thread.join().unwrap(), Some(()), "{clock:?}");
431            }
432        }
433    }
434
435    // The real clock ends a wait by its own timer once the deadline passes.
436    // Only real time can end it, so this test waits 1 ms.
437    #[test]
438    fn test_real_deadline_expires() {
439        let waiter = Clock::real().waiter();
440        let deadline = Instant::now() + Duration::from_millis(1);
441        assert_eq!(waiter.wait_until(Some(deadline), || None::<()>), None);
442        assert!(Instant::now() >= deadline);
443    }
444}