darkbio_clock/lib.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// Pull in the README as the package doc
8#![doc = include_str!("../README.md")]
9// Enable the experimental doc_cfg feature
10#![cfg_attr(docsrs, feature(doc_cfg))]
11// Allow excluding test code from coverage measurements on nightly
12#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
13// The crate only builds on safe synchronization and never needs unsafe
14#![forbid(unsafe_code)]
15
16pub mod sync;
17
18// Every internal lock comes from here, so loom can swap in its own when model
19// checking the waits
20mod primitives;
21
22#[cfg(feature = "crossbeam")]
23mod timers;
24
25/// The crossbeam-channel crate that the clock's timers use, for naming its
26/// types at the same version.
27#[cfg(feature = "crossbeam")]
28#[cfg_attr(docsrs, doc(cfg(feature = "crossbeam")))]
29pub use crossbeam_channel;
30
31// Test clocks exist only in tests, so a build without the test-clock feature
32// cannot stop its own time
33#[cfg(any(test, feature = "test-clock"))]
34mod paused;
35
36#[cfg(feature = "test-clock")]
37pub use paused::TestClock;
38#[cfg(all(test, not(feature = "test-clock")))]
39use paused::TestClock;
40
41use std::fmt;
42use std::sync::Arc;
43use std::time::{Duration, Instant, SystemTime};
44
45use primitives::{Condvar, Mutex, MutexGuard};
46
47/// A clock that reads real time, or a test's time that moves only on command.
48///
49/// Clones share one clock. Equality compares identity, so all real clocks are
50/// equal and a test clock equals only the handles of its own `TestClock`.
51#[derive(Clone)]
52pub struct Clock {
53 /// Shared state of a test clock, or nothing for the real clock.
54 #[cfg(any(test, feature = "test-clock"))]
55 paused: Option<Arc<paused::Paused>>,
56}
57
58impl Clock {
59 /// Returns the real clock, which reads the system's monotonic and wall times.
60 pub const fn real() -> Self {
61 Self {
62 #[cfg(any(test, feature = "test-clock"))]
63 paused: None,
64 }
65 }
66
67 /// Returns the clock's current monotonic time.
68 pub fn now(&self) -> Instant {
69 #[cfg(any(test, feature = "test-clock"))]
70 if let Some(paused) = &self.paused {
71 return paused.now();
72 }
73 Instant::now()
74 }
75
76 /// Returns the time since `since`, or zero if it is later than now.
77 pub fn elapsed(&self, since: Instant) -> Duration {
78 self.now().saturating_duration_since(since)
79 }
80
81 /// Returns the clock's current wall time.
82 pub fn system_time(&self) -> SystemTime {
83 #[cfg(any(test, feature = "test-clock"))]
84 if let Some(paused) = &self.paused {
85 return paused.system_time();
86 }
87 SystemTime::now()
88 }
89
90 /// Blocks the thread until `duration` has passed on this clock.
91 ///
92 /// On a test clock, only its advances end the sleep.
93 ///
94 /// # Panics
95 ///
96 /// Panics on a test clock if the deadline overflows [`Instant`].
97 pub fn sleep(&self, duration: Duration) {
98 #[cfg(any(test, feature = "test-clock"))]
99 if self.paused.is_some() {
100 self.sleep_until(self.now() + duration);
101 return;
102 }
103 std::thread::sleep(duration);
104 }
105
106 /// Blocks the thread until this clock reaches `deadline`, returning at once
107 /// if it already has.
108 ///
109 /// On a test clock, only its advances end the sleep.
110 pub fn sleep_until(&self, deadline: Instant) {
111 self.waiter().wait_until(Some(deadline), || None::<()>);
112 }
113
114 /// Returns the shared state's address, or nothing for the real clock.
115 fn identity(&self) -> Option<*const ()> {
116 #[cfg(any(test, feature = "test-clock"))]
117 if let Some(paused) = &self.paused {
118 return Some(Arc::as_ptr(paused).cast());
119 }
120 None
121 }
122
123 /// Creates a waiter that measures deadlines on this clock.
124 fn waiter(&self) -> Waiter {
125 // Register the signal before any wait can observe the clock
126 let signal = Arc::new(Signal::default());
127 #[cfg(any(test, feature = "test-clock"))]
128 if let Some(paused) = &self.paused {
129 paused.register(&signal);
130 }
131
132 // Keep the registration alive for the waiter's lifetime
133 Waiter {
134 clock: self.clone(),
135 signal,
136 }
137 }
138}
139
140// A production clock carries no state, so passing one around costs nothing
141#[cfg(not(any(test, feature = "test-clock")))]
142const _: () = assert!(size_of::<Clock>() == 0);
143
144impl PartialEq for Clock {
145 /// Compares identity, with all real clocks equal to each other.
146 fn eq(&self, other: &Self) -> bool {
147 self.identity() == other.identity()
148 }
149}
150
151impl Eq for Clock {}
152
153impl fmt::Debug for Clock {
154 /// Shows whether the clock is paused, its advance and its wall time.
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 let mut clock = f.debug_struct("Clock");
157 #[cfg(any(test, feature = "test-clock"))]
158 if let Some(paused) = &self.paused {
159 return clock
160 .field("paused", &true)
161 .field("advanced", &paused.advanced())
162 .field("system_time", &paused.system_time())
163 .finish();
164 }
165 clock.field("paused", &false).finish()
166 }
167}
168
169/// Blocks threads until a condition holds or a deadline passes on a clock.
170struct Waiter {
171 /// Clock that decides when deadlines pass.
172 clock: Clock,
173 /// Wakeups registered with a test clock until this waiter drops.
174 signal: Arc<Signal>,
175}
176
177impl Waiter {
178 /// Blocks until `ready` returns a value or the clock reaches `deadline`.
179 ///
180 /// A ready value wins over an expired deadline. Without a deadline, only
181 /// a ready value ends the wait. The callback runs without crate locks held.
182 fn wait_until<T>(
183 &self,
184 deadline: Option<Instant>,
185 mut ready: impl FnMut() -> Option<T>,
186 ) -> Option<T> {
187 // Pick the real timer once, so the test seam reports what every park uses
188 let timer = self.timer(deadline);
189
190 // Check and park in turns, until a value arrives or the deadline passes
191 loop {
192 // Take the generation before checking, so a change after the check cuts the park short
193 let seen = self.signal.generation();
194 if let Some(value) = ready() {
195 return Some(value);
196 }
197 if deadline.is_some_and(|deadline| self.clock.now() >= deadline) {
198 return None;
199 }
200 drop(self.park(self.signal.lock(), seen, deadline, timer));
201 }
202 }
203
204 /// Returns the deadline as a real timer on a real clock, and no timer on a
205 /// test clock, whose advances end its waits.
206 fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
207 #[cfg(any(test, feature = "test-clock"))]
208 if self.clock.paused.is_some() {
209 return None;
210 }
211 deadline
212 }
213
214 /// Parks until the generation moves on from `seen` or the clock reaches
215 /// `deadline`, and returns the state locked again.
216 ///
217 /// Only a real clock's park uses a real timer. In tests, one-shot hooks run
218 /// before its first wait and before a wait after a spurious wakeup, with
219 /// the signal unlocked.
220 fn park<'a>(
221 &'a self,
222 mut state: MutexGuard<'a, SignalState>,
223 seen: u64,
224 deadline: Option<Instant>,
225 timer: Option<Instant>,
226 ) -> MutexGuard<'a, SignalState> {
227 // Count the park once, keeping it counted through spurious wakeups
228 #[cfg(any(test, feature = "test-clock"))]
229 let mut blocked = None;
230
231 loop {
232 // Stop once the generation moves or the clock reaches the deadline
233 let now = self.clock.now();
234 if state.generation != seen || deadline.is_some_and(|deadline| now >= deadline) {
235 break;
236 }
237
238 // Let tests act before a wait, then check again for any change they made
239 #[cfg(test)]
240 if let Some(hook) = self
241 .clock
242 .paused
243 .as_ref()
244 .and_then(|paused| paused.take_hook(blocked.is_some()))
245 {
246 drop(state);
247 hook(timer);
248 state = self.signal.lock();
249 continue;
250 }
251
252 // Count the park while the signal is locked, so that an advance seen after
253 // the count still wakes it
254 #[cfg(any(test, feature = "test-clock"))]
255 if blocked.is_none() {
256 blocked = self
257 .clock
258 .paused
259 .as_ref()
260 .map(|paused| paused.block(deadline));
261 }
262 #[cfg(test)]
263 {
264 state.parks += 1;
265 self.signal.parked.notify_all();
266 }
267
268 // Wait for a wakeup or the real timer, leaving expiry to the check above
269 state = match timer {
270 None => self
271 .signal
272 .changed
273 .wait(state)
274 .expect("waiter signal not poisoned"),
275 Some(timer) => {
276 self.signal
277 .changed
278 .wait_timeout(state, timer - now)
279 .expect("waiter signal not poisoned")
280 .0
281 }
282 };
283 }
284
285 // Uncount the park before the caller acts on its wakeup
286 #[cfg(any(test, feature = "test-clock"))]
287 drop(blocked);
288 state
289 }
290}
291
292#[cfg(any(test, feature = "test-clock"))]
293impl Drop for Waiter {
294 /// Removes the waiter's registration as soon as it is no longer live.
295 fn drop(&mut self) {
296 if let Some(paused) = &self.clock.paused {
297 paused.unregister(&self.signal);
298 }
299 }
300}
301
302impl fmt::Debug for Waiter {
303 /// Shows the clock, never the wakeup bookkeeping.
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 f.debug_struct("Waiter")
306 .field("clock", &self.clock)
307 .finish_non_exhaustive()
308 }
309}
310
311/// Wakes threads through a generation read before each condition check.
312#[derive(Default)]
313struct Signal {
314 /// Counters and the test park count, guarded together.
315 state: Mutex<SignalState>,
316 /// Wakes parked threads when the generation moves.
317 changed: Condvar,
318 /// Reports new parks to tests, without waking parked threads.
319 #[cfg(test)]
320 parked: Condvar,
321}
322
323/// Mutable part of a signal.
324#[derive(Default)]
325struct SignalState {
326 /// Number of notifications and clock advances, wrapping on overflow.
327 generation: u64,
328 /// Number of notifications alone, which end condvar waits, wrapping on overflow.
329 notifications: u64,
330 /// Total parks that found the generation unchanged, for test synchronization.
331 #[cfg(test)]
332 parks: usize,
333}
334
335impl Signal {
336 /// Locks the state, which no code panics under.
337 fn lock(&self) -> MutexGuard<'_, SignalState> {
338 self.state.lock().expect("waiter signal not poisoned")
339 }
340
341 /// Returns the current generation, to compare against when parking.
342 fn generation(&self) -> u64 {
343 self.lock().generation
344 }
345
346 /// Counts a notification and wakes one parked thread.
347 ///
348 /// Every waiting thread sees the count, so another one may return too,
349 /// as a spurious wakeup, the next time it wakes.
350 fn notify_one(&self) {
351 let mut state = self.lock();
352 state.generation = state.generation.wrapping_add(1);
353 state.notifications = state.notifications.wrapping_add(1);
354 self.changed.notify_one();
355 }
356
357 /// Counts a notification and wakes every parked thread.
358 fn notify_all(&self) {
359 let mut state = self.lock();
360 state.generation = state.generation.wrapping_add(1);
361 state.notifications = state.notifications.wrapping_add(1);
362 self.changed.notify_all();
363 }
364
365 /// Wakes every parked thread to recheck the time, without counting a notification.
366 #[cfg(any(test, feature = "test-clock"))]
367 fn advance(&self) {
368 let mut state = self.lock();
369 state.generation = state.generation.wrapping_add(1);
370 self.changed.notify_all();
371 }
372}
373
374// The tests live in src/tests, apart from the code they check
375#[cfg(test)]
376#[cfg_attr(coverage_nightly, coverage(off))]
377mod tests;