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 *guard;
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. Sharing it between
161/// threads needs the value to be `Send` as well as `Sync`, one bound more than
162/// std's guard needs.
163#[must_use = "if unused the Mutex will immediately unlock"]
164pub struct MutexGuard<'a, T: ?Sized + 'a> {
165 /// The wrapped guard, which unlocks the mutex and poisons it on a panic.
166 inner: primitives::MutexGuard<'a, T>,
167 /// The mutex a condvar wait relocks.
168 mutex: &'a Mutex<T>,
169}
170
171impl<T: ?Sized> Deref for MutexGuard<'_, T> {
172 /// The value the mutex protects.
173 type Target = T;
174
175 /// Borrows the value.
176 fn deref(&self) -> &T {
177 &self.inner
178 }
179}
180
181impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
182 /// Borrows the value mutably.
183 fn deref_mut(&mut self) -> &mut T {
184 &mut self.inner
185 }
186}
187
188impl<T: ?Sized + fmt::Debug> fmt::Debug for MutexGuard<'_, T> {
189 /// Formats the value, as std's guard does.
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 fmt::Debug::fmt(&**self, f)
192 }
193}
194
195impl<T: ?Sized + fmt::Display> fmt::Display for MutexGuard<'_, T> {
196 /// Formats the value, as std's guard does.
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 fmt::Display::fmt(&**self, f)
199 }
200}
201
202/// A condition variable whose deadline waits follow a clock.
203///
204/// It mirrors std's, except that it takes its clock when created, and its
205/// timed wait takes an absolute deadline. A wait releases the mutex while it
206/// blocks and relocks it before returning. Notifications are not buffered, and
207/// a wait may also return spuriously, so callers recheck their condition after
208/// every return.
209///
210/// Unlike std's, a wait that starts while its thread unwinds from a panic, on
211/// a guard taken before the panic, poisons the mutex as it releases it.
212pub struct Condvar {
213 /// Parks and wakes this condvar's waits, timing them on its clock.
214 waiter: Waiter,
215}
216
217impl Condvar {
218 /// Creates a condition variable whose deadlines are read from `clock`.
219 #[must_use]
220 pub fn new(clock: &Clock) -> Self {
221 Self {
222 waiter: clock.waiter(),
223 }
224 }
225
226 /// Releases the mutex and blocks until notified, then relocks it.
227 ///
228 /// If a thread panicked while holding the mutex, the relocked guard comes
229 /// back inside an error.
230 pub fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult<MutexGuard<'a, T>> {
231 self.wait_inner(guard, None).0
232 }
233
234 /// Blocks while `condition` holds, checking it with the mutex locked.
235 ///
236 /// Returns with the mutex locked once `condition` is false. If a thread
237 /// panicked while holding the mutex, the relocked guard comes back inside
238 /// an error.
239 pub fn wait_while<'a, T, F>(
240 &self,
241 mut guard: MutexGuard<'a, T>,
242 mut condition: F,
243 ) -> LockResult<MutexGuard<'a, T>>
244 where
245 F: FnMut(&mut T) -> bool,
246 {
247 while condition(&mut *guard) {
248 guard = self.wait(guard)?;
249 }
250 Ok(guard)
251 }
252
253 /// Releases the mutex and blocks until notified or until the clock reaches
254 /// `deadline`, then relocks it.
255 ///
256 /// A deadline already reached returns at once. As with std's, the result
257 /// reports a timeout only if the deadline ended the wait, however late the
258 /// relock. A notification this wait takes wins over a reached deadline. If
259 /// a thread panicked while holding the mutex, the guard and the result come
260 /// back inside an error.
261 pub fn wait_deadline<'a, T>(
262 &self,
263 guard: MutexGuard<'a, T>,
264 deadline: Instant,
265 ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> {
266 // Report how the wait ended, so a late relock cannot turn a notification into a timeout
267 let (guard, notified) = self.wait_inner(guard, Some(deadline));
268 let result = WaitTimeoutResult(!notified);
269
270 // Hand back the guard and the result even from a poisoned mutex
271 match guard {
272 Ok(guard) => Ok((guard, result)),
273 Err(err) => Err(PoisonError::new((err.into_inner(), result))),
274 }
275 }
276
277 /// Wakes one thread waiting on this condvar, if any. The others keep waiting.
278 pub fn notify_one(&self) {
279 self.waiter.signal.notify_one();
280 }
281
282 /// Wakes every thread waiting on this condvar.
283 pub fn notify_all(&self) {
284 self.waiter.signal.notify_all();
285 }
286
287 /// Waits with the mutex released until notified or until the clock reaches
288 /// `deadline`, then relocks it, reporting whether a notification ended the wait.
289 fn wait_inner<'a, T>(
290 &self,
291 guard: MutexGuard<'a, T>,
292 deadline: Option<Instant>,
293 ) -> (LockResult<MutexGuard<'a, T>>, bool) {
294 // Start the wait under the caller's mutex, so that a notification sent
295 // after the caller's last check cannot slip past it
296 let MutexGuard { inner, mutex } = guard;
297 let mut state = self.waiter.signal.lock();
298 let start = state.start();
299 #[cfg(test)]
300 if let Some(hook) = self
301 .waiter
302 .clock
303 .paused
304 .as_ref()
305 .and_then(|paused| paused.take_hook(false))
306 {
307 // Let tests act while the caller's mutex is still held
308 drop(state);
309 hook(self.waiter.timer(deadline));
310 state = self.waiter.signal.lock();
311 }
312
313 // Release the caller's mutex, keeping the signal locked until the park
314 drop(inner);
315
316 // Park until notified or the deadline passes, and note which while the signal is locked
317 let (state, notified) = self.waiter.park(state, start, deadline);
318
319 // Unlock the signal first, since notifiers take the two locks the other way round
320 drop(state);
321 (mutex.lock(), notified)
322 }
323}
324
325impl fmt::Debug for Condvar {
326 /// Shows the clock, never the wakeup bookkeeping.
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 f.debug_struct("Condvar")
329 .field("clock", &self.waiter.clock)
330 .finish_non_exhaustive()
331 }
332}
333
334/// Whether the clock's deadline ended a wait.
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub struct WaitTimeoutResult(
337 /// Whether the deadline ended the wait.
338 bool,
339);
340
341impl WaitTimeoutResult {
342 /// Returns whether the deadline ended the wait.
343 #[must_use]
344 pub fn timed_out(&self) -> bool {
345 self.0
346 }
347}
348
349// The tests live in src/tests, loaded from here so that they keep this
350// module's private items in reach
351#[cfg(all(test, not(loom)))]
352#[cfg_attr(coverage_nightly, coverage(off))]
353#[path = "tests/sync.rs"]
354mod tests;