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 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, since an advance never counts as a notification. A zero
74 /// 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, since an advance never counts as a notification. Advancing to
92 /// 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, and the result is then the
158 /// current time. Await an advance's effect before reading the next
159 /// deadline. A timer leaves the list when it fires.
160 pub fn next_deadline(&self) -> Option<Instant> {
161 // Compare the earliest parked wait with the earliest unfired timer, and report
162 // one already reached at the current time, so that advancing to it stays valid
163 let state = self.paused.lock();
164 let deadline = state.deadlines.keys().next().map(|&(deadline, _)| deadline);
165 #[cfg(feature = "crossbeam")]
166 let deadline = deadline
167 .into_iter()
168 .chain(state.timers.keys().next().map(|&(deadline, _)| deadline))
169 .min();
170 deadline.map(|deadline| deadline.max(state.now))
171 }
172
173 /// Validates both new times, publishes them together with the due timers'
174 /// messages, then wakes the reached waits.
175 fn advance_with(&mut self, next: impl FnOnce(Instant) -> Instant) {
176 // Check the new times before taking the lock, so a failed check panics with
177 // no lock held. Only the owner advances, so nothing changes them in between.
178 let (now, wall) = {
179 let state = self.paused.lock();
180 (state.now, state.wall)
181 };
182 let next = next(now);
183 if next == now {
184 return;
185 }
186 wall.at(next).expect("clock advance overflows SystemTime");
187
188 // Publish the time and collect each reached wait's signal once, in the same
189 // lock hold that parks register in. The wakes follow deadline and park order,
190 // since the address set only filters out repeats.
191 let mut state = self.paused.lock();
192 state.now = next;
193 let mut seen = HashSet::new();
194 let signals: Vec<_> = state
195 .deadlines
196 .range(..=(next, u64::MAX))
197 .filter(|(_, signal)| seen.insert(Arc::as_ptr(signal)))
198 .map(|(_, signal)| signal.clone())
199 .collect();
200
201 // Deliver every due timer before another thread can read the new time
202 #[cfg(feature = "crossbeam")]
203 {
204 while let Some(entry) = state
205 .timers
206 .first_entry()
207 .filter(|entry| entry.key().0 <= next)
208 {
209 let timer = entry.remove();
210 state.fire(timer);
211 }
212 }
213 drop(state);
214
215 // Wake outside the clock lock, since parking takes the signal lock first
216 for signal in signals {
217 signal.wake();
218 }
219 }
220}
221
222impl Default for TestClock {
223 /// Creates a stopped clock at the current real monotonic and wall times.
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl fmt::Debug for TestClock {
230 /// Shows the advance, wall time, parked threads and armed timers.
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 // Snapshot the counts before calling the formatter's writer
233 let state = self.paused.lock();
234 let advanced = state.now - self.paused.start;
235 let system_time = state.system_time();
236 let blocked = state.blocked;
237 #[cfg(feature = "crossbeam")]
238 let timers = state.timers.len();
239 drop(state);
240
241 // Format without the clock lock, since the writer may read this clock
242 let mut debug = f.debug_struct("TestClock");
243 debug
244 .field("advanced", &advanced)
245 .field("system_time", &system_time)
246 .field("blocked", &blocked);
247 #[cfg(feature = "crossbeam")]
248 debug.field("timers", &timers);
249 debug.finish()
250 }
251}
252
253/// A test clock's state, shared by its owner, handles and waiters.
254pub(crate) struct Paused {
255 /// Time the clock started at, to show how far it has advanced.
256 start: Instant,
257 /// Current times, parked thread count, deadlines and timers.
258 pub(crate) state: Mutex<PausedState>,
259 /// Wakes drivers when parked threads or armed timers change.
260 pub(crate) changed: Condvar,
261 /// One-shot test hook, run before a park's first wait with no clock or
262 /// signal lock held.
263 #[cfg(test)]
264 pub(crate) before_park: std::sync::Mutex<Option<BeforePark>>,
265 /// One-shot test hook, run like `before_park` before a park waits again
266 /// after a spurious wakeup.
267 #[cfg(test)]
268 pub(crate) before_rewait: std::sync::Mutex<Option<BeforePark>>,
269 /// One-shot test hook, run when a receive's timer wins, before the receiver
270 /// is checked again, with no crate lock held.
271 #[cfg(all(test, feature = "crossbeam"))]
272 after_timer_receive: std::sync::Mutex<Option<TimerHook>>,
273}
274
275/// Mutable part of a test clock.
276pub(crate) struct PausedState {
277 /// Current monotonic time.
278 now: Instant,
279 /// Wall time as last set, with the monotonic instant it was set at.
280 wall: WallAnchor,
281 /// Last sequence number given to a timed park or timer, which a `u64`
282 /// cannot outgrow within any run.
283 sequence: u64,
284 /// Threads committed to parking while holding their signal's lock.
285 pub(crate) blocked: usize,
286 /// Parked timed waits by deadline and park order, each with the signal
287 /// that wakes it, and each unlisted when its park ends.
288 deadlines: BTreeMap<(Instant, u64), Arc<Signal>>,
289 /// Unfired timers by deadline and arming order.
290 #[cfg(feature = "crossbeam")]
291 timers: BTreeMap<(Instant, u64), Timer>,
292 /// Senders of delivered public timers, kept so their channels stay connected
293 /// while the clock lives.
294 #[cfg(feature = "crossbeam")]
295 fired: Vec<Sender<Instant>>,
296}
297
298impl PausedState {
299 /// Keys a timed park or timer by its deadline, ordering equal deadlines by
300 /// when they were listed.
301 fn next_key(&mut self, deadline: Instant) -> (Instant, u64) {
302 self.sequence += 1;
303 (deadline, self.sequence)
304 }
305
306 /// Returns the wall time at the current monotonic time.
307 fn system_time(&self) -> SystemTime {
308 self.wall
309 .at(self.now)
310 .expect("wall time fits, since every change checks it first")
311 }
312
313 /// Lists a timer until an advance reaches it, or delivers it at once if it
314 /// is already due.
315 #[cfg(feature = "crossbeam")]
316 fn arm_timer(
317 &mut self,
318 deadline: Instant,
319 retain: bool,
320 ) -> ((Instant, u64), Receiver<Instant>) {
321 // Give the timer a sequence number and room for its only message
322 let key = self.next_key(deadline);
323 let (sender, receiver) = crossbeam_channel::bounded(1);
324 let timer = Timer {
325 deadline,
326 sender,
327 retain,
328 };
329
330 // List or deliver it in the lock hold that read the time, so no advance slips between
331 if deadline > self.now {
332 self.timers.insert(key, timer);
333 } else {
334 self.fire(timer);
335 }
336 (key, receiver)
337 }
338
339 /// Sends a timer's deadline, and keeps the sender of a delivered public timer.
340 #[cfg(feature = "crossbeam")]
341 fn fire(&mut self, timer: Timer) {
342 // The capacity-1 channel has never been sent to, so this cannot block
343 let delivered = timer.sender.send(timer.deadline).is_ok();
344
345 // Keep a delivered public timer's sender, so its channel stays connected like
346 // crossbeam's. Nothing reports a dropped receiver without sending, and a second
347 // send would refill a consumed timer.
348 if delivered && timer.retain {
349 self.fired.push(timer.sender);
350 }
351 }
352}
353
354/// A wall time and the monotonic instant it was set at.
355#[derive(Clone, Copy)]
356struct WallAnchor {
357 /// Wall time when the clock was created or last set.
358 time: SystemTime,
359 /// Monotonic time at that moment.
360 instant: Instant,
361}
362
363impl WallAnchor {
364 /// Returns the wall time at `instant`, or `None` if it does not fit.
365 ///
366 /// The whole span since the anchor is added at once, so a platform that
367 /// rounds wall time, like Windows to 100 ns, rounds once and never per advance.
368 fn at(&self, instant: Instant) -> Option<SystemTime> {
369 self.time.checked_add(instant - self.instant)
370 }
371}
372
373impl Paused {
374 /// Returns armed timers and retained senders for the bookkeeping model.
375 #[cfg(all(test, feature = "crossbeam", not(loom)))]
376 pub(crate) fn timer_counts(&self) -> (usize, usize) {
377 let state = self.lock();
378 (state.timers.len(), state.fired.len())
379 }
380
381 /// Arms a public timer, whose channel stays connected after delivery.
382 #[cfg(feature = "crossbeam")]
383 pub(crate) fn at(&self, deadline: Instant) -> Receiver<Instant> {
384 let mut state = self.lock();
385 let (_, receiver) = state.arm_timer(deadline, true);
386 self.changed.notify_all();
387 receiver
388 }
389
390 /// Receives until the clock reaches `deadline`, where a message or a
391 /// disconnection wins over expiry, as in crossbeam.
392 ///
393 /// The receiver is never checked under the clock lock, since a rendezvous
394 /// receive can wait on a sender that reads the clock. Expiry is decided only
395 /// at a time no advance changed since the check, and an advance delivers its
396 /// due timers before anyone reads its time, so a timer due by the deadline
397 /// holds its message by then.
398 #[cfg(feature = "crossbeam")]
399 pub(crate) fn recv_deadline<T>(
400 &self,
401 receiver: &Receiver<T>,
402 deadline: Instant,
403 ) -> Result<T, RecvTimeoutError> {
404 // Check the receiver at a known time, and look again if an advance ran meanwhile
405 let (key, timeout) = loop {
406 let seen = self.now();
407 match receiver.try_recv() {
408 Ok(value) => return Ok(value),
409 Err(TryRecvError::Disconnected) => return Err(RecvTimeoutError::Disconnected),
410 Err(TryRecvError::Empty) => {}
411 }
412 let mut state = self.lock();
413 if state.now != seen {
414 continue;
415 }
416
417 // Decide expiry at the checked time, or list the timeout before unlocking so
418 // that no advance slips between and wait_timers counts it
419 if seen >= deadline {
420 return Err(RecvTimeoutError::Timeout);
421 }
422 let timer = state.arm_timer(deadline, false);
423 self.changed.notify_all();
424 break timer;
425 };
426
427 // Unlist the timeout on every return from the adapter
428 let _registration = ReceiveTimer { paused: self, key };
429
430 // Wait on the receiver and the clock's timer, with no real timeout
431 crossbeam_channel::select! {
432 recv(receiver) -> result => result.map_err(RecvTimeoutError::from),
433 recv(timeout) -> _ => {
434 // Let tests make the receiver ready after the timer has won
435 #[cfg(test)]
436 {
437 let hook = self.after_timer_receive.lock().unwrap().take();
438 if let Some(hook) = hook {
439 hook();
440 }
441 }
442
443 // Wait out an advance still delivering, then recheck without the lock, since
444 // select may pick the timeout before a message due at the same time
445 drop(self.lock());
446 receiver.try_recv().map_err(|err| match err {
447 TryRecvError::Empty => RecvTimeoutError::Timeout,
448 TryRecvError::Disconnected => RecvTimeoutError::Disconnected,
449 })
450 }
451 }
452 }
453
454 /// Returns the clock's current monotonic time.
455 pub(crate) fn now(&self) -> Instant {
456 self.lock().now
457 }
458
459 /// Returns the clock's current wall time.
460 pub(crate) fn system_time(&self) -> SystemTime {
461 self.lock().system_time()
462 }
463
464 /// Reads the advance and wall time together for formatting.
465 pub(crate) fn snapshot(&self) -> (Duration, SystemTime) {
466 let state = self.lock();
467 (state.now - self.start, state.system_time())
468 }
469
470 /// Counts a park and lists its deadline until the guard drops, or returns
471 /// `None` if the deadline has already been reached.
472 ///
473 /// Advances collect the waits to wake under the same lock, so a park either
474 /// registers in time to be woken or sees the new time. The caller holds its
475 /// signal lock until it waits, so the wake cannot arrive before the wait.
476 pub(crate) fn block(
477 &self,
478 deadline: Option<Instant>,
479 signal: &Arc<Signal>,
480 ) -> Option<Blocked<'_>> {
481 // Refuse a deadline that an advance has already reached
482 let mut state = self.lock();
483 if deadline.is_some_and(|deadline| deadline <= state.now) {
484 return None;
485 }
486
487 // Count the park, and list a timed one under its own key
488 let key = deadline.map(|deadline| state.next_key(deadline));
489 state.blocked += 1;
490 if let Some(key) = key {
491 state.deadlines.insert(key, signal.clone());
492 }
493
494 // Wake drivers waiting for the count to grow
495 self.changed.notify_all();
496 Some(Blocked { paused: self, key })
497 }
498
499 /// Takes the one-shot test hook for a park's first wait, or for a wait
500 /// `again` after a spurious wakeup, for the caller to run with no lock held.
501 #[cfg(test)]
502 pub(crate) fn take_hook(&self, again: bool) -> Option<BeforePark> {
503 let hook = if again {
504 &self.before_rewait
505 } else {
506 &self.before_park
507 };
508 hook.lock().unwrap().take()
509 }
510
511 /// Locks the state, recovering it from poisoning, since no update under the
512 /// lock can stop halfway.
513 fn lock(&self) -> MutexGuard<'_, PausedState> {
514 self.state.lock().unwrap_or_else(PoisonError::into_inner)
515 }
516}
517
518/// A timer's one message, and whether its channel outlives delivery.
519#[cfg(feature = "crossbeam")]
520struct Timer {
521 /// Deadline sent as the message, even when an advance overshoots it.
522 deadline: Instant,
523 /// Sender of the capacity-1 channel, sent to only once under the clock lock.
524 sender: Sender<Instant>,
525 /// Whether the clock keeps the sender after delivery, as for public timers.
526 retain: bool,
527}
528
529/// Removes a receive adapter's timer when it returns, even before expiry.
530#[cfg(feature = "crossbeam")]
531struct ReceiveTimer<'a> {
532 /// Clock holding the armed timer, if it has not fired yet.
533 paused: &'a Paused,
534 /// Key of this receive's timer in the clock's list.
535 key: (Instant, u64),
536}
537
538#[cfg(feature = "crossbeam")]
539impl Drop for ReceiveTimer<'_> {
540 /// Unlists an unfired timer without retaining its sender.
541 fn drop(&mut self) {
542 self.paused.lock().timers.remove(&self.key);
543 }
544}
545
546/// Counts one park, and lists its deadline, from its first wait until it returns.
547pub(crate) struct Blocked<'a> {
548 /// Clock whose parked count includes this thread.
549 paused: &'a Paused,
550 /// Key of this park's listed deadline, if it is timed.
551 key: Option<(Instant, u64)>,
552}
553
554impl Drop for Blocked<'_> {
555 /// Removes this thread and its deadline from the clock's parked state.
556 fn drop(&mut self) {
557 // Uncount the park and unlist its deadline
558 let mut state = self.paused.lock();
559 state.blocked -= 1;
560 if let Some(key) = self.key {
561 state.deadlines.remove(&key);
562 }
563
564 // Only the test helper wait_unblocked waits for this count to drop
565 #[cfg(test)]
566 self.paused.changed.notify_all();
567 }
568}
569
570/// Pauses a test wait before parking and exposes its real timer, if any.
571#[cfg(test)]
572pub(crate) type BeforePark = Box<dyn FnOnce(Option<Instant>) + Send>;
573
574/// Observes a timer race with no crate lock held.
575#[cfg(all(test, feature = "crossbeam"))]
576type TimerHook = Box<dyn FnOnce() + Send>;
577
578// The timer tests live in src/tests, loaded from here so that they keep this
579// module's private items in reach
580#[cfg(all(test, feature = "crossbeam", not(loom)))]
581#[cfg_attr(coverage_nightly, coverage(off))]
582#[path = "tests/paused.rs"]
583mod tests;