#![allow(clippy::redundant_pub_crate)]
use std::sync::Arc;
#[non_exhaustive]
pub struct FatalContext {
pub cause: String,
pub site: FatalSite,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FatalSite {
PoolWorker,
InlineSubmit,
ExecutorRunLoop,
}
pub type FatalHandler = Arc<dyn Fn(&FatalContext) + Send + Sync + 'static>;
pub(crate) struct FatalDispatch {
handler: FatalHandler,
terminal: Arc<dyn Fn(&FatalContext) + Send + Sync + 'static>,
}
impl FatalDispatch {
pub(crate) fn new(handler: FatalHandler) -> Self {
Self {
handler,
terminal: Arc::new(|_ctx| std::process::abort()),
}
}
#[cfg(test)]
pub(crate) fn with_terminal(
handler: FatalHandler,
terminal: impl Fn(&FatalContext) + Send + Sync + 'static,
) -> Self {
Self {
handler,
terminal: Arc::new(terminal),
}
}
#[cfg(test)]
pub(crate) fn handler(&self) -> &FatalHandler {
&self.handler
}
pub(crate) fn fire(&self, ctx: &FatalContext) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.handler)(ctx)));
(self.terminal)(ctx);
}
}
pub(crate) fn guard_or_fatal<R>(
fatal: &FatalDispatch,
site: FatalSite,
f: impl FnOnce() -> R,
) -> Option<R> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(r) => Some(r),
Err(payload) => {
let cause =
panic_payload_message(&*payload).unwrap_or_else(|| "framework panic".to_string());
fatal.fire(&FatalContext { cause, site });
None
}
}
}
pub(crate) fn panic_payload_message(payload: &(dyn core::any::Any + Send)) -> Option<String> {
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn panic_payload_message_str_payload() {
let payload = std::panic::catch_unwind(|| panic!("static str msg")).unwrap_err();
assert_eq!(
panic_payload_message(&*payload),
Some("static str msg".to_string())
);
}
#[test]
fn panic_payload_message_string_payload() {
let msg = "owned string msg".to_string();
let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| panic!("{}", msg)))
.unwrap_err();
assert_eq!(
panic_payload_message(&*payload),
Some("owned string msg".to_string())
);
}
#[test]
fn panic_payload_message_non_string_payload() {
let payload = std::panic::catch_unwind(|| std::panic::panic_any(42_u32)).unwrap_err();
assert_eq!(panic_payload_message(&*payload), None);
}
fn recording_terminal() -> (
Arc<Mutex<Vec<String>>>,
impl Fn(&FatalContext) + Send + Sync + 'static,
) {
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let log2 = Arc::clone(&log);
let terminal = move |ctx: &FatalContext| {
log2.lock().unwrap().push(format!("terminal:{}", ctx.cause));
};
(log, terminal)
}
#[test]
fn fire_runs_handler_then_terminal_in_order() {
let order: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let order_h = Arc::clone(&order);
let handler: FatalHandler = Arc::new(move |_ctx| {
order_h.lock().unwrap().push("handler");
});
let order_t = Arc::clone(&order);
let terminal = move |_ctx: &FatalContext| {
order_t.lock().unwrap().push("terminal");
};
let dispatch = FatalDispatch::with_terminal(handler, terminal);
dispatch.fire(&FatalContext {
cause: "boom".to_string(),
site: FatalSite::PoolWorker,
});
let log = order.lock().unwrap().clone();
assert_eq!(
log,
vec!["handler", "terminal"],
"handler must run before terminal"
);
}
#[test]
fn fire_handler_panic_still_reaches_terminal() {
let (log, terminal) = recording_terminal();
let panicking_handler: FatalHandler = Arc::new(|_ctx| panic!("handler exploded"));
let dispatch = FatalDispatch::with_terminal(panicking_handler, terminal);
dispatch.fire(&FatalContext {
cause: "cause-xyz".to_string(),
site: FatalSite::ExecutorRunLoop,
});
let entries = log.lock().unwrap().clone();
assert!(
entries.iter().any(|e| e.contains("terminal:cause-xyz")),
"terminal not reached after handler panic; log: {entries:?}"
);
}
type Recorder = Arc<Mutex<Vec<(FatalSite, String)>>>;
fn recording_dispatch() -> (Recorder, FatalDispatch) {
let rec: Recorder = Arc::new(Mutex::new(Vec::new()));
let rec2 = Arc::clone(&rec);
let handler: FatalHandler = Arc::new(|_ctx| {});
let dispatch = FatalDispatch::with_terminal(handler, move |ctx| {
rec2.lock().unwrap().push((ctx.site, ctx.cause.clone()));
});
(rec, dispatch)
}
#[test]
fn guard_or_fatal_success_returns_some_and_does_not_fire() {
let (rec, dispatch) = recording_dispatch();
let out = guard_or_fatal(&dispatch, FatalSite::ExecutorRunLoop, || 7_u32);
assert_eq!(out, Some(7));
assert!(
rec.lock().unwrap().is_empty(),
"terminal must not fire on success"
);
}
#[test]
fn guard_or_fatal_panic_fires_once_with_site_and_cause() {
let (rec, dispatch) = recording_dispatch();
let out: Option<()> = guard_or_fatal(&dispatch, FatalSite::PoolWorker, || {
panic!("synthetic infra panic")
});
assert!(
out.is_none(),
"panic path must yield None under test terminal"
);
let entries = rec.lock().unwrap().clone();
assert_eq!(entries.len(), 1, "fatal must fire exactly once");
assert_eq!(entries[0].0, FatalSite::PoolWorker);
assert_eq!(entries[0].1, "synthetic infra panic");
}
#[test]
fn guard_or_fatal_propagates_run_loop_site() {
let (rec, dispatch) = recording_dispatch();
let out: Option<()> = guard_or_fatal(&dispatch, FatalSite::ExecutorRunLoop, || {
panic!("run-loop boom")
});
assert!(out.is_none());
let entries = rec.lock().unwrap().clone();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, FatalSite::ExecutorRunLoop);
assert_eq!(entries[0].1, "run-loop boom");
}
#[test]
fn guard_or_fatal_non_string_payload_uses_fallback_cause() {
let (rec, dispatch) = recording_dispatch();
let out: Option<()> = guard_or_fatal(&dispatch, FatalSite::InlineSubmit, || {
std::panic::panic_any(42_u32)
});
assert!(out.is_none());
let entries = rec.lock().unwrap().clone();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].1, "framework panic");
}
#[test]
fn fire_default_noop_handler_reaches_terminal() {
let (log, terminal) = recording_terminal();
let noop: FatalHandler = Arc::new(|_ctx| {});
let dispatch = FatalDispatch::with_terminal(noop, terminal);
dispatch.fire(&FatalContext {
cause: "default".to_string(),
site: FatalSite::InlineSubmit,
});
let entries = log.lock().unwrap().clone();
assert!(
entries.iter().any(|e| e.contains("terminal:default")),
"terminal not reached for default no-op handler; log: {entries:?}"
);
}
}