Skip to main content

rx_rust/utils/
on_panic.rs

1//! The guard that undoes what an unwinding callback would otherwise leave behind.
2//!
3//! Everything an operator keeps on its stack — a subscription, an observer, a queued value — is
4//! released by the unwind itself, so it needs no guard. What the unwind does not undo is a change
5//! already written to a **shared state that outlives the panicking call**: a delivery left in its
6//! delivering state, a subscriber counted into a ref count, a source left subscribed after its
7//! termination. Those states are reached by writing them *before* the callback and finishing the
8//! transaction *after* it, which is exactly the step a panic skips.
9//!
10//! [`OnPanic`] runs that finishing step on the unwinding thread instead. Its action therefore
11//! runs under the constraints of a `Drop` during a panic:
12//!
13//! - **It must not panic.** A panic while panicking aborts the process. It may drop values, and
14//!   run the ordinary disposals that dropping them entails, but it should not call further into
15//!   code that is free to unwind.
16//! - **It must not take a lock the panicking thread already holds**, which in this crate means it
17//!   is only ever wrapped around callbacks that run with no lock held — observer notifications and
18//!   external subscriptions. A guard that has to lock is safe exactly where the returning path
19//!   locks too.
20//!
21//! The guard is armed for the scope it is bound to, so it must be bound: `let _guard = …`, and
22//! `drop(guard)` where the scope ends before the enclosing block. When the returning path needs
23//! something back from the guard, park it in the guard's state and take it with
24//! [`OnPanic::disarm`], which ends the scope and hands the state over.
25
26use educe::Educe;
27
28/// Runs `action` with the guarded state if the current scope unwinds, and nothing otherwise.
29///
30/// See the [module documentation](self) for what the action may do. Use [`on_panic`] when there is
31/// no state to carry.
32#[must_use = "a guard that is not bound to a variable is dropped at once, guarding nothing"]
33#[derive(Educe)]
34#[educe(Debug)]
35pub struct OnPanic<T, F: FnOnce(T)>(Option<(T, F)>);
36
37impl<T, F: FnOnce(T)> OnPanic<T, F> {
38    /// Arms the guard, parking `state` in it until the scope ends one way or the other.
39    pub fn new(state: T, action: F) -> Self {
40        Self(Some((state, action)))
41    }
42
43    /// Ends the guarded scope the returning way, handing the state back untouched.
44    pub fn disarm(mut self) -> T {
45        let (state, _action) = self.0.take().expect("the guard is disarmed at most once");
46        state
47    }
48}
49
50impl<T, F: FnOnce(T)> Drop for OnPanic<T, F> {
51    fn drop(&mut self) {
52        if !std::thread::panicking() {
53            return;
54        }
55        if let Some((state, action)) = self.0.take() {
56            action(state);
57        }
58    }
59}
60
61/// Runs `action` if the current scope unwinds, for a guard that carries no state.
62pub fn on_panic<F: FnOnce()>(action: F) -> OnPanic<(), impl FnOnce(())> {
63    OnPanic::new((), move |()| action())
64}