darkbio_clock/sync.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//! Mutexes and condition variables whose waits follow a clock.
8//!
9//! Both mirror std's. A condvar takes its clock when created and waits on this
10//! module's mutex, which otherwise works like std's.
11//!
12//! Wait for a condition until a deadline, checking it again after every return:
13//!
14//! ```
15//! # #[cfg(feature = "test-clock")] {
16//! use darkbio_clock::TestClock;
17//! use darkbio_clock::sync::{Condvar, Mutex};
18//! use std::thread;
19//! use std::time::Duration;
20//!
21//! let mut tester = TestClock::new();
22//! let clock = tester.clock();
23//! let (ready, condvar) = (Mutex::new(false), Condvar::new(&clock));
24//! let deadline = clock.now() + Duration::from_secs(5);
25//!
26//! thread::scope(|scope| {
27//! let worker = scope.spawn(|| {
28//! let mut guard = ready.lock().unwrap();
29//! while !*guard {
30//! let (next, result) = condvar.wait_deadline(guard, deadline).unwrap();
31//! guard = next;
32//! if result.timed_out() {
33//! return false;
34//! }
35//! }
36//! true
37//! });
38//! tester.wait_blocked(1);
39//! tester.advance(Duration::from_secs(5));
40//! assert!(!worker.join().unwrap());
41//! });
42//! # }
43//! ```
44
45use crate::{Clock, Waiter, primitives};
46use std::fmt;
47use std::ops::{Deref, DerefMut};
48use std::sync::{LockResult, PoisonError, TryLockError, TryLockResult};
49use std::time::Instant;
50
51/// A mutual exclusion lock that this module's condvars can wait on.
52///
53/// It wraps std's mutex, keeping its locking and poisoning.
54pub struct Mutex<T: ?Sized> {
55 /// The wrapped std mutex, or loom's under model checking, which holds the value.
56 inner: primitives::Mutex<T>,
57}
58
59impl<T> Mutex<T> {
60 /// Creates an unlocked mutex holding `value`.
61 #[cfg(not(all(test, loom)))]
62 pub const fn new(value: T) -> Self {
63 Self {
64 inner: primitives::Mutex::new(value),
65 }
66 }
67
68 /// Creates an unlocked mutex holding `value`, without the `const` that
69 /// loom's mutex cannot offer.
70 #[cfg(all(test, loom))]
71 pub fn new(value: T) -> Self {
72 Self {
73 inner: primitives::Mutex::new(value),
74 }
75 }
76
77 /// Consumes the mutex and returns its value, inside an error if the mutex
78 /// is poisoned.
79 pub fn into_inner(self) -> LockResult<T> {
80 self.inner.into_inner()
81 }
82}
83
84impl<T: ?Sized> Mutex<T> {
85 /// Blocks until the mutex is free, then locks it.
86 ///
87 /// If a thread panicked while holding the mutex, the guard comes back
88 /// inside an error.
89 pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
90 match self.inner.lock() {
91 Ok(inner) => Ok(MutexGuard { inner, mutex: self }),
92 Err(err) => Err(PoisonError::new(MutexGuard {
93 inner: err.into_inner(),
94 mutex: self,
95 })),
96 }
97 }
98
99 /// Locks the mutex if it is free, without blocking.
100 ///
101 /// Fails with `WouldBlock` while the mutex is locked. If a thread panicked
102 /// while holding the mutex, the guard comes back inside `Poisoned`.
103 pub fn try_lock(&self) -> TryLockResult<MutexGuard<'_, T>> {
104 match self.inner.try_lock() {
105 Ok(inner) => Ok(MutexGuard { inner, mutex: self }),
106 Err(TryLockError::WouldBlock) => Err(TryLockError::WouldBlock),
107 Err(TryLockError::Poisoned(err)) => {
108 Err(TryLockError::Poisoned(PoisonError::new(MutexGuard {
109 inner: err.into_inner(),
110 mutex: self,
111 })))
112 }
113 }
114 }
115
116 /// Reports whether a thread panicked while holding the mutex.
117 #[cfg(not(all(test, loom)))]
118 pub fn is_poisoned(&self) -> bool {
119 self.inner.is_poisoned()
120 }
121
122 /// Clears the poisoned state, marking the value as recovered.
123 #[cfg(not(all(test, loom)))]
124 pub fn clear_poison(&self) {
125 self.inner.clear_poison();
126 }
127
128 /// Borrows the value mutably, inside an error if the mutex is poisoned.
129 ///
130 /// The mutable borrow of the mutex rules out other users, so this takes
131 /// no lock.
132 pub fn get_mut(&mut self) -> LockResult<&mut T> {
133 self.inner.get_mut()
134 }
135}
136
137impl<T: Default> Default for Mutex<T> {
138 /// Creates an unlocked mutex holding the value type's default.
139 fn default() -> Self {
140 Self::new(T::default())
141 }
142}
143
144impl<T> From<T> for Mutex<T> {
145 /// Creates an unlocked mutex holding `value`.
146 fn from(value: T) -> Self {
147 Self::new(value)
148 }
149}
150
151impl<T: ?Sized + fmt::Debug> fmt::Debug for Mutex<T> {
152 /// Shows the value if the mutex is free, never blocking, as std does.
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 self.inner.fmt(f)
155 }
156}
157
158/// Exclusive access to a mutex's value, which unlocks the mutex on drop.
159///
160/// Like std's guard, it cannot be sent to another thread.
161#[must_use = "if unused the Mutex will immediately unlock"]
162pub struct MutexGuard<'a, T: ?Sized + 'a> {
163 /// The wrapped guard, which unlocks the mutex and poisons it on a panic.
164 inner: primitives::MutexGuard<'a, T>,
165 /// The mutex a condvar wait relocks.
166 mutex: &'a Mutex<T>,
167}
168
169impl<T: ?Sized> Deref for MutexGuard<'_, T> {
170 /// The value the mutex protects.
171 type Target = T;
172
173 /// Borrows the value.
174 fn deref(&self) -> &T {
175 &self.inner
176 }
177}
178
179impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
180 /// Borrows the value mutably.
181 fn deref_mut(&mut self) -> &mut T {
182 &mut self.inner
183 }
184}
185
186impl<T: ?Sized + fmt::Debug> fmt::Debug for MutexGuard<'_, T> {
187 /// Formats the value, as std's guard does.
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 fmt::Debug::fmt(&**self, f)
190 }
191}
192
193impl<T: ?Sized + fmt::Display> fmt::Display for MutexGuard<'_, T> {
194 /// Formats the value, as std's guard does.
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 fmt::Display::fmt(&**self, f)
197 }
198}
199
200/// A condition variable whose deadline waits follow a clock.
201///
202/// It mirrors std's, except that it takes its clock when created, and its
203/// timed wait takes an absolute deadline. A wait releases the mutex while it
204/// blocks and relocks it before returning. Notifications are not buffered, and
205/// a wait may also return spuriously, so callers recheck their condition after
206/// every return.
207pub struct Condvar {
208 /// Parks and wakes this condvar's waits, registered with a test clock.
209 waiter: Waiter,
210}
211
212impl Condvar {
213 /// Creates a condition variable whose deadlines are read from `clock`.
214 pub fn new(clock: &Clock) -> Self {
215 Self {
216 waiter: clock.waiter(),
217 }
218 }
219
220 /// Releases the mutex and blocks until notified, then relocks it.
221 ///
222 /// If a thread panicked while holding the mutex, the relocked guard comes
223 /// back inside an error.
224 pub fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult<MutexGuard<'a, T>> {
225 self.wait_inner(guard, None)
226 }
227
228 /// Blocks while `condition` holds, checking it with the mutex locked.
229 ///
230 /// Returns with the mutex locked once `condition` is false. If a thread
231 /// panicked while holding the mutex, the relocked guard comes back inside
232 /// an error.
233 pub fn wait_while<'a, T, F>(
234 &self,
235 mut guard: MutexGuard<'a, T>,
236 mut condition: F,
237 ) -> LockResult<MutexGuard<'a, T>>
238 where
239 F: FnMut(&mut T) -> bool,
240 {
241 while condition(&mut *guard) {
242 guard = self.wait(guard)?;
243 }
244 Ok(guard)
245 }
246
247 /// Releases the mutex and blocks until notified or until the clock reaches
248 /// `deadline`, then relocks it.
249 ///
250 /// A deadline already reached returns at once. The result reports whether
251 /// the clock had reached the deadline once the mutex was relocked. If a
252 /// thread panicked while holding the mutex, the guard and the result come
253 /// back inside an error.
254 pub fn wait_deadline<'a, T>(
255 &self,
256 guard: MutexGuard<'a, T>,
257 deadline: Instant,
258 ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> {
259 // Decide expiry by the clock after the relock, never by how the wait ended
260 let guard = self.wait_inner(guard, Some(deadline));
261 let result = WaitTimeoutResult(self.waiter.clock.now() >= deadline);
262
263 // Hand back the guard and the result even from a poisoned mutex
264 match guard {
265 Ok(guard) => Ok((guard, result)),
266 Err(err) => Err(PoisonError::new((err.into_inner(), result))),
267 }
268 }
269
270 /// Wakes one thread waiting on this condvar, if any.
271 pub fn notify_one(&self) {
272 self.waiter.signal.notify_one();
273 }
274
275 /// Wakes every thread waiting on this condvar.
276 pub fn notify_all(&self) {
277 self.waiter.signal.notify_all();
278 }
279
280 /// Waits with the mutex released until notified or until the clock reaches
281 /// `deadline`, then relocks it.
282 fn wait_inner<'a, T>(
283 &self,
284 guard: MutexGuard<'a, T>,
285 deadline: Option<Instant>,
286 ) -> LockResult<MutexGuard<'a, T>> {
287 // Pick the real timer once, so the test seam reports what every park uses
288 let timer = self.waiter.timer(deadline);
289
290 // Start the wait under the caller's mutex, so that a notification sent
291 // after the caller's last check cannot slip past it
292 let MutexGuard { inner, mutex } = guard;
293 let mut state = self.waiter.signal.lock();
294 let notifications = state.notifications;
295 #[cfg(test)]
296 if let Some(hook) = self
297 .waiter
298 .clock
299 .paused
300 .as_ref()
301 .and_then(|paused| paused.take_hook(false))
302 {
303 // Let tests act while the caller's mutex is still held
304 drop(state);
305 hook(timer);
306 state = self.waiter.signal.lock();
307 }
308
309 // Release the caller's mutex, keeping the signal locked until the park
310 drop(inner);
311
312 // Park until notified or the deadline passes, parking again after any
313 // advance short of it
314 loop {
315 let seen = state.generation;
316 if state.notifications != notifications
317 || deadline.is_some_and(|deadline| self.waiter.clock.now() >= deadline)
318 {
319 break;
320 }
321 state = self.waiter.park(state, seen, deadline, timer);
322 }
323
324 // Unlock the signal first, since notifiers take the two locks the other way round
325 drop(state);
326 mutex.lock()
327 }
328}
329
330impl fmt::Debug for Condvar {
331 /// Shows the clock, never the wakeup bookkeeping.
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 f.debug_struct("Condvar")
334 .field("clock", &self.waiter.clock)
335 .finish_non_exhaustive()
336 }
337}
338
339/// Whether a deadline wait's clock had reached the deadline when it returned.
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub struct WaitTimeoutResult(
342 /// Whether the clock had reached the deadline once the mutex was relocked.
343 bool,
344);
345
346impl WaitTimeoutResult {
347 /// Returns whether the clock had reached the deadline once the mutex was
348 /// relocked.
349 pub fn timed_out(&self) -> bool {
350 self.0
351 }
352}
353
354// The tests live in src/tests, loaded from here so that they keep this
355// module's private items in reach
356#[cfg(all(test, not(loom)))]
357#[cfg_attr(coverage_nightly, coverage(off))]
358#[path = "tests/sync.rs"]
359mod tests;