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