Skip to main content

#
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}