Skip to main content

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;