The web server holds several long-lived Mutexes in WebState (library, downloader, watched, flags, positions, sessions, …). Every access used .lock().unwrap(), so a single panic while any one was held would poison it and turn EVERY subsequent request into a panic — one handler's bug killing the whole server until restart. New util::LockExt::lock_recover() acquires the guard but recovers the inner data via PoisonError::into_inner() on poison. Our critical sections are short and don't leave half-updated invariants, so the data stays usable and a stuck request beats a stuck process. Applied to all 70 lock sites in web.rs. Test poisons a mutex from a panicking thread and confirms lock_recover still returns usable data. 96 pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
48 lines
1.8 KiB
Rust
48 lines
1.8 KiB
Rust
//! Small shared helpers.
|
|
|
|
use std::sync::{Mutex, MutexGuard};
|
|
|
|
/// Lock a [`Mutex`], recovering the inner data if the lock was poisoned by
|
|
/// a panic in another thread while it was held.
|
|
///
|
|
/// The web server holds several long-lived `Mutex`es in `WebState`. With
|
|
/// the default `.lock().unwrap()`, a single panic while one of them is held
|
|
/// would poison it and turn *every* subsequent access into a panic —
|
|
/// cascading one handler's bug into a permanently dead server. Recovering
|
|
/// via [`std::sync::PoisonError::into_inner`] keeps it serving: our
|
|
/// critical sections are short and don't leave half-updated invariants a
|
|
/// later reader would choke on, so the data behind the lock is still
|
|
/// usable, and one stuck request is far better than a stuck process.
|
|
pub trait LockExt<T> {
|
|
/// Acquire the guard, recovering from poisoning instead of panicking.
|
|
fn lock_recover(&self) -> MutexGuard<'_, T>;
|
|
}
|
|
|
|
impl<T> LockExt<T> for Mutex<T> {
|
|
fn lock_recover(&self) -> MutexGuard<'_, T> {
|
|
self.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::Arc;
|
|
|
|
#[test]
|
|
fn recovers_after_a_panic_poisoned_the_lock() {
|
|
let m = Arc::new(Mutex::new(41));
|
|
// Poison the mutex: panic inside a thread while holding the guard.
|
|
let m2 = m.clone();
|
|
let _ = std::thread::spawn(move || {
|
|
let _g = m2.lock().unwrap();
|
|
panic!("boom while holding the lock");
|
|
})
|
|
.join();
|
|
// Plain lock() would now return Err(PoisonError). lock_recover
|
|
// hands back the data so the server keeps going.
|
|
assert!(m.lock().is_err(), "precondition: mutex is poisoned");
|
|
*m.lock_recover() += 1;
|
|
assert_eq!(*m.lock_recover(), 42);
|
|
}
|
|
}
|