Skip to main content

darkbio_clock/
paused.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//! Test clocks, which move only when their owner advances them.
8
9use crate::primitives::{Condvar, Mutex, MutexGuard};
10use crate::{Clock, Signal};
11#[cfg(feature = "crossbeam")]
12use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError};
13use std::collections::{BTreeMap, HashSet};
14use std::fmt;
15use std::sync::{Arc, PoisonError};
16use std::time::{Duration, Instant, SystemTime};
17
18/// Owns a clock that moves only when advanced, for tests.
19///
20/// [`Self::clock`] hands out handles that read and sleep on its time. Only the
21/// owner moves it, through methods that take `&mut self`, so each test clock
22/// has one driver.
23#[cfg_attr(docsrs, doc(cfg(feature = "test-clock")))]
24pub struct TestClock {
25    /// State shared with every clock handle.
26    paused: Arc<Paused>,
27}
28
29impl TestClock {
30    /// Creates a stopped clock at the current real monotonic and wall times.
31    pub fn new() -> Self {
32        let now = Instant::now();
33        Self {
34            paused: Arc::new(Paused {
35                start: now,
36                state: Mutex::new(PausedState {
37                    now,
38                    wall: WallAnchor {
39                        time: SystemTime::now(),
40                        instant: now,
41                    },
42                    sequence: 0,
43                    registered: 0,
44                    notified: 0,
45                    deadlines: BTreeMap::new(),
46                    #[cfg(feature = "crossbeam")]
47                    timers: BTreeMap::new(),
48                    #[cfg(feature = "crossbeam")]
49                    fired: Vec::new(),
50                }),
51                changed: Condvar::new(),
52                #[cfg(test)]
53                before_wait: std::sync::Mutex::new(None),
54                #[cfg(test)]
55                before_rewait: std::sync::Mutex::new(None),
56                #[cfg(test)]
57                before_parked_wait: std::sync::Mutex::new(None),
58                #[cfg(all(test, feature = "crossbeam"))]
59                after_timer_receive: std::sync::Mutex::new(None),
60            }),
61        }
62    }
63
64    /// Returns a handle that reads and sleeps on this clock.
65    pub fn clock(&self) -> Clock {
66        Clock {
67            paused: Some(self.paused.clone()),
68        }
69    }
70
71    /// Moves both times forward by `by`, waking the sleeps and deadline waits
72    /// it reaches and firing its due timers.
73    ///
74    /// Returns once the reached waits are woken and the due timers hold their
75    /// messages, without waiting for any thread to act. Other waits keep
76    /// waiting, since an advance never counts as a notification. A zero
77    /// advance does nothing.
78    ///
79    /// # Panics
80    ///
81    /// Panics if either time would overflow, before changing either one.
82    pub fn advance(&mut self, by: Duration) {
83        self.advance_with(|now| {
84            now.checked_add(by)
85                .expect("clock advance overflows Instant")
86        });
87    }
88
89    /// Moves monotonic time to `target` and wall time by the same amount,
90    /// waking the sleeps and deadline waits it reaches and firing its due timers.
91    ///
92    /// Returns once the reached waits are woken and the due timers hold their
93    /// messages, without waiting for any thread to act. Other waits keep
94    /// waiting, since an advance never counts as a notification. Advancing to
95    /// the current time does nothing.
96    ///
97    /// # Panics
98    ///
99    /// Panics if `target` is before now or wall time would overflow.
100    /// Neither time changes after a panic.
101    pub fn advance_to(&mut self, target: Instant) {
102        self.advance_with(|now| {
103            assert!(target >= now, "clock cannot go backwards");
104            target
105        });
106    }
107
108    /// Sets wall time forwards or backwards without moving monotonic time.
109    ///
110    /// This wakes no waits and fires no timers, since deadlines use monotonic time.
111    pub fn set_system_time(&mut self, time: SystemTime) {
112        let mut state = self.paused.lock();
113        state.wall = WallAnchor {
114            time,
115            instant: state.now,
116        };
117    }
118
119    /// Blocks until at least `count` clock sleeps or condvar waits are
120    /// registered, timed or not.
121    ///
122    /// A wait stays registered from the moment it starts waiting until it
123    /// returns. So the count includes a thread that a notification or an
124    /// advance has woken but that has not run yet. Threads blocked in crossbeam
125    /// receives, selects or mutexes, or busy outside clock waits, do not count.
126    /// An earlier registration counts too, so the count proves no progress on
127    /// its own. Nothing bounds the wait, so run tests under a runner with a
128    /// per-test timeout, since `cargo test` alone never stops a hung test.
129    pub fn wait_registered(&self, count: usize) {
130        let mut state = self.paused.lock();
131        while state.registered < count {
132            state = self
133                .paused
134                .changed
135                .wait(state)
136                .unwrap_or_else(PoisonError::into_inner);
137        }
138    }
139
140    /// Blocks until at least `count` registered waits are parked, with their
141    /// deadlines unreached or absent, and no notification on this clock is
142    /// outstanding.
143    ///
144    /// A notification stays outstanding until every wait it ended has
145    /// returned. So a woken thread that has not run yet holds the barrier, on
146    /// whichever condvar of this clock it waits.
147    ///
148    /// The count must cover every worker the test expects to park, and the
149    /// test must control the work they receive. Threads blocked in crossbeam
150    /// receives, selects or mutexes, or busy outside clock waits, are invisible
151    /// to it, and a worker that exits never counts again. Nothing bounds the
152    /// wait, so run tests under a runner with a per-test timeout.
153    pub fn wait_parked(&self, count: usize) {
154        let mut state = self.paused.lock();
155        while !state.parked(count) {
156            // Let a test release a held worker once this barrier must wait
157            #[cfg(test)]
158            {
159                let hook = self.paused.before_parked_wait.lock().unwrap().take();
160                if let Some(hook) = hook {
161                    drop(state);
162                    hook();
163                    state = self.paused.lock();
164                    continue;
165                }
166            }
167            state = self
168                .paused
169                .changed
170                .wait(state)
171                .unwrap_or_else(PoisonError::into_inner);
172        }
173    }
174
175    /// Blocks until at least `count` timers are armed on this clock and unfired.
176    ///
177    /// A waiting receive's timer counts, and so does a timer whose receiver was
178    /// dropped. A timer armed earlier counts too, so the count proves no
179    /// progress on its own. Nothing bounds the wait, so run tests under a runner
180    /// with a per-test timeout, since `cargo test` alone never stops a hung test.
181    #[cfg(feature = "crossbeam")]
182    #[cfg_attr(docsrs, doc(cfg(all(feature = "test-clock", feature = "crossbeam"))))]
183    pub fn wait_timers(&self, count: usize) {
184        let mut state = self.paused.lock();
185        while state.timers.len() < count {
186            state = self
187                .paused
188                .changed
189                .wait(state)
190                .unwrap_or_else(PoisonError::into_inner);
191        }
192    }
193
194    /// Returns the earliest deadline among this clock's registered sleeps,
195    /// deadline waits and unfired timers, or `None` when there is none.
196    ///
197    /// A timed wait stays listed until it stops waiting, so one an advance
198    /// reaches stays listed until its thread runs, and the result is then the
199    /// current time. Await an advance's effect before reading the next
200    /// deadline. A timer leaves the list when it fires.
201    pub fn next_deadline(&self) -> Option<Instant> {
202        // Compare the earliest registered wait with the earliest unfired timer, and report
203        // one already reached at the current time, so that advancing to it stays valid
204        let state = self.paused.lock();
205        let deadline = state.deadlines.keys().next().map(|&(deadline, _)| deadline);
206        #[cfg(feature = "crossbeam")]
207        let deadline = deadline
208            .into_iter()
209            .chain(state.timers.keys().next().map(|&(deadline, _)| deadline))
210            .min();
211        deadline.map(|deadline| deadline.max(state.now))
212    }
213
214    /// Validates both new times, publishes them together with the due timers'
215    /// messages, then wakes the reached waits.
216    fn advance_with(&mut self, next: impl FnOnce(Instant) -> Instant) {
217        // Check the new times before taking the lock, so a failed check panics with
218        // no lock held. Only the owner advances, so nothing changes them in between.
219        let (now, wall) = {
220            let state = self.paused.lock();
221            (state.now, state.wall)
222        };
223        let next = next(now);
224        if next == now {
225            return;
226        }
227        wall.at(next).expect("clock advance overflows SystemTime");
228
229        // Publish the time and collect each reached wait's signal once, in the same
230        // lock hold that registrations take. The wakes follow deadline and
231        // registration order, since the address set only filters out repeats.
232        let mut state = self.paused.lock();
233        state.now = next;
234        let mut seen = HashSet::new();
235        let signals: Vec<_> = state
236            .deadlines
237            .range(..=(next, u64::MAX))
238            .filter(|(_, signal)| seen.insert(Arc::as_ptr(signal)))
239            .map(|(_, signal)| signal.clone())
240            .collect();
241
242        // Deliver every due timer before another thread can read the new time
243        #[cfg(feature = "crossbeam")]
244        {
245            while let Some(entry) = state
246                .timers
247                .first_entry()
248                .filter(|entry| entry.key().0 <= next)
249            {
250                let timer = entry.remove();
251                state.fire(timer);
252            }
253        }
254        drop(state);
255
256        // Wake outside the clock lock, since registration takes the signal lock first
257        for signal in signals {
258            signal.wake();
259        }
260    }
261}
262
263impl Default for TestClock {
264    /// Creates a stopped clock at the current real monotonic and wall times.
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270impl fmt::Debug for TestClock {
271    /// Shows the advance, wall time, registered waits and armed timers.
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        // Snapshot the counts before calling the formatter's writer
274        let state = self.paused.lock();
275        let advanced = state.now - self.paused.start;
276        let system_time = state.system_time();
277        let registered = state.registered;
278        #[cfg(feature = "crossbeam")]
279        let timers = state.timers.len();
280        drop(state);
281
282        // Format without the clock lock, since the writer may read this clock
283        let mut debug = f.debug_struct("TestClock");
284        debug
285            .field("advanced", &advanced)
286            .field("system_time", &system_time)
287            .field("registered", &registered);
288        #[cfg(feature = "crossbeam")]
289        debug.field("timers", &timers);
290        debug.finish()
291    }
292}
293
294/// A test clock's state, shared by its owner, handles and waiters.
295pub(crate) struct Paused {
296    /// Time the clock started at, to show how far it has advanced.
297    start: Instant,
298    /// Current times, registered wait count, deadlines and timers.
299    pub(crate) state: Mutex<PausedState>,
300    /// Wakes drivers when registrations, notifications or armed timers change.
301    pub(crate) changed: Condvar,
302    /// One-shot test hook, run before a wait's registration with no clock or
303    /// signal lock held.
304    #[cfg(test)]
305    pub(crate) before_wait: std::sync::Mutex<Option<BeforeWait>>,
306    /// One-shot test hook, run like `before_wait` before a registered wait waits again
307    /// after a spurious wakeup.
308    #[cfg(test)]
309    pub(crate) before_rewait: std::sync::Mutex<Option<BeforeWait>>,
310    /// Test hook before `wait_parked` blocks, run once with no clock lock held.
311    #[cfg(test)]
312    pub(crate) before_parked_wait: std::sync::Mutex<Option<BeforeParkedWait>>,
313    /// One-shot test hook, run when a receive's timer wins, before the receiver
314    /// is checked again, with no crate lock held.
315    #[cfg(all(test, feature = "crossbeam"))]
316    after_timer_receive: std::sync::Mutex<Option<TimerHook>>,
317}
318
319/// Mutable part of a test clock.
320pub(crate) struct PausedState {
321    /// Current monotonic time.
322    now: Instant,
323    /// Wall time as last set, with the monotonic instant it was set at.
324    wall: WallAnchor,
325    /// Last sequence number given to a timed wait or timer, which a `u64`
326    /// cannot outgrow within any run.
327    sequence: u64,
328    /// Clock waits registered until their calls return, including woken waits.
329    pub(crate) registered: usize,
330    /// Notifications whose affected waits have not finished retiring.
331    notified: usize,
332    /// Registered timed waits by deadline and registration order, each with
333    /// its signal and unlisted when its wait returns.
334    deadlines: BTreeMap<(Instant, u64), Arc<Signal>>,
335    /// Unfired timers by deadline and arming order.
336    #[cfg(feature = "crossbeam")]
337    timers: BTreeMap<(Instant, u64), Timer>,
338    /// Senders of delivered public timers, kept so their channels stay connected
339    /// while the clock lives.
340    #[cfg(feature = "crossbeam")]
341    fired: Vec<Sender<Instant>>,
342}
343
344impl PausedState {
345    /// Reports whether at least `count` waits are parked with no outstanding notification.
346    pub(crate) fn parked(&self, count: usize) -> bool {
347        self.notified == 0 && self.unreached() >= count
348    }
349
350    /// Counts registered waits whose deadlines have not been reached.
351    fn unreached(&self) -> usize {
352        self.registered - self.deadlines.range(..=(self.now, u64::MAX)).count()
353    }
354
355    /// Keys a timed wait or timer by its deadline, ordering equal deadlines by
356    /// when they were listed.
357    fn next_key(&mut self, deadline: Instant) -> (Instant, u64) {
358        self.sequence += 1;
359        (deadline, self.sequence)
360    }
361
362    /// Returns the wall time at the current monotonic time.
363    fn system_time(&self) -> SystemTime {
364        self.wall
365            .at(self.now)
366            .expect("wall time fits, since every change checks it first")
367    }
368
369    /// Lists a timer until an advance reaches it, or delivers it at once if it
370    /// is already due.
371    #[cfg(feature = "crossbeam")]
372    fn arm_timer(
373        &mut self,
374        deadline: Instant,
375        retain: bool,
376    ) -> ((Instant, u64), Receiver<Instant>) {
377        // Give the timer a sequence number and room for its only message
378        let key = self.next_key(deadline);
379        let (sender, receiver) = crossbeam_channel::bounded(1);
380        let timer = Timer {
381            deadline,
382            sender,
383            retain,
384        };
385
386        // List or deliver it in the lock hold that read the time, so no advance slips between
387        if deadline > self.now {
388            self.timers.insert(key, timer);
389        } else {
390            self.fire(timer);
391        }
392        (key, receiver)
393    }
394
395    /// Sends a timer's deadline, and keeps the sender of a delivered public timer.
396    #[cfg(feature = "crossbeam")]
397    fn fire(&mut self, timer: Timer) {
398        // The capacity-1 channel has never been sent to, so this cannot block
399        let delivered = timer.sender.send(timer.deadline).is_ok();
400
401        // Keep a delivered public timer's sender, so its channel stays connected like
402        // crossbeam's. Nothing reports a dropped receiver without sending, and a second
403        // send would refill a consumed timer.
404        if delivered && timer.retain {
405            self.fired.push(timer.sender);
406        }
407    }
408}
409
410/// A wall time and the monotonic instant it was set at.
411#[derive(Clone, Copy)]
412struct WallAnchor {
413    /// Wall time when the clock was created or last set.
414    time: SystemTime,
415    /// Monotonic time at that moment.
416    instant: Instant,
417}
418
419impl WallAnchor {
420    /// Returns the wall time at `instant`, or `None` if it does not fit.
421    ///
422    /// The whole span since the anchor is added at once, so a platform that
423    /// rounds wall time, like Windows to 100 ns, rounds once and never per advance.
424    fn at(&self, instant: Instant) -> Option<SystemTime> {
425        self.time.checked_add(instant - self.instant)
426    }
427}
428
429impl Paused {
430    /// Replaces one signal's contribution to the outstanding notifications.
431    pub(crate) fn notifications(&self, previous: usize, current: usize) {
432        let mut state = self.lock();
433        state.notified = state.notified - previous + current;
434        self.changed.notify_all();
435    }
436
437    /// Returns armed timers and retained senders for the bookkeeping model.
438    #[cfg(all(test, feature = "crossbeam", not(loom)))]
439    pub(crate) fn timer_counts(&self) -> (usize, usize) {
440        let state = self.lock();
441        (state.timers.len(), state.fired.len())
442    }
443
444    /// Arms a public timer, whose channel stays connected after delivery.
445    #[cfg(feature = "crossbeam")]
446    pub(crate) fn at(&self, deadline: Instant) -> Receiver<Instant> {
447        let mut state = self.lock();
448        let (_, receiver) = state.arm_timer(deadline, true);
449        self.changed.notify_all();
450        receiver
451    }
452
453    /// Receives until the clock reaches `deadline`, where a message or a
454    /// disconnection wins over expiry, as in crossbeam.
455    ///
456    /// The receiver is never checked under the clock lock, since a rendezvous
457    /// receive can wait on a sender that reads the clock. Expiry is decided only
458    /// at a time no advance changed since the check, and an advance delivers its
459    /// due timers before anyone reads its time, so a timer due by the deadline
460    /// holds its message by then.
461    #[cfg(feature = "crossbeam")]
462    pub(crate) fn recv_deadline<T>(
463        &self,
464        receiver: &Receiver<T>,
465        deadline: Instant,
466    ) -> Result<T, RecvTimeoutError> {
467        // Check the receiver at a known time, and look again if an advance ran meanwhile
468        let (key, timeout) = loop {
469            let seen = self.now();
470            match receiver.try_recv() {
471                Ok(value) => return Ok(value),
472                Err(TryRecvError::Disconnected) => return Err(RecvTimeoutError::Disconnected),
473                Err(TryRecvError::Empty) => {}
474            }
475            let mut state = self.lock();
476            if state.now != seen {
477                continue;
478            }
479
480            // Decide expiry at the checked time, or list the timeout before unlocking so
481            // that no advance slips between and wait_timers counts it
482            if seen >= deadline {
483                return Err(RecvTimeoutError::Timeout);
484            }
485            let timer = state.arm_timer(deadline, false);
486            self.changed.notify_all();
487            break timer;
488        };
489
490        // Unlist the timeout on every return from the adapter
491        let _registration = ReceiveTimer { paused: self, key };
492
493        // Wait on the receiver and the clock's timer, with no real timeout
494        crossbeam_channel::select! {
495            recv(receiver) -> result => result.map_err(RecvTimeoutError::from),
496            recv(timeout) -> _ => {
497                // Let tests make the receiver ready after the timer has won
498                #[cfg(test)]
499                {
500                    let hook = self.after_timer_receive.lock().unwrap().take();
501                    if let Some(hook) = hook {
502                        hook();
503                    }
504                }
505
506                // Wait out an advance still delivering, then recheck without the lock, since
507                // select may pick the timeout before a message due at the same time
508                drop(self.lock());
509                receiver.try_recv().map_err(|err| match err {
510                    TryRecvError::Empty => RecvTimeoutError::Timeout,
511                    TryRecvError::Disconnected => RecvTimeoutError::Disconnected,
512                })
513            }
514        }
515    }
516
517    /// Returns the clock's current monotonic time.
518    pub(crate) fn now(&self) -> Instant {
519        self.lock().now
520    }
521
522    /// Returns the clock's current wall time.
523    pub(crate) fn system_time(&self) -> SystemTime {
524        self.lock().system_time()
525    }
526
527    /// Reads the advance and wall time together for formatting.
528    pub(crate) fn snapshot(&self) -> (Duration, SystemTime) {
529        let state = self.lock();
530        (state.now - self.start, state.system_time())
531    }
532
533    /// Counts a wait and lists its deadline until the guard drops, or returns
534    /// `None` if the deadline has already been reached.
535    ///
536    /// Advances collect the waits to wake under the same lock, so a wait either
537    /// registers in time to be woken or sees the new time. The caller holds its
538    /// signal lock until it waits, so the wake cannot arrive before the wait.
539    pub(crate) fn register(
540        &self,
541        deadline: Option<Instant>,
542        signal: &Arc<Signal>,
543    ) -> Option<Registration<'_>> {
544        // Refuse a deadline that an advance has already reached
545        let mut state = self.lock();
546        if deadline.is_some_and(|deadline| deadline <= state.now) {
547            return None;
548        }
549
550        // Count the wait, and list a timed one under its own key
551        let key = deadline.map(|deadline| state.next_key(deadline));
552        state.registered += 1;
553        if let Some(key) = key {
554            state.deadlines.insert(key, signal.clone());
555        }
556
557        // Wake drivers waiting for the count to grow
558        self.changed.notify_all();
559        Some(Registration { paused: self, key })
560    }
561
562    /// Takes the one-shot test hook before registration, or for a wait
563    /// `again` after a spurious wakeup, for the caller to run with no lock held.
564    #[cfg(test)]
565    pub(crate) fn take_hook(&self, again: bool) -> Option<BeforeWait> {
566        let hook = if again {
567            &self.before_rewait
568        } else {
569            &self.before_wait
570        };
571        hook.lock().unwrap().take()
572    }
573
574    /// Locks the state, recovering it from poisoning, since no update under the
575    /// lock can stop halfway.
576    fn lock(&self) -> MutexGuard<'_, PausedState> {
577        self.state.lock().unwrap_or_else(PoisonError::into_inner)
578    }
579}
580
581/// A timer's one message, and whether its channel outlives delivery.
582#[cfg(feature = "crossbeam")]
583struct Timer {
584    /// Deadline sent as the message, even when an advance overshoots it.
585    deadline: Instant,
586    /// Sender of the capacity-1 channel, sent to only once under the clock lock.
587    sender: Sender<Instant>,
588    /// Whether the clock keeps the sender after delivery, as for public timers.
589    retain: bool,
590}
591
592/// Removes a receive adapter's timer when it returns, even before expiry.
593#[cfg(feature = "crossbeam")]
594struct ReceiveTimer<'a> {
595    /// Clock holding the armed timer, if it has not fired yet.
596    paused: &'a Paused,
597    /// Key of this receive's timer in the clock's list.
598    key: (Instant, u64),
599}
600
601#[cfg(feature = "crossbeam")]
602impl Drop for ReceiveTimer<'_> {
603    /// Unlists an unfired timer without retaining its sender.
604    fn drop(&mut self) {
605        self.paused.lock().timers.remove(&self.key);
606    }
607}
608
609/// Counts one registered wait and lists its deadline until the wait returns.
610pub(crate) struct Registration<'a> {
611    /// Clock whose registration count includes this thread.
612    paused: &'a Paused,
613    /// Key of this wait's listed deadline, if it is timed.
614    key: Option<(Instant, u64)>,
615}
616
617impl Drop for Registration<'_> {
618    /// Removes this wait and its deadline from the clock's registrations.
619    fn drop(&mut self) {
620        // Uncount the wait and unlist its deadline
621        let mut state = self.paused.lock();
622        state.registered -= 1;
623        if let Some(key) = self.key {
624            state.deadlines.remove(&key);
625        }
626
627        // The test helper wait_unregistered waits for this count to drop
628        #[cfg(test)]
629        self.paused.changed.notify_all();
630    }
631}
632
633/// Pauses a test wait before registration and exposes its real timer, if any.
634#[cfg(test)]
635pub(crate) type BeforeWait = Box<dyn FnOnce(Option<Instant>) + Send>;
636
637/// Observes a parked wait barrier that cannot yet return.
638#[cfg(test)]
639pub(crate) type BeforeParkedWait = Box<dyn FnOnce() + Send>;
640
641/// Observes a timer race with no crate lock held.
642#[cfg(all(test, feature = "crossbeam"))]
643type TimerHook = Box<dyn FnOnce() + Send>;
644
645// The timer tests live in src/tests, loaded from here so that they keep this
646// module's private items in reach
647#[cfg(all(test, feature = "crossbeam", not(loom)))]
648#[cfg_attr(coverage_nightly, coverage(off))]
649#[path = "tests/paused.rs"]
650mod tests;