Skip to main content

motion_core/
sync.rs

1//! Poison-tolerant locking shared by every crate.
2//!
3//! A mutex is poisoned when a thread panics while holding it. Scene callbacks run
4//! outside these locks and worker panics are caught at thread boundaries and reported
5//! as errors, so a poisoned lock never means the process must stop. Every state guarded
6//! by a library mutex is plain data that stays valid at each statement (caches,
7//! mailboxes, counters and progress), so these helpers continue with the guarded value
8//! instead of propagating the poison as a second panic. The font engine's lock is the
9//! exception: it guards third-party shaping state, so a poisoned engine is reported as
10//! a validation error.
11
12use std::sync::{Condvar, Mutex, MutexGuard, PoisonError, WaitTimeoutResult};
13use std::time::Duration;
14
15/// Locks a [`Mutex`] and continues with its value when a previous holder panicked.
16pub trait MutexExt<T> {
17    /// Acquires the lock, recovering the guarded value from a poisoned mutex.
18    fn lock_unpoisoned(&self) -> MutexGuard<'_, T>;
19}
20
21impl<T> MutexExt<T> for Mutex<T> {
22    fn lock_unpoisoned(&self) -> MutexGuard<'_, T> {
23        self.lock().unwrap_or_else(PoisonError::into_inner)
24    }
25}
26
27/// Waits on a [`Condvar`] and continues when the mutex was poisoned meanwhile.
28pub trait CondvarExt {
29    /// Blocks until notified, recovering the guard from a poisoned mutex.
30    fn wait_unpoisoned<'a, T>(&self, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T>;
31
32    /// Blocks until notified or `timeout` elapses, recovering from a poisoned mutex.
33    fn wait_timeout_unpoisoned<'a, T>(
34        &self,
35        guard: MutexGuard<'a, T>,
36        timeout: Duration,
37    ) -> (MutexGuard<'a, T>, WaitTimeoutResult);
38}
39
40impl CondvarExt for Condvar {
41    fn wait_unpoisoned<'a, T>(&self, guard: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
42        self.wait(guard).unwrap_or_else(PoisonError::into_inner)
43    }
44
45    fn wait_timeout_unpoisoned<'a, T>(
46        &self,
47        guard: MutexGuard<'a, T>,
48        timeout: Duration,
49    ) -> (MutexGuard<'a, T>, WaitTimeoutResult) {
50        self.wait_timeout(guard, timeout)
51            .unwrap_or_else(PoisonError::into_inner)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::sync::Arc;
59
60    #[test]
61    fn poisoned_locks_keep_their_last_value() {
62        let shared = Arc::new(Mutex::new(1));
63        let writer = shared.clone();
64        let panicked = std::thread::spawn(move || {
65            let mut value = writer.lock().unwrap();
66            *value = 7;
67            panic!("poison the lock");
68        })
69        .join();
70        assert!(panicked.is_err() && shared.is_poisoned());
71        assert_eq!(*shared.lock_unpoisoned(), 7);
72        let (guard, result) =
73            Condvar::new().wait_timeout_unpoisoned(shared.lock_unpoisoned(), Duration::ZERO);
74        assert_eq!(*guard, 7);
75        assert!(result.timed_out());
76    }
77}