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