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;
43#[cfg(test)]
44use std::sync::atomic::{AtomicUsize, Ordering};
45use std::time::{Duration, Instant, SystemTime};
46
47use primitives::{Condvar, Mutex, MutexGuard};
48
49/// Longest real wait handed to the OS in one call, since Windows waits forever
50/// on timeouts of about 49.7 days or more.
51const MAX_REAL_WAIT: Duration = Duration::from_secs(24 * 60 * 60);
52
53/// A clock that reads real time, or a test's time that moves only on command.
54///
55/// Clones share one clock. Equality compares identity, so all real clocks are
56/// equal and a test clock equals only the handles of its own `TestClock`.
57///
58/// It cannot be built from a struct literal and it has a destructor in every
59/// build, so code that compiles without `test-clock` compiles with it too.
60#[derive(Clone)]
61#[non_exhaustive]
62pub struct Clock {
63 /// Shared state of a test clock, or nothing for the real clock.
64 #[cfg(any(test, feature = "test-clock"))]
65 paused: Option<Arc<paused::Paused>>,
66}
67
68impl Clock {
69 /// Returns the real clock, which reads the system's monotonic and wall times.
70 pub const fn real() -> Self {
71 Self {
72 #[cfg(any(test, feature = "test-clock"))]
73 paused: None,
74 }
75 }
76
77 /// Returns the clock's current monotonic time.
78 #[must_use]
79 pub fn now(&self) -> Instant {
80 #[cfg(any(test, feature = "test-clock"))]
81 if let Some(paused) = &self.paused {
82 return paused.now();
83 }
84 Instant::now()
85 }
86
87 /// Returns the time since `since`, or zero if it is later than now.
88 #[must_use]
89 pub fn elapsed(&self, since: Instant) -> Duration {
90 self.now().saturating_duration_since(since)
91 }
92
93 /// Returns the clock's current wall time.
94 #[must_use]
95 pub fn system_time(&self) -> SystemTime {
96 #[cfg(any(test, feature = "test-clock"))]
97 if let Some(paused) = &self.paused {
98 return paused.system_time();
99 }
100 SystemTime::now()
101 }
102
103 /// Blocks the thread until `duration` has passed on this clock.
104 ///
105 /// On a test clock, only its advances end the sleep.
106 ///
107 /// # Panics
108 ///
109 /// Panics on a test clock if the deadline overflows [`Instant`].
110 pub fn sleep(&self, duration: Duration) {
111 #[cfg(any(test, feature = "test-clock"))]
112 if self.paused.is_some() {
113 self.sleep_until(self.now() + duration);
114 return;
115 }
116 std::thread::sleep(duration);
117 }
118
119 /// Blocks the thread until this clock reaches `deadline`, returning at once
120 /// if it already has.
121 ///
122 /// On a test clock, only its advances end the sleep.
123 pub fn sleep_until(&self, deadline: Instant) {
124 // A test clock's sleep parks until an advance reaches its deadline
125 #[cfg(any(test, feature = "test-clock"))]
126 if self.paused.is_some() {
127 self.waiter().wait_until(Some(deadline), || None::<()>);
128 return;
129 }
130
131 // A real sleep waits in capped slices, rechecking the deadline after each
132 loop {
133 let now = Instant::now();
134 if now >= deadline {
135 return;
136 }
137 std::thread::sleep((deadline - now).min(MAX_REAL_WAIT));
138 }
139 }
140
141 /// Returns the shared state's address, or nothing for the real clock.
142 fn identity(&self) -> Option<*const ()> {
143 #[cfg(any(test, feature = "test-clock"))]
144 if let Some(paused) = &self.paused {
145 return Some(Arc::as_ptr(paused).cast());
146 }
147 None
148 }
149
150 /// Creates a waiter that measures deadlines on this clock.
151 fn waiter(&self) -> Waiter {
152 // Register the signal before any wait can observe the clock
153 let signal = Arc::new(Signal::default());
154 #[cfg(any(test, feature = "test-clock"))]
155 if let Some(paused) = &self.paused {
156 paused.register(&signal);
157 }
158
159 // Keep the registration alive for the waiter's lifetime
160 Waiter {
161 clock: self.clone(),
162 signal,
163 }
164 }
165}
166
167// A production clock carries no state, so passing one around costs nothing
168#[cfg(not(any(test, feature = "test-clock")))]
169const _: () = assert!(size_of::<Clock>() == 0);
170
171// A production clock still has a destructor, as a test build's does
172#[cfg(not(any(test, feature = "test-clock")))]
173const _: () = assert!(std::mem::needs_drop::<Clock>());
174
175impl Drop for Clock {
176 /// Does nothing, but gives every build a destructor, so enabling
177 /// `test-clock` does not change which const contexts accept a clock.
178 fn drop(&mut self) {}
179}
180
181impl PartialEq for Clock {
182 /// Compares identity, with all real clocks equal to each other.
183 fn eq(&self, other: &Self) -> bool {
184 self.identity() == other.identity()
185 }
186}
187
188impl Eq for Clock {}
189
190impl fmt::Debug for Clock {
191 /// Shows whether the clock is paused, its advance and its wall time.
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 // Read both times under one lock before calling the formatter's writer
194 #[cfg(any(test, feature = "test-clock"))]
195 let snapshot = self.paused.as_ref().map(|paused| paused.snapshot());
196
197 // Format without the clock lock, since the writer may read this clock
198 let mut clock = f.debug_struct("Clock");
199 #[cfg(any(test, feature = "test-clock"))]
200 if let Some((advanced, system_time)) = snapshot {
201 return clock
202 .field("paused", &true)
203 .field("advanced", &advanced)
204 .field("system_time", &system_time)
205 .finish();
206 }
207 clock.field("paused", &false).finish()
208 }
209}
210
211/// Blocks threads until a condition holds or a deadline passes on a clock.
212struct Waiter {
213 /// Clock that decides when deadlines pass.
214 clock: Clock,
215 /// Wakeups registered with a test clock until this waiter drops.
216 signal: Arc<Signal>,
217}
218
219impl Waiter {
220 /// Blocks until `ready` returns a value or the clock reaches `deadline`.
221 ///
222 /// A ready value wins over an expired deadline. Without a deadline, only
223 /// a ready value ends the wait. The callback runs again after every
224 /// notification and once the deadline is reached, without crate locks held.
225 #[cfg(any(test, feature = "test-clock"))]
226 fn wait_until<T>(
227 &self,
228 deadline: Option<Instant>,
229 mut ready: impl FnMut() -> Option<T>,
230 ) -> Option<T> {
231 // Pick the real timer once, so the test seam reports what every park uses
232 let timer = self.timer(deadline);
233
234 // Check and park in turns, until a value arrives or the deadline passes
235 loop {
236 // Read the count before checking, so a notification during the check ends the park at once
237 let seen = self.signal.lock().notifications;
238 if let Some(value) = ready() {
239 return Some(value);
240 }
241 if deadline.is_some_and(|deadline| self.clock.now() >= deadline) {
242 return None;
243 }
244 drop(self.park(self.signal.lock(), seen, deadline, timer));
245 }
246 }
247
248 /// Returns the deadline as a real timer on a real clock, and no timer on a
249 /// test clock, whose advances end its waits.
250 fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
251 #[cfg(any(test, feature = "test-clock"))]
252 if self.clock.paused.is_some() {
253 return None;
254 }
255 deadline
256 }
257
258 /// Parks until the notification count moves past `seen` or the clock
259 /// reaches `deadline`, and returns the state locked again.
260 ///
261 /// On a test clock, the park counts as blocked and lists its deadline from
262 /// its first wait until it returns, however often it wakes in between. Only
263 /// a real clock's park uses a real timer. In tests, one-shot hooks run
264 /// before its first wait and before a wait after a spurious wakeup, with
265 /// the signal unlocked.
266 fn park<'a>(
267 &'a self,
268 mut state: MutexGuard<'a, SignalState>,
269 seen: u64,
270 deadline: Option<Instant>,
271 timer: Option<Instant>,
272 ) -> MutexGuard<'a, SignalState> {
273 // Count the park once, keeping it counted through every wakeup until it returns
274 #[cfg(any(test, feature = "test-clock"))]
275 let mut blocked = None;
276
277 loop {
278 // Stop once a notification arrives or the clock reaches the deadline
279 let now = self.clock.now();
280 if state.notifications != seen || deadline.is_some_and(|deadline| now >= deadline) {
281 break;
282 }
283
284 // Let tests act before a wait, then check again for any change they made
285 #[cfg(test)]
286 if let Some(hook) = self
287 .clock
288 .paused
289 .as_ref()
290 .and_then(|paused| paused.take_hook(blocked.is_some()))
291 {
292 drop(state);
293 hook(timer);
294 state = self.signal.lock();
295 continue;
296 }
297
298 // Count the park under the clock lock, so an advance either finds it or has
299 // already passed its deadline
300 #[cfg(any(test, feature = "test-clock"))]
301 if blocked.is_none() {
302 if let Some(paused) = &self.clock.paused {
303 blocked = paused.block(deadline, &self.signal);
304 if blocked.is_none() {
305 break;
306 }
307 }
308 }
309 #[cfg(test)]
310 {
311 state.parks += 1;
312 self.signal.parked.notify_all();
313 }
314
315 // Wait for a wakeup or the real timer, leaving expiry to the check above
316 state = match timer {
317 None => self
318 .signal
319 .changed
320 .wait(state)
321 .expect("waiter signal not poisoned"),
322 Some(timer) => {
323 self.signal
324 .changed
325 .wait_timeout(state, (timer - now).min(MAX_REAL_WAIT))
326 .expect("waiter signal not poisoned")
327 .0
328 }
329 };
330 }
331
332 // Uncount the park before the caller acts on its wakeup
333 #[cfg(any(test, feature = "test-clock"))]
334 drop(blocked);
335 state
336 }
337}
338
339#[cfg(any(test, feature = "test-clock"))]
340impl Drop for Waiter {
341 /// Removes the waiter's registration as soon as it is no longer live.
342 fn drop(&mut self) {
343 if let Some(paused) = &self.clock.paused {
344 paused.unregister(&self.signal);
345 }
346 }
347}
348
349/// Counts notifications and wakes parked threads to recheck their condition
350/// or deadline.
351#[derive(Default)]
352struct Signal {
353 /// Notification count and the test park count, guarded together.
354 state: Mutex<SignalState>,
355 /// Wakes parked threads for a notification or a reached deadline.
356 changed: Condvar,
357 /// Reports new parks to tests, without waking parked threads.
358 #[cfg(test)]
359 parked: Condvar,
360 /// Clock wakes delivered, so tests can check which waits an advance reached.
361 #[cfg(test)]
362 wakes: AtomicUsize,
363}
364
365/// Mutable part of a signal.
366#[derive(Default)]
367struct SignalState {
368 /// Notifications sent so far, wrapping on overflow, which each waiter
369 /// compares with the count it read before parking.
370 notifications: u64,
371 /// Total waits entered, for test synchronization.
372 #[cfg(test)]
373 parks: usize,
374}
375
376impl Signal {
377 /// Locks the state, which no code panics under.
378 fn lock(&self) -> MutexGuard<'_, SignalState> {
379 self.state.lock().expect("waiter signal not poisoned")
380 }
381
382 /// Identifies this signal in the test clock's registrations and parked deadlines.
383 #[cfg(any(test, feature = "test-clock"))]
384 fn key(&self) -> usize {
385 self as *const Self as usize
386 }
387
388 /// Counts a notification and wakes one parked thread.
389 ///
390 /// Every waiting thread sees the count, so another one may return too,
391 /// as a spurious wakeup, the next time it wakes.
392 fn notify_one(&self) {
393 let mut state = self.lock();
394 state.notifications = state.notifications.wrapping_add(1);
395 self.changed.notify_one();
396 }
397
398 /// Counts a notification and wakes every parked thread.
399 fn notify_all(&self) {
400 let mut state = self.lock();
401 state.notifications = state.notifications.wrapping_add(1);
402 self.changed.notify_all();
403 }
404
405 /// Wakes every parked thread to recheck the time, without counting a notification.
406 #[cfg(any(test, feature = "test-clock"))]
407 fn wake(&self) {
408 #[cfg(test)]
409 self.wakes.fetch_add(1, Ordering::SeqCst);
410
411 // Take the lock, so a park that read the time before the advance is waiting by now
412 let _state = self.lock();
413 self.changed.notify_all();
414 }
415}
416
417// The tests live in src/tests, apart from the code they check
418#[cfg(test)]
419#[cfg_attr(coverage_nightly, coverage(off))]
420mod tests;