use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
static SESSION_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn flag() -> &'static Arc<AtomicBool> {
SESSION_STOP.get_or_init(|| Arc::new(AtomicBool::new(false)))
}
#[inline]
pub fn stop_requested() -> bool {
let global = SESSION_STOP
.get()
.map(|f| f.load(Ordering::Relaxed))
.unwrap_or(false);
let local = crate::execution_context::current_stop()
.map(|f| f.load(Ordering::Relaxed))
.unwrap_or(false);
global || local
}
pub fn request_stop() {
flag().store(true, Ordering::Relaxed);
}
const SHUTDOWN_COUNTDOWN_SECS: u64 = 10;
static SHUTDOWN: OnceLock<tokio::sync::watch::Sender<u8>> = OnceLock::new();
static SHUTDOWN_DONE: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn shutdown_tx() -> &'static tokio::sync::watch::Sender<u8> {
SHUTDOWN.get_or_init(|| tokio::sync::watch::channel(0u8).0)
}
fn done_flag() -> &'static Arc<AtomicBool> {
SHUTDOWN_DONE.get_or_init(|| Arc::new(AtomicBool::new(false)))
}
#[inline]
pub fn shutdown_level() -> u8 {
SHUTDOWN.get().map(|tx| *tx.borrow()).unwrap_or(0)
}
#[inline]
pub fn cancel_ops_requested() -> bool {
shutdown_level() >= 2
}
pub fn subscribe_shutdown() -> tokio::sync::watch::Receiver<u8> {
shutdown_tx().subscribe()
}
pub async fn ops_cancelled(rx: &mut tokio::sync::watch::Receiver<u8>) {
loop {
if *rx.borrow() >= 2 {
return;
}
if rx.changed().await.is_err() {
std::future::pending::<()>().await;
}
}
}
pub fn mark_shutdown_complete() {
done_flag().store(true, Ordering::Relaxed);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShutdownOrigin {
CtrlC,
Term,
StopAction,
}
impl ShutdownOrigin {
fn lead(self) -> &'static str {
match self {
ShutdownOrigin::CtrlC => "session: graceful shutdown requested (Ctrl-C).",
ShutdownOrigin::Term => "session: graceful shutdown requested (SIGTERM).",
ShutdownOrigin::StopAction => "session: shutdown requested by stop action `abort`.",
}
}
fn trigger(self) -> &'static str {
match self {
ShutdownOrigin::CtrlC => "Ctrl-C",
ShutdownOrigin::Term => "SIGTERM",
ShutdownOrigin::StopAction => "stop action `abort`",
}
}
}
pub fn abort_shutdown(origin: ShutdownOrigin) -> u8 {
request_stop();
let modified = shutdown_tx().send_if_modified(|level| {
if *level < 2 {
*level = 2;
true
} else {
false
}
});
if modified {
crate::diag!(
crate::observer::LogLevel::Warn,
"session: aborting on {} — cancelling in-flight ops now \
(skipping the cooperative drain); remaining phases skipped. \
Process-level cleanup (metrics flush, WAL consolidation, \
summaries) still runs. Ctrl-C to force-exit.",
origin.trigger()
);
}
2
}
pub fn escalate_shutdown(origin: ShutdownOrigin) -> u8 {
let tx = shutdown_tx();
let mut entered: u8 = 0;
tx.send_if_modified(|level| {
if *level >= 2 {
entered = *level;
false
} else {
*level += 1;
entered = *level;
true
}
});
match entered {
1 => {
request_stop();
crate::diag!(
crate::observer::LogLevel::Info,
"{} Active fibers exit at the next cycle boundary; profiler / \
metrics / summaries will flush. In-flight ops will be \
CANCELLED in {SHUTDOWN_COUNTDOWN_SECS}s (Ctrl-C: cancel \
them now; a further Ctrl-C force-exits).",
origin.lead()
);
#[cfg(not(test))]
spawn_cancel_countdown();
}
2 => announce_cancel_ops(),
_ => {}
}
entered
}
#[cfg_attr(test, allow(dead_code))]
fn escalate_cancel_ops() {
let tx = shutdown_tx();
let modified = tx.send_if_modified(|level| {
if *level < 2 {
*level = 2;
true
} else {
false
}
});
if modified {
announce_cancel_ops();
}
}
fn announce_cancel_ops() {
crate::diag!(
crate::observer::LogLevel::Warn,
"session: cancelling in-flight ops — process-level cleanup \
(metrics flush, WAL consolidation, summaries) continues. \
Ctrl-C again to force-exit."
);
}
#[cfg_attr(test, allow(dead_code))]
fn spawn_cancel_countdown() {
let done = done_flag().clone();
std::thread::Builder::new()
.name("shutdown-countdown".into())
.spawn(move || {
for remaining in (1..=SHUTDOWN_COUNTDOWN_SECS).rev() {
if done.load(Ordering::Relaxed) || shutdown_level() >= 2 {
return;
}
crate::diag!(
crate::observer::LogLevel::Info,
"session: cancelling in-flight ops in {remaining}s \
(Ctrl-C to cancel now)"
);
std::thread::sleep(std::time::Duration::from_secs(1));
}
if !done.load(Ordering::Relaxed) {
escalate_cancel_ops();
}
})
.expect("spawn shutdown-countdown thread");
}
static GRACEFUL_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
#[inline]
pub fn graceful_stop_requested() -> bool {
GRACEFUL_STOP
.get()
.map(|f| f.load(Ordering::Relaxed))
.unwrap_or(false)
}
pub fn request_graceful_stop() {
GRACEFUL_STOP
.get_or_init(|| Arc::new(AtomicBool::new(false)))
.store(true, Ordering::Relaxed);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopCause {
Interrupt,
Fault,
}
static FAULT_STOP: OnceLock<Arc<AtomicBool>> = OnceLock::new();
#[inline]
pub fn fault_stop_requested() -> bool {
FAULT_STOP
.get()
.map(|f| f.load(Ordering::Relaxed))
.unwrap_or(false)
}
pub fn request_fault_stop() {
FAULT_STOP
.get_or_init(|| Arc::new(AtomicBool::new(false)))
.store(true, Ordering::Relaxed);
}
pub fn request_shell_stop(cause: StopCause) {
match cause {
StopCause::Fault => request_fault_stop(),
StopCause::Interrupt => request_graceful_stop(),
}
}
#[derive(Clone, Default)]
pub struct StopView {
activity: Option<Arc<AtomicBool>>,
walk: Option<Arc<AtomicBool>>,
daemon: Option<Arc<AtomicBool>>,
}
impl StopView {
pub fn new(
activity: Option<Arc<AtomicBool>>,
walk: Option<Arc<AtomicBool>>,
daemon: Option<Arc<AtomicBool>>,
) -> Self {
Self {
activity,
walk,
daemon,
}
}
#[inline]
fn on(f: &Option<Arc<AtomicBool>>) -> bool {
f.as_ref().is_some_and(|b| b.load(Ordering::Relaxed))
}
#[inline]
pub fn stopped(&self) -> bool {
Self::on(&self.activity)
|| stop_requested()
|| Self::on(&self.walk)
|| Self::on(&self.daemon)
}
#[inline]
pub fn abnormal(&self) -> bool {
Self::on(&self.activity) || stop_requested() || Self::on(&self.walk)
}
#[inline]
pub fn poll(&self) -> Option<StopCause> {
if fault_stop_requested() || Self::on(&self.activity) {
Some(StopCause::Fault)
} else if self.stopped() {
Some(StopCause::Interrupt)
} else {
None
}
}
}
pub fn install_signal_handler() {
LADDER_ARMED.store(true, Ordering::Relaxed);
static INSTALLED: OnceLock<()> = OnceLock::new();
if INSTALLED.set(()).is_err() {
return;
}
let _ = flag();
tokio::spawn(async move {
loop {
if tokio::signal::ctrl_c().await.is_err() {
return;
}
if shutdown_level() >= 2 {
crate::diag!(
crate::observer::LogLevel::Warn,
"session: force-exit (Ctrl-C past the cancel rung) — \
profiler output and metrics may be incomplete."
);
std::process::exit(130);
}
escalate_shutdown(ShutdownOrigin::CtrlC);
}
});
}
static LADDER_ARMED: AtomicBool = AtomicBool::new(false);
#[cfg(unix)]
#[inline]
fn ladder_armed() -> bool {
LADDER_ARMED.load(Ordering::Relaxed)
}
static CONSOLE_LOSS_HOOK: OnceLock<Box<dyn Fn() + Send + Sync>> = OnceLock::new();
pub fn set_console_loss_hook(hook: Box<dyn Fn() + Send + Sync>) {
let _ = CONSOLE_LOSS_HOOK.set(hook);
}
static DIAG_DUMP_HOOK: OnceLock<Box<dyn Fn() + Send + Sync>> = OnceLock::new();
pub fn set_diag_dump_hook(hook: Box<dyn Fn() + Send + Sync>) {
let _ = DIAG_DUMP_HOOK.set(hook);
}
#[cfg(unix)]
const DISPATCHED_SIGNALS: [libc::c_int; 4] =
[libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT];
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SignalAction {
Exit(i32),
Escalate(ShutdownOrigin),
ForceExit(i32),
ConsoleLoss,
DiagDump,
}
#[cfg(unix)]
fn dispatch_decision(signo: libc::c_int, armed: bool, level: u8) -> SignalAction {
let exit_code = 128 + signo as i32;
match signo {
libc::SIGINT | libc::SIGTERM => {
let origin = if signo == libc::SIGTERM {
ShutdownOrigin::Term
} else {
ShutdownOrigin::CtrlC
};
if !armed {
SignalAction::Exit(exit_code)
} else if level >= 2 {
SignalAction::ForceExit(exit_code)
} else {
SignalAction::Escalate(origin)
}
}
libc::SIGHUP if armed => SignalAction::ConsoleLoss,
libc::SIGQUIT if armed => SignalAction::DiagDump,
_ => SignalAction::Exit(exit_code),
}
}
#[cfg(unix)]
pub fn block_shutdown_signals() {
unsafe {
let mut set: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut set);
for s in DISPATCHED_SIGNALS {
libc::sigaddset(&mut set, s);
}
libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut());
}
}
#[cfg(not(unix))]
pub fn block_shutdown_signals() {}
#[cfg(unix)]
pub fn spawn_signal_dispatcher() {
static SPAWNED: OnceLock<()> = OnceLock::new();
if SPAWNED.set(()).is_err() {
return;
}
std::thread::Builder::new()
.name("signal-dispatch".into())
.spawn(|| {
let mut set: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe {
libc::sigemptyset(&mut set);
for s in DISPATCHED_SIGNALS {
libc::sigaddset(&mut set, s);
}
}
loop {
let mut signo: libc::c_int = 0;
if unsafe { libc::sigwait(&set, &mut signo) } != 0 {
return;
}
match dispatch_decision(signo, ladder_armed(), shutdown_level()) {
SignalAction::Exit(code) => std::process::exit(code),
SignalAction::Escalate(origin) => {
escalate_shutdown(origin);
}
SignalAction::ForceExit(code) => {
crate::diag!(
crate::observer::LogLevel::Warn,
"session: force-exit (signal past the cancel rung) — \
profiler output and metrics may be incomplete."
);
std::process::exit(code);
}
SignalAction::ConsoleLoss => {
if let Some(hook) = CONSOLE_LOSS_HOOK.get() {
hook();
}
crate::diag!(
crate::observer::LogLevel::Warn,
"session: SIGHUP — controlling terminal lost; \
continuing headless (the run is unaffected)."
);
}
SignalAction::DiagDump => {
crate::diag!(
crate::observer::LogLevel::Info,
"session: SIGQUIT diagnostics — shutdown ladder level {}, \
session stop {}, graceful stop {}, fault stop {}.",
shutdown_level(),
stop_requested(),
graceful_stop_requested(),
fault_stop_requested()
);
if let Some(hook) = DIAG_DUMP_HOOK.get() {
hook();
}
}
}
}
})
.expect("spawn signal-dispatch thread");
}
#[cfg(not(unix))]
pub fn spawn_signal_dispatcher() {}
#[cfg(test)]
pub(crate) static STOP_GLOBAL_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) fn clear_session_stop_for_test() {
flag().store(false, Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn reset_shutdown_ladder_for_test() {
let _ = shutdown_tx().send_replace(0);
done_flag().store(false, Ordering::Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_starts_unset_and_responds_to_request() {
let _guard = STOP_GLOBAL_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
clear_session_stop_for_test();
assert!(!stop_requested());
request_stop();
assert!(stop_requested());
clear_session_stop_for_test();
}
#[test]
fn ladder_advances_one_rung_per_escalation_and_holds_at_cancel() {
let _guard = STOP_GLOBAL_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
clear_session_stop_for_test();
reset_shutdown_ladder_for_test();
assert_eq!(shutdown_level(), 0);
assert!(!cancel_ops_requested());
assert_eq!(
escalate_shutdown(ShutdownOrigin::CtrlC),
1,
"first rung: graceful"
);
assert!(stop_requested(), "graceful rung sets the session stop");
assert!(!cancel_ops_requested());
assert_eq!(
escalate_shutdown(ShutdownOrigin::CtrlC),
2,
"second rung: cancel ops"
);
assert!(cancel_ops_requested());
assert_eq!(
escalate_shutdown(ShutdownOrigin::CtrlC),
2,
"ladder holds at cancel"
);
assert!(cancel_ops_requested());
clear_session_stop_for_test();
reset_shutdown_ladder_for_test();
}
#[test]
fn abort_jumps_straight_to_cancel_rung() {
let _guard = STOP_GLOBAL_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
clear_session_stop_for_test();
reset_shutdown_ladder_for_test();
assert_eq!(shutdown_level(), 0);
assert!(!stop_requested());
assert_eq!(
abort_shutdown(ShutdownOrigin::StopAction),
2,
"abort skips the graceful rung and lands on cancel-ops"
);
assert!(cancel_ops_requested(), "in-flight ops cancel immediately");
assert!(stop_requested(), "abort also raises the session stop");
assert_eq!(abort_shutdown(ShutdownOrigin::StopAction), 2);
clear_session_stop_for_test();
reset_shutdown_ladder_for_test();
}
#[tokio::test]
async fn ops_cancelled_resolves_at_cancel_rung_only() {
let _guard = STOP_GLOBAL_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
clear_session_stop_for_test();
reset_shutdown_ladder_for_test();
let mut rx = subscribe_shutdown();
escalate_shutdown(ShutdownOrigin::CtrlC); let pending =
tokio::time::timeout(std::time::Duration::from_millis(50), ops_cancelled(&mut rx))
.await;
assert!(pending.is_err(), "graceful rung must not cancel ops");
escalate_shutdown(ShutdownOrigin::CtrlC); tokio::time::timeout(
std::time::Duration::from_millis(200),
ops_cancelled(&mut rx),
)
.await
.expect("cancel rung resolves the in-flight race");
let mut late = subscribe_shutdown();
tokio::time::timeout(
std::time::Duration::from_millis(200),
ops_cancelled(&mut late),
)
.await
.expect("already-cancelled resolves immediately");
clear_session_stop_for_test();
reset_shutdown_ladder_for_test();
}
#[cfg(unix)]
#[test]
fn dispatch_routes_by_signal_arming_and_rung() {
use SignalAction::*;
assert_eq!(dispatch_decision(libc::SIGINT, false, 0), Exit(130));
assert_eq!(dispatch_decision(libc::SIGTERM, false, 0), Exit(143));
assert_eq!(dispatch_decision(libc::SIGHUP, false, 0), Exit(129));
assert_eq!(dispatch_decision(libc::SIGQUIT, false, 0), Exit(131));
assert_eq!(
dispatch_decision(libc::SIGINT, true, 0),
Escalate(ShutdownOrigin::CtrlC)
);
assert_eq!(
dispatch_decision(libc::SIGTERM, true, 1),
Escalate(ShutdownOrigin::Term)
);
assert_eq!(dispatch_decision(libc::SIGINT, true, 2), ForceExit(130));
assert_eq!(dispatch_decision(libc::SIGTERM, true, 2), ForceExit(143));
assert_eq!(dispatch_decision(libc::SIGHUP, true, 0), ConsoleLoss);
assert_eq!(dispatch_decision(libc::SIGHUP, true, 2), ConsoleLoss);
assert_eq!(dispatch_decision(libc::SIGQUIT, true, 0), DiagDump);
assert_eq!(dispatch_decision(libc::SIGQUIT, true, 2), DiagDump);
}
}