Skip to main content

offline_protocol_core/
sync.rs

1//! Poison-recovering lock extensions.
2//!
3//! The SDK runs on mobile platforms where a process crash is worse than
4//! operating on potentially inconsistent state. A panic on one thread while
5//! holding a lock poisons it; with plain `.lock().unwrap()` every later
6//! access panics too, permanently wedging the node. Mutex poisoning in Rust
7//! is advisory — the protected data is still structurally valid — so these
8//! helpers recover the guard via [`std::sync::PoisonError::into_inner`] and
9//! log a warning with the caller's location for observability.
10//!
11//! Use these in code driven by foreign (FFI) threads and in event-emission
12//! paths that must keep flowing. Test code should keep `.lock().unwrap()`
13//! so poisoning fails loudly.
14
15use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
16
17/// Extension trait adding poison-recovering locking to [`Mutex`].
18pub trait MutexExt<T> {
19    /// Locks the mutex, recovering the guard if it is poisoned.
20    ///
21    /// On poison, logs a warning naming the call site and returns the inner
22    /// guard anyway.
23    fn lock_or_recover(&self) -> MutexGuard<'_, T>;
24}
25
26impl<T> MutexExt<T> for Mutex<T> {
27    #[track_caller]
28    fn lock_or_recover(&self) -> MutexGuard<'_, T> {
29        let location = std::panic::Location::caller();
30        self.lock().unwrap_or_else(|e| {
31            tracing::warn!(location = %location, "Mutex poisoned — recovering with inner value");
32            e.into_inner()
33        })
34    }
35}
36
37/// Extension trait adding poison-recovering locking to [`RwLock`].
38pub trait RwLockExt<T> {
39    /// Acquires a read guard, recovering it if the lock is poisoned.
40    ///
41    /// On poison, logs a warning naming the call site and returns the inner
42    /// guard anyway.
43    fn read_or_recover(&self) -> RwLockReadGuard<'_, T>;
44
45    /// Acquires a write guard, recovering it if the lock is poisoned.
46    ///
47    /// On poison, logs a warning naming the call site and returns the inner
48    /// guard anyway.
49    fn write_or_recover(&self) -> RwLockWriteGuard<'_, T>;
50}
51
52impl<T> RwLockExt<T> for RwLock<T> {
53    #[track_caller]
54    fn read_or_recover(&self) -> RwLockReadGuard<'_, T> {
55        let location = std::panic::Location::caller();
56        self.read().unwrap_or_else(|e| {
57            tracing::warn!(location = %location, "RwLock poisoned — recovering with inner value");
58            e.into_inner()
59        })
60    }
61
62    #[track_caller]
63    fn write_or_recover(&self) -> RwLockWriteGuard<'_, T> {
64        let location = std::panic::Location::caller();
65        self.write().unwrap_or_else(|e| {
66            tracing::warn!(location = %location, "RwLock poisoned — recovering with inner value");
67            e.into_inner()
68        })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::sync::Arc;
76    use std::thread;
77
78    fn poison_mutex(lock: &Arc<Mutex<Vec<u32>>>) {
79        let cloned = Arc::clone(lock);
80        let _ = thread::spawn(move || {
81            let _guard = cloned.lock().unwrap();
82            panic!("poison the lock");
83        })
84        .join();
85    }
86
87    #[test]
88    fn mutex_recovers_after_poison() {
89        let lock = Arc::new(Mutex::new(vec![1u32]));
90        poison_mutex(&lock);
91        assert!(lock.is_poisoned());
92
93        let mut guard = lock.lock_or_recover();
94        guard.push(2);
95        assert_eq!(*guard, vec![1, 2]);
96        drop(guard);
97
98        // Subsequent recoveries keep working too.
99        assert_eq!(lock.lock_or_recover().len(), 2);
100    }
101
102    #[test]
103    fn mutex_unpoisoned_behaves_normally() {
104        let lock = Mutex::new(7u32);
105        assert_eq!(*lock.lock_or_recover(), 7);
106    }
107
108    #[test]
109    fn rwlock_recovers_after_poison() {
110        let lock = Arc::new(RwLock::new(vec![1u32]));
111        let cloned = Arc::clone(&lock);
112        let _ = thread::spawn(move || {
113            let _guard = cloned.write().unwrap();
114            panic!("poison the lock");
115        })
116        .join();
117        assert!(lock.is_poisoned());
118
119        lock.write_or_recover().push(2);
120        assert_eq!(*lock.read_or_recover(), vec![1, 2]);
121    }
122}