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}