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::collections::VecDeque;
42use std::fmt;
43use std::sync::Arc;
44#[cfg(test)]
45use std::sync::atomic::{AtomicUsize, Ordering};
46use std::time::{Duration, Instant, SystemTime};
47
48use primitives::{Condvar, Mutex, MutexGuard};
49
50/// Longest real wait handed to the OS in one call, since Windows waits forever
51/// on timeouts of about 49.7 days or more.
52const MAX_REAL_WAIT: Duration = Duration::from_secs(24 * 60 * 60);
53
54/// A clock that reads real time, or a test's time that moves only on command.
55///
56/// Clones share one clock. Equality compares identity, so all real clocks are
57/// equal and a test clock equals only the handles of its own `TestClock`.
58///
59/// It cannot be built from a struct literal and it has a destructor in every
60/// build, so code that compiles without `test-clock` compiles with it too.
61#[derive(Clone)]
62#[non_exhaustive]
63pub struct Clock {
64 /// Shared state of a test clock, or nothing for the real clock.
65 #[cfg(any(test, feature = "test-clock"))]
66 paused: Option<Arc<paused::Paused>>,
67}
68
69impl Clock {
70 /// Returns the real clock, which reads the system's monotonic and wall times.
71 pub const fn real() -> Self {
72 Self {
73 #[cfg(any(test, feature = "test-clock"))]
74 paused: None,
75 }
76 }
77
78 /// Returns the clock's current monotonic time.
79 #[must_use]
80 pub fn now(&self) -> Instant {
81 #[cfg(any(test, feature = "test-clock"))]
82 if let Some(paused) = &self.paused {
83 return paused.now();
84 }
85 Instant::now()
86 }
87
88 /// Returns the time since `since`, or zero if it is later than now.
89 #[must_use]
90 pub fn elapsed(&self, since: Instant) -> Duration {
91 self.now().saturating_duration_since(since)
92 }
93
94 /// Returns the clock's current wall time.
95 #[must_use]
96 pub fn system_time(&self) -> SystemTime {
97 #[cfg(any(test, feature = "test-clock"))]
98 if let Some(paused) = &self.paused {
99 return paused.system_time();
100 }
101 SystemTime::now()
102 }
103
104 /// Blocks the thread until `duration` has passed on this clock.
105 ///
106 /// On a test clock, only its advances end the sleep.
107 ///
108 /// # Panics
109 ///
110 /// Panics on a test clock if the deadline overflows [`Instant`].
111 pub fn sleep(&self, duration: Duration) {
112 // A test clock's sleep waits for the deadline this duration gives
113 #[cfg(any(test, feature = "test-clock"))]
114 if self.paused.is_some() {
115 self.sleep_until(self.now() + duration);
116 return;
117 }
118
119 // A real sleep waits in capped slices, rechecking the time slept after each
120 let start = Instant::now();
121 loop {
122 let elapsed = start.elapsed();
123 if elapsed >= duration {
124 return;
125 }
126 std::thread::sleep((duration - elapsed).min(MAX_REAL_WAIT));
127 }
128 }
129
130 /// Blocks the thread until this clock reaches `deadline`, returning at once
131 /// if it already has.
132 ///
133 /// On a test clock, only its advances end the sleep.
134 pub fn sleep_until(&self, deadline: Instant) {
135 // A test clock's sleep waits until an advance reaches its deadline
136 #[cfg(any(test, feature = "test-clock"))]
137 if self.paused.is_some() {
138 self.waiter().wait(Some(deadline));
139 return;
140 }
141
142 // A real sleep waits in capped slices, rechecking the deadline after each
143 loop {
144 let now = Instant::now();
145 if now >= deadline {
146 return;
147 }
148 std::thread::sleep((deadline - now).min(MAX_REAL_WAIT));
149 }
150 }
151
152 /// Returns the shared state's address, or nothing for the real clock.
153 fn identity(&self) -> Option<*const ()> {
154 #[cfg(any(test, feature = "test-clock"))]
155 if let Some(paused) = &self.paused {
156 return Some(Arc::as_ptr(paused).cast());
157 }
158 None
159 }
160
161 /// Creates a waiter that measures deadlines on this clock.
162 fn waiter(&self) -> Waiter {
163 Waiter {
164 clock: self.clone(),
165 signal: Arc::new(Signal {
166 #[cfg(any(test, feature = "test-clock"))]
167 paused: self.paused.as_ref().map(Arc::downgrade),
168 ..Signal::default()
169 }),
170 }
171 }
172}
173
174// A production clock carries no state, so passing one around costs nothing
175#[cfg(not(any(test, feature = "test-clock")))]
176const _: () = assert!(size_of::<Clock>() == 0);
177
178// A production clock still has a destructor, as a test build's does
179#[cfg(not(any(test, feature = "test-clock")))]
180const _: () = assert!(std::mem::needs_drop::<Clock>());
181
182impl Drop for Clock {
183 /// Does nothing, but gives every build a destructor, so enabling
184 /// `test-clock` does not change which const contexts accept a clock.
185 fn drop(&mut self) {}
186}
187
188impl PartialEq for Clock {
189 /// Compares identity, with all real clocks equal to each other.
190 fn eq(&self, other: &Self) -> bool {
191 self.identity() == other.identity()
192 }
193}
194
195impl Eq for Clock {}
196
197impl fmt::Debug for Clock {
198 /// Shows whether the clock is paused, its advance and its wall time.
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 // Read both times under one lock before calling the formatter's writer
201 #[cfg(any(test, feature = "test-clock"))]
202 let snapshot = self.paused.as_ref().map(|paused| paused.snapshot());
203
204 // Format without the clock lock, since the writer may read this clock
205 let mut clock = f.debug_struct("Clock");
206 #[cfg(any(test, feature = "test-clock"))]
207 if let Some((advanced, system_time)) = snapshot {
208 return clock
209 .field("paused", &true)
210 .field("advanced", &advanced)
211 .field("system_time", &system_time)
212 .finish();
213 }
214 clock.field("paused", &false).finish()
215 }
216}
217
218/// Blocks threads until notified or a deadline passes on a clock.
219struct Waiter {
220 /// Clock that decides when deadlines pass.
221 clock: Clock,
222 /// Notifications for this waiter's waits, and the wakeups that deliver them.
223 signal: Arc<Signal>,
224}
225
226impl Waiter {
227 /// Starts a wait and blocks until a notification ends it or the clock
228 /// reaches `deadline`, returning whether a notification ended it.
229 #[cfg(any(test, feature = "test-clock"))]
230 fn wait(&self, deadline: Option<Instant>) -> bool {
231 let mut state = self.signal.lock();
232 let start = state.start();
233 self.wait_locked(state, start, deadline).1
234 }
235
236 /// Returns the deadline as a real timer on a real clock, and no timer on a
237 /// test clock, whose advances end its waits.
238 fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
239 #[cfg(any(test, feature = "test-clock"))]
240 if self.clock.paused.is_some() {
241 return None;
242 }
243 deadline
244 }
245
246 /// Waits, with the signal state locked on entry and return, until a
247 /// notification ends the wait that started at `start` or the clock
248 /// reaches `deadline`, returning whether a notification ended it.
249 ///
250 /// On a test clock, the wait stays registered with its deadline until it
251 /// returns, including after a wake. Only a real clock uses a real timer.
252 /// Test hooks run with the signal unlocked before registration and before
253 /// a repeated wait after a spurious wake.
254 fn wait_locked<'a>(
255 &'a self,
256 mut state: MutexGuard<'a, SignalState>,
257 start: u64,
258 deadline: Option<Instant>,
259 ) -> (MutexGuard<'a, SignalState>, bool) {
260 // Only a real clock times its waits, since a test clock's advances end them
261 let timer = self.timer(deadline);
262
263 // Keep the wait registered through every wakeup until it returns
264 #[cfg(any(test, feature = "test-clock"))]
265 let mut registration = None;
266
267 let notified = loop {
268 // Take a notification first, so that one wins over a reached deadline
269 if state.claim(start) {
270 break true;
271 }
272 let remaining =
273 deadline.map(|deadline| deadline.saturating_duration_since(self.clock.now()));
274 if remaining.is_some_and(|remaining| remaining.is_zero()) {
275 break false;
276 }
277
278 // Let tests act before a wait, then check again for any change they made
279 #[cfg(test)]
280 if let Some(hook) = self
281 .clock
282 .paused
283 .as_ref()
284 .and_then(|paused| paused.take_hook(registration.is_some()))
285 {
286 drop(state);
287 hook(timer);
288 state = self.signal.lock();
289 continue;
290 }
291
292 // Register under the clock lock, so an advance either finds the wait or has
293 // already passed its deadline
294 #[cfg(any(test, feature = "test-clock"))]
295 if registration.is_none() {
296 if let Some(paused) = &self.clock.paused {
297 registration = paused.register(deadline, &self.signal);
298 if registration.is_none() {
299 break false;
300 }
301 }
302 }
303 #[cfg(test)]
304 {
305 state.waits += 1;
306 self.signal.entered.notify_all();
307 }
308
309 // Wait for a wakeup or the real timer, leaving expiry to the check above
310 state = match timer.and(remaining) {
311 None => self
312 .signal
313 .changed
314 .wait(state)
315 .expect("waiter signal not poisoned"),
316 Some(remaining) => {
317 self.signal
318 .changed
319 .wait_timeout(state, remaining.min(MAX_REAL_WAIT))
320 .expect("waiter signal not poisoned")
321 .0
322 }
323 };
324 };
325
326 // Retire and unlist the wait under the signal lock before its caller acts
327 state.retire(start);
328 #[cfg(any(test, feature = "test-clock"))]
329 drop(registration);
330 #[cfg(any(test, feature = "test-clock"))]
331 self.signal.publish(&mut state);
332 (state, notified)
333 }
334}
335
336/// Hands notifications to registered waits and wakes threads to check
337/// for one or for their deadline.
338#[derive(Default)]
339struct Signal {
340 /// Clock observing outstanding notifications for its parked wait barrier.
341 #[cfg(any(test, feature = "test-clock"))]
342 paused: Option<std::sync::Weak<paused::Paused>>,
343 /// Notification bookkeeping and the test wait count, guarded together.
344 state: Mutex<SignalState>,
345 /// Wakes waiting threads for a notification or a reached deadline.
346 changed: Condvar,
347 /// Reports new physical waits to tests without waking the waiting threads.
348 #[cfg(test)]
349 entered: Condvar,
350 /// Clock wakes delivered, so tests can check which waits an advance reached.
351 #[cfg(test)]
352 wakes: AtomicUsize,
353}
354
355/// Mutable part of a signal.
356///
357/// Notifications are numbered in the order they are sent, and a wait can only
358/// take one sent after it started. Each queued `notify_one` can be matched to
359/// a distinct counted wait that started before it and will check again, so
360/// none is ever left behind. A `u64` count cannot wrap within any run.
361#[derive(Default)]
362struct SignalState {
363 /// Number of the latest notification, one-shot or broadcast.
364 notifications: u64,
365 /// Number of the latest `notify_all`, which ends every wait started before it.
366 broadcast: u64,
367 /// Waits started and neither finished nor ended by a broadcast.
368 waiting: usize,
369 /// Numbers of the `notify_one` calls no wait has taken yet, oldest first.
370 pending: VecDeque<u64>,
371 /// Waits ended by broadcasts that have not yet returned.
372 #[cfg(any(test, feature = "test-clock"))]
373 ended: usize,
374 /// Notification obligations last published to the test clock.
375 #[cfg(any(test, feature = "test-clock"))]
376 published: usize,
377 /// Total physical waits entered, for test synchronization.
378 #[cfg(test)]
379 waits: usize,
380}
381
382impl SignalState {
383 /// Counts a new wait, returning its start, the number after which it
384 /// can take notifications.
385 fn start(&mut self) -> u64 {
386 self.waiting += 1;
387 self.notifications
388 }
389
390 /// Returns whether a notification ends the wait that started at `start`,
391 /// taking the oldest queued one sent after it unless a broadcast ended it.
392 fn claim(&mut self, start: u64) -> bool {
393 // A broadcast ends every wait started before it, and takes no queued notification
394 if self.broadcast > start {
395 return true;
396 }
397
398 // Take the oldest notification this wait can, leaving earlier ones to earlier waits
399 if let Some(index) = self.pending.iter().position(|&sent| sent > start) {
400 self.pending.remove(index);
401 return true;
402 }
403 false
404 }
405
406 /// Uncounts a finished wait, unless the broadcast that ended it already did.
407 fn retire(&mut self, start: u64) {
408 if self.broadcast <= start {
409 self.waiting -= 1;
410 } else {
411 #[cfg(any(test, feature = "test-clock"))]
412 {
413 self.ended -= 1;
414 }
415 }
416 }
417}
418
419impl Signal {
420 /// Locks the state, which no code panics under.
421 fn lock(&self) -> MutexGuard<'_, SignalState> {
422 self.state.lock().expect("waiter signal not poisoned")
423 }
424
425 /// Queues a notification for one of the waits already started, and wakes a
426 /// waiting thread to take it. Queues none once every counted wait has one.
427 fn notify_one(&self) {
428 let mut state = self.lock();
429 state.notifications += 1;
430 if state.pending.len() < state.waiting {
431 let sent = state.notifications;
432 state.pending.push_back(sent);
433 #[cfg(any(test, feature = "test-clock"))]
434 self.publish(&mut state);
435 self.changed.notify_one();
436 }
437 }
438
439 /// Ends every wait started so far, dropping their queued notifications,
440 /// and wakes every waiting thread.
441 fn notify_all(&self) {
442 // Uncount the ended waits at once, so that no later notify_one queues one for them
443 let mut state = self.lock();
444 state.notifications += 1;
445 state.broadcast = state.notifications;
446 state.pending.clear();
447 #[cfg(any(test, feature = "test-clock"))]
448 {
449 state.ended += state.waiting;
450 }
451 state.waiting = 0;
452 #[cfg(any(test, feature = "test-clock"))]
453 self.publish(&mut state);
454 self.changed.notify_all();
455 }
456
457 /// Publishes this signal's outstanding notifications to its clock while the
458 /// signal stays locked.
459 ///
460 /// Claiming a credit alone publishes nothing. A wait publishes only after
461 /// it retires and drops its registration, so the parked wait barrier never
462 /// counts a wait whose credit it has already consumed.
463 #[cfg(any(test, feature = "test-clock"))]
464 fn publish(&self, state: &mut SignalState) {
465 if let Some(paused) = self.paused.as_ref().and_then(std::sync::Weak::upgrade) {
466 let pending = state.ended + state.pending.len();
467 if pending != state.published {
468 paused.notifications(state.published, pending);
469 state.published = pending;
470 }
471 }
472 }
473
474 /// Wakes every waiting thread to recheck the time without counting a notification.
475 #[cfg(any(test, feature = "test-clock"))]
476 fn wake(&self) {
477 #[cfg(test)]
478 self.wakes.fetch_add(1, Ordering::SeqCst);
479
480 // Take the lock, so a thread that read the time before the advance is waiting by now
481 let _state = self.lock();
482 self.changed.notify_all();
483 }
484}
485
486// The tests live in src/tests, apart from the code they check
487#[cfg(test)]
488#[cfg_attr(coverage_nightly, coverage(off))]
489mod tests;