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;