use std::any::Any;
use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::runtime::log::{ErrlogSevEnum, errlog_sev_printf};
thread_local! {
static ON_FACILITY_THREAD: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn on_facility_thread() -> bool {
ON_FACILITY_THREAD.with(Cell::get)
}
struct FacilityThreadMark(bool);
impl FacilityThreadMark {
fn set() -> Self {
FacilityThreadMark(ON_FACILITY_THREAD.with(|f| f.replace(true)))
}
}
impl Drop for FacilityThreadMark {
fn drop(&mut self) {
ON_FACILITY_THREAD.with(|f| f.set(self.0));
}
}
static POISON_ANNOUNCED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn poison_should_be_announced() -> bool {
!POISON_ANNOUNCED.swap(true, std::sync::atomic::Ordering::Relaxed)
}
pub fn recover<T>(facility: &str, result: std::sync::LockResult<T>) -> T {
match result {
Ok(guard) => guard,
Err(poisoned) => {
if poison_should_be_announced() {
errlog_sev_printf(
ErrlogSevEnum::Minor,
&format!(
"{facility}: recovered a lock poisoned by a panic elsewhere; \
the queued work is intact and the facility continues. Later \
recoveries are not repeated."
),
);
}
poisoned.into_inner()
}
}
}
pub fn run_isolated(facility: &str, cb: impl FnOnce()) -> bool {
match catch_unwind(AssertUnwindSafe(cb)) {
Ok(()) => true,
Err(payload) => {
errlog_sev_printf(
ErrlogSevEnum::Major,
&format!(
"{facility}: a callback panicked ({}); the callback was abandoned, \
the facility continues",
panic_text(&*payload)
),
);
false
}
}
}
pub(super) fn run_facility_loop(facility: &str, body: impl FnOnce(), on_lost: impl FnOnce()) {
let _marked = FacilityThreadMark::set();
if let Err(payload) = catch_unwind(AssertUnwindSafe(body)) {
on_lost();
errlog_sev_printf(
ErrlogSevEnum::Fatal,
&format!(
"{facility}: the worker thread has STOPPED ({}). Every operation this \
facility carries has stopped with it; the IOC keeps serving requests.",
panic_text(&*payload)
),
);
}
}
fn panic_text(payload: &(dyn Any + Send)) -> &str {
if let Some(s) = payload.downcast_ref::<&'static str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"non-string panic payload"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
#[test]
fn a_poisoned_lock_still_yields_its_data() {
let m = std::sync::Mutex::new(7u32);
let _ = catch_unwind(AssertUnwindSafe(|| {
let _g = m.lock().expect("first lock");
panic!("poison it");
}));
assert!(m.lock().is_err(), "the mutex must actually be poisoned");
assert_eq!(
*recover("test-facility", m.lock()),
7,
"the data survived the panic"
);
}
#[test]
fn an_unwinding_callback_is_contained_and_reported() {
assert!(!run_isolated("test-facility", || panic!(
"callback blew up"
)));
assert!(run_isolated("test-facility", || {}));
}
#[test]
fn a_lost_loop_marks_the_facility_before_it_announces() {
let marked = AtomicBool::new(false);
run_facility_loop(
"test-facility",
|| panic!("loop blew up"),
|| marked.store(true, Ordering::SeqCst),
);
assert!(
marked.load(Ordering::SeqCst),
"a lost facility must be marked stopped, or submitters queue into a \
ring with no worker"
);
}
#[test]
fn a_loop_that_returns_normally_is_not_announced() {
let marked = AtomicBool::new(false);
run_facility_loop(
"test-facility",
|| {},
|| marked.store(true, Ordering::SeqCst),
);
assert!(
!marked.load(Ordering::SeqCst),
"an orderly shutdown is not a loss"
);
}
#[test]
fn no_facility_propagates_a_poisoned_lock() {
let files = [
(
"delayed_timer.rs",
include_str!("delayed_timer.rs"),
"delayed-callback timer",
),
(
"scan_once.rs",
include_str!("scan_once.rs"),
"scanOnce worker",
),
(
"callback_executor.rs",
include_str!("callback_executor.rs"),
"callback band",
),
];
let poison = concat!(".lock()", ".unwrap()");
let wait_poison = concat!(".wait(", "st).unwrap()");
let mut offences = Vec::new();
for (label, src, facility) in files {
let prod = match src.find("\n#[cfg(test)]") {
Some(i) => &src[..i],
None => src,
};
for (n, line) in prod.lines().enumerate() {
if line.trim_start().starts_with("//") {
continue;
}
if line.contains(poison) || line.contains(wait_poison) {
offences.push(format!("{label}:{}", n + 1));
}
}
assert!(
prod.contains("run_facility_loop("),
"{label} does not guard its worker loop: its loss would be silent, \
and its thread would not be marked a facility worker — so \
`block_on_sync` would hand it a blocking bridge that parks the \
facility"
);
assert!(
prod.contains("run_isolated("),
"{label} runs a caller's callback unprotected on its own thread"
);
assert!(
prod.contains(facility),
"{label} must name itself ({facility:?}) in what it reports"
);
}
assert!(
offences.is_empty(),
"these lock sites propagate poisoning instead of recovering: {offences:?}"
);
}
#[test]
#[serial_test::serial]
fn the_first_poison_recovery_is_the_one_announced() {
POISON_ANNOUNCED.store(false, Ordering::SeqCst);
assert!(poison_should_be_announced(), "the first is announced");
assert!(!poison_should_be_announced(), "the second is not");
assert!(!poison_should_be_announced(), "nor any later one");
}
#[test]
fn the_panic_message_survives_into_the_report() {
let payload = catch_unwind(|| panic!("a formatted {}", "message")).expect_err("panics");
assert_eq!(panic_text(&*payload), "a formatted message");
let payload = catch_unwind(|| panic!("a literal")).expect_err("panics");
assert_eq!(panic_text(&*payload), "a literal");
}
}