jellytau_lib/utils/lock.rs
1//! Poison-tolerant locking helpers.
2//!
3//! `std::sync::Mutex` and `RwLock` become *poisoned* if a thread panics while
4//! holding the guard. After that, every `.lock().unwrap()` / `.read().unwrap()`
5//! / `.write().unwrap()` panics as well — so a single failure can cascade into
6//! an unrecoverable crash. That is a real risk for stateful subsystems like the
7//! player, whose locks are touched from many background threads (the MPV event
8//! loop, sleep/autoplay timers, JNI callbacks, the session poller).
9//!
10//! These extension traits recover the guard from a poisoned lock instead of
11//! panicking. The data behind a poisoned lock may be in an unexpected state,
12//! but for this application's state (queues, settings, flags) recovering and
13//! continuing is far preferable to taking down playback entirely.
14//!
15//! Use `lock_safe()` / `read_safe()` / `write_safe()` in place of
16//! `.lock().unwrap()` / `.read().unwrap()` / `.write().unwrap()`.
17//!
18//! Note: these apply only to `std::sync` primitives. `tokio::sync::Mutex` does
19//! not poison, so async code keeps using `.lock().await` unchanged.
20
21use std::sync::{Mutex, MutexGuard, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
22
23/// Poison-tolerant locking for [`std::sync::Mutex`].
24pub trait MutexSafe<T: ?Sized> {
25 /// Lock the mutex, recovering the inner guard if the lock was poisoned.
26 fn lock_safe(&self) -> MutexGuard<'_, T>;
27}
28
29impl<T: ?Sized> MutexSafe<T> for Mutex<T> {
30 fn lock_safe(&self) -> MutexGuard<'_, T> {
31 self.lock().unwrap_or_else(PoisonError::into_inner)
32 }
33}
34
35/// Poison-tolerant locking for [`std::sync::RwLock`].
36pub trait RwLockSafe<T: ?Sized> {
37 /// Acquire a read guard, recovering it if the lock was poisoned.
38 fn read_safe(&self) -> RwLockReadGuard<'_, T>;
39 /// Acquire a write guard, recovering it if the lock was poisoned.
40 fn write_safe(&self) -> RwLockWriteGuard<'_, T>;
41}
42
43impl<T: ?Sized> RwLockSafe<T> for RwLock<T> {
44 fn read_safe(&self) -> RwLockReadGuard<'_, T> {
45 self.read().unwrap_or_else(PoisonError::into_inner)
46 }
47
48 fn write_safe(&self) -> RwLockWriteGuard<'_, T> {
49 self.write().unwrap_or_else(PoisonError::into_inner)
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use std::sync::Arc;
57 use std::thread;
58
59 /// Run `f` with the panic hook suppressed so deliberately-poisoning panics
60 /// don't spam the test output.
61 fn without_panic_noise<R>(f: impl FnOnce() -> R) -> R {
62 let prev = std::panic::take_hook();
63 std::panic::set_hook(Box::new(|_| {}));
64 let result = f();
65 std::panic::set_hook(prev);
66 result
67 }
68
69 #[test]
70 fn lock_safe_recovers_from_poison() {
71 let mutex = Arc::new(Mutex::new(0u32));
72 let poisoner = Arc::clone(&mutex);
73
74 without_panic_noise(|| {
75 let _ = thread::spawn(move || {
76 let mut guard = poisoner.lock().unwrap();
77 *guard = 42;
78 panic!("poison the mutex while holding the guard");
79 })
80 .join();
81 });
82
83 // The mutex is now poisoned: the normal path would panic.
84 assert!(mutex.lock().is_err());
85
86 // lock_safe recovers the guard with the last written value intact.
87 let guard = mutex.lock_safe();
88 assert_eq!(*guard, 42);
89 }
90
91 #[test]
92 fn rwlock_safe_recovers_from_poison() {
93 let lock = Arc::new(RwLock::new(0u32));
94 let poisoner = Arc::clone(&lock);
95
96 without_panic_noise(|| {
97 let _ = thread::spawn(move || {
98 let mut guard = poisoner.write().unwrap();
99 *guard = 7;
100 panic!("poison the rwlock while holding the write guard");
101 })
102 .join();
103 });
104
105 assert!(lock.write().is_err());
106 assert_eq!(*lock.read_safe(), 7);
107
108 *lock.write_safe() = 9;
109 assert_eq!(*lock.read_safe(), 9);
110 }
111}