leviath_core/sync.rs
1//! Taking a lock without a panic path.
2//!
3//! `std::sync::Mutex::lock` returns a `Result` because a thread that panics
4//! while holding the guard *poisons* it, and every later locker is told so. The
5//! workspace used to answer that with `.expect("...")` at 21 sites, which turns
6//! one unrelated panic into a daemon-wide cascade: the first failure poisons a
7//! telemetry mutex, and the next observation - on a healthy run, in a different
8//! subsystem - aborts the process.
9//!
10//! # Why recovering is sound here, and would not be everywhere
11//!
12//! Poisoning is not noise. It reports a real condition: a writer stopped
13//! mid-update, so the value may be half-written and its invariants may not hold.
14//! Ignoring that in general is how a corrupt state gets read as a good one, and
15//! a crate that reaches for [`lock`] to silence a poison it has not thought
16//! about has made its data *less* trustworthy, not more.
17//!
18//! What makes it sound in this workspace is a property of the call sites rather
19//! than of this function: **every critical section guarded this way is
20//! panic-free.** They take the lock, do one bounded thing to a container - push,
21//! clone, read a field, swap an `Option` - and drop it. No indexing, no
22//! `unwrap`, no arithmetic that can overflow, no user input parsed, no callback
23//! invoked. A section like that has no intermediate state to be caught in: a
24//! `Vec::push` either happened or did not, and the `Vec` is a valid `Vec` either
25//! way. Poisoning is therefore unreachable, and this function's recovery is not
26//! papering over a risk - it is stating that the risk does not exist and
27//! degrading gracefully if a future edit ever creates one.
28//!
29//! That invariant is the thing to protect. When adding a call, keep the section
30//! short enough that "can anything in here panic?" is answerable by reading it.
31//! If the answer is ever no, hoist the fallible work out of the lock rather than
32//! reaching for this.
33//!
34//! Locks held across an `.await` are `tokio::sync::Mutex`, which has no
35//! poisoning to begin with and is unaffected by any of this.
36
37use std::sync::{Mutex, MutexGuard, PoisonError, TryLockError};
38
39/// Take `mutex`, recovering the guard if a previous holder panicked.
40///
41/// See the module docs: this is only correct because the sections it guards
42/// cannot panic, so the recovery branch is unreachable rather than merely
43/// tolerated.
44pub fn lock<T: ?Sized>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
45 mutex.lock().unwrap_or_else(PoisonError::into_inner)
46}
47
48/// [`lock`], but gives up rather than waiting when the mutex is already held.
49///
50/// `None` means "someone else has it right now", not "it is broken": a poisoned
51/// but free mutex still yields its guard, for the reason [`lock`] documents.
52pub fn try_lock<T: ?Sized>(mutex: &Mutex<T>) -> Option<MutexGuard<'_, T>> {
53 match mutex.try_lock() {
54 Ok(guard) => Some(guard),
55 Err(TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
56 Err(TryLockError::WouldBlock) => None,
57 }
58}
59
60#[cfg(test)]
61mod tests;