Skip to main content

asc_cache/
lock.rs

1//! How the workspace treats a poisoned lock.
2//!
3//! A `Mutex` is poisoned when a thread panics while holding it. Hosts that
4//! stay alive (the MCP server, the daemon) contain a panicking request with
5//! `catch_unwind`, so whatever that request held stays poisoned for every
6//! later one. Each lock takes one of two policies:
7//!
8//! * **Caches and derived state** (built slots, LRUs, pools of open
9//!   engines, accounting that can start over) lock with
10//!   [`LockExt::lock_or_reset`] / [`LockExt::lock_or_default`]: after a
11//!   panic the state may be half-written, so it is reset (rebuilt on demand)
12//!   and the poison cleared. One bad request costs a rebuild, not the host.
13//! * **Everything else** locks with `.lock().expect("<what> mutex
14//!   poisoned")`: short bookkeeping that cannot panic in practice, and the
15//!   collectors of scoped worker threads (the scope re-raises a worker's
16//!   panic anyway). Poison there is a bug that should surface.
17//!
18//! Expensive or untrusted work (parsing a DEX, building a graph) runs
19//! outside cache locks, so a panic in it poisons nothing.
20
21use std::sync::{Mutex, MutexGuard};
22
23/// Locking a [`Mutex`] whose state can be thrown away after a panic.
24pub trait LockExt<T> {
25    /// Lock; if a panic poisoned the lock, `reset` the state first and clear
26    /// the poison.
27    fn lock_or_reset(&self, reset: impl FnOnce(&mut T)) -> MutexGuard<'_, T>;
28
29    /// [`LockExt::lock_or_reset`] back to `T::default()`.
30    fn lock_or_default(&self) -> MutexGuard<'_, T>
31    where
32        T: Default,
33    {
34        self.lock_or_reset(|state| *state = T::default())
35    }
36}
37
38impl<T> LockExt<T> for Mutex<T> {
39    fn lock_or_reset(&self, reset: impl FnOnce(&mut T)) -> MutexGuard<'_, T> {
40        self.lock().unwrap_or_else(|poisoned| {
41            let mut state = poisoned.into_inner();
42            reset(&mut state);
43            self.clear_poison();
44            state
45        })
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    fn poison(m: &Mutex<Vec<u32>>) {
54        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
55            let mut v = m.lock().unwrap();
56            v.push(2);
57            panic!("half-written");
58        }));
59        assert!(caught.is_err() && m.is_poisoned());
60    }
61
62    #[test]
63    fn a_poisoned_lock_resets_once_and_is_healthy_again() {
64        let m = Mutex::new(vec![1]);
65        assert_eq!(*m.lock_or_default(), [1]);
66        poison(&m);
67        assert!(m.lock_or_default().is_empty());
68        assert!(!m.is_poisoned());
69        m.lock_or_default().push(3);
70        assert_eq!(*m.lock_or_reset(|_| unreachable!("not poisoned")), [3]);
71        poison(&m);
72        assert_eq!(*m.lock_or_reset(|v| v.retain(|&x| x != 2)), [3]);
73    }
74}