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 parks 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::default()),
166 }
167 }
168}
169
170// A production clock carries no state, so passing one around costs nothing
171#[cfg(not(any(test, feature = "test-clock")))]
172const _: () = assert!(size_of::<Clock>() == 0);
173
174// A production clock still has a destructor, as a test build's does
175#[cfg(not(any(test, feature = "test-clock")))]
176const _: () = assert!(std::mem::needs_drop::<Clock>());
177
178impl Drop for Clock {
179 /// Does nothing, but gives every build a destructor, so enabling
180 /// `test-clock` does not change which const contexts accept a clock.
181 fn drop(&mut self) {}
182}
183
184impl PartialEq for Clock {
185 /// Compares identity, with all real clocks equal to each other.
186 fn eq(&self, other: &Self) -> bool {
187 self.identity() == other.identity()
188 }
189}
190
191impl Eq for Clock {}
192
193impl fmt::Debug for Clock {
194 /// Shows whether the clock is paused, its advance and its wall time.
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 // Read both times under one lock before calling the formatter's writer
197 #[cfg(any(test, feature = "test-clock"))]
198 let snapshot = self.paused.as_ref().map(|paused| paused.snapshot());
199
200 // Format without the clock lock, since the writer may read this clock
201 let mut clock = f.debug_struct("Clock");
202 #[cfg(any(test, feature = "test-clock"))]
203 if let Some((advanced, system_time)) = snapshot {
204 return clock
205 .field("paused", &true)
206 .field("advanced", &advanced)
207 .field("system_time", &system_time)
208 .finish();
209 }
210 clock.field("paused", &false).finish()
211 }
212}
213
214/// Blocks threads until notified or a deadline passes on a clock.
215struct Waiter {
216 /// Clock that decides when deadlines pass.
217 clock: Clock,
218 /// Notifications for this waiter's waits, and the wakeups that deliver them.
219 signal: Arc<Signal>,
220}
221
222impl Waiter {
223 /// Starts a wait and parks until a notification ends it or the clock
224 /// reaches `deadline`, returning whether a notification ended it.
225 #[cfg(any(test, feature = "test-clock"))]
226 fn wait(&self, deadline: Option<Instant>) -> bool {
227 let mut state = self.signal.lock();
228 let start = state.start();
229 self.park(state, start, deadline).1
230 }
231
232 /// Returns the deadline as a real timer on a real clock, and no timer on a
233 /// test clock, whose advances end its waits.
234 fn timer(&self, deadline: Option<Instant>) -> Option<Instant> {
235 #[cfg(any(test, feature = "test-clock"))]
236 if self.clock.paused.is_some() {
237 return None;
238 }
239 deadline
240 }
241
242 /// Parks the wait that started at `start` until a notification ends it or
243 /// the clock reaches `deadline`. Returns the state locked again, and
244 /// whether a notification ended the wait.
245 ///
246 /// On a test clock, the park counts as blocked and lists its deadline from
247 /// its first wait until it returns, however often it wakes in between. Only
248 /// a real clock's park uses a real timer. In tests, one-shot hooks run
249 /// before its first wait and before a wait after a spurious wakeup, with
250 /// the signal unlocked.
251 fn park<'a>(
252 &'a self,
253 mut state: MutexGuard<'a, SignalState>,
254 start: u64,
255 deadline: Option<Instant>,
256 ) -> (MutexGuard<'a, SignalState>, bool) {
257 // Only a real clock times its waits, since a test clock's advances end them
258 let timer = self.timer(deadline);
259
260 // Count the park once, keeping it counted through every wakeup until it returns
261 #[cfg(any(test, feature = "test-clock"))]
262 let mut blocked = None;
263
264 let notified = loop {
265 // Take a notification first, so that one wins over a reached deadline
266 if state.claim(start) {
267 break true;
268 }
269 let remaining =
270 deadline.map(|deadline| deadline.saturating_duration_since(self.clock.now()));
271 if remaining.is_some_and(|remaining| remaining.is_zero()) {
272 break false;
273 }
274
275 // Let tests act before a wait, then check again for any change they made
276 #[cfg(test)]
277 if let Some(hook) = self
278 .clock
279 .paused
280 .as_ref()
281 .and_then(|paused| paused.take_hook(blocked.is_some()))
282 {
283 drop(state);
284 hook(timer);
285 state = self.signal.lock();
286 continue;
287 }
288
289 // Count the park under the clock lock, so an advance either finds it or has
290 // already passed its deadline
291 #[cfg(any(test, feature = "test-clock"))]
292 if blocked.is_none() {
293 if let Some(paused) = &self.clock.paused {
294 blocked = paused.block(deadline, &self.signal);
295 if blocked.is_none() {
296 break false;
297 }
298 }
299 }
300 #[cfg(test)]
301 {
302 state.parks += 1;
303 self.signal.parked.notify_all();
304 }
305
306 // Wait for a wakeup or the real timer, leaving expiry to the check above
307 state = match timer.and(remaining) {
308 None => self
309 .signal
310 .changed
311 .wait(state)
312 .expect("waiter signal not poisoned"),
313 Some(remaining) => {
314 self.signal
315 .changed
316 .wait_timeout(state, remaining.min(MAX_REAL_WAIT))
317 .expect("waiter signal not poisoned")
318 .0
319 }
320 };
321 };
322
323 // Retire and unlist the wait under the signal lock before its caller acts
324 state.retire(start);
325 #[cfg(any(test, feature = "test-clock"))]
326 drop(blocked);
327 (state, notified)
328 }
329}
330
331/// Hands notifications to a waiter's waits and wakes parked threads to check
332/// for one or for their deadline.
333#[derive(Default)]
334struct Signal {
335 /// Notification bookkeeping and the test park count, guarded together.
336 state: Mutex<SignalState>,
337 /// Wakes parked threads for a notification or a reached deadline.
338 changed: Condvar,
339 /// Reports new parks to tests, without waking parked threads.
340 #[cfg(test)]
341 parked: Condvar,
342 /// Clock wakes delivered, so tests can check which waits an advance reached.
343 #[cfg(test)]
344 wakes: AtomicUsize,
345}
346
347/// Mutable part of a signal.
348///
349/// Notifications are numbered in the order they are sent, and a wait can only
350/// take one sent after it started. Each queued `notify_one` can be matched to
351/// a distinct counted wait that started before it and will check again, so
352/// none is ever left behind. A `u64` count cannot wrap within any run.
353#[derive(Default)]
354struct SignalState {
355 /// Number of the latest notification, one-shot or broadcast.
356 notifications: u64,
357 /// Number of the latest `notify_all`, which ends every wait started before it.
358 broadcast: u64,
359 /// Waits started and neither finished nor ended by a broadcast.
360 waiting: usize,
361 /// Numbers of the `notify_one` calls no wait has taken yet, oldest first.
362 pending: VecDeque<u64>,
363 /// Total physical waits entered, for test synchronization.
364 #[cfg(test)]
365 parks: usize,
366}
367
368impl SignalState {
369 /// Counts a new wait, returning its start, the number after which it
370 /// can take notifications.
371 fn start(&mut self) -> u64 {
372 self.waiting += 1;
373 self.notifications
374 }
375
376 /// Returns whether a notification ends the wait that started at `start`,
377 /// taking the oldest queued one sent after it unless a broadcast ended it.
378 fn claim(&mut self, start: u64) -> bool {
379 // A broadcast ends every wait started before it, and takes no queued notification
380 if self.broadcast > start {
381 return true;
382 }
383
384 // Take the oldest notification this wait can, leaving earlier ones to earlier waits
385 if let Some(index) = self.pending.iter().position(|&sent| sent > start) {
386 self.pending.remove(index);
387 return true;
388 }
389 false
390 }
391
392 /// Uncounts a finished wait, unless the broadcast that ended it already did.
393 fn retire(&mut self, start: u64) {
394 if self.broadcast <= start {
395 self.waiting -= 1;
396 }
397 }
398}
399
400impl Signal {
401 /// Locks the state, which no code panics under.
402 fn lock(&self) -> MutexGuard<'_, SignalState> {
403 self.state.lock().expect("waiter signal not poisoned")
404 }
405
406 /// Queues a notification for one of the waits already started, and wakes a
407 /// parked thread to take it. Queues none once every counted wait has one.
408 fn notify_one(&self) {
409 let mut state = self.lock();
410 state.notifications += 1;
411 if state.pending.len() < state.waiting {
412 let sent = state.notifications;
413 state.pending.push_back(sent);
414 self.changed.notify_one();
415 }
416 }
417
418 /// Ends every wait started so far, dropping their queued notifications,
419 /// and wakes every parked thread.
420 fn notify_all(&self) {
421 // Uncount the ended waits at once, so that no later notify_one queues one for them
422 let mut state = self.lock();
423 state.notifications += 1;
424 state.broadcast = state.notifications;
425 state.pending.clear();
426 state.waiting = 0;
427 self.changed.notify_all();
428 }
429
430 /// Wakes every parked thread to recheck the time, without counting a notification.
431 #[cfg(any(test, feature = "test-clock"))]
432 fn wake(&self) {
433 #[cfg(test)]
434 self.wakes.fetch_add(1, Ordering::SeqCst);
435
436 // Take the lock, so a park that read the time before the advance is waiting by now
437 let _state = self.lock();
438 self.changed.notify_all();
439 }
440}
441
442// The tests live in src/tests, apart from the code they check
443#[cfg(test)]
444#[cfg_attr(coverage_nightly, coverage(off))]
445mod tests;