fix(process): eliminate stop-signal registration race

Install SIGINT and SIGTERM handlers synchronously before publishing process readiness. Propagate registration failures through Myelin and demo startup instead of silently leaving the default signal action active.
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-08-29 12:28:26 +04:00
parent 02701280ed
commit 74ba89c0ce
4 changed files with 16 additions and 10 deletions

View file

@ -2375,7 +2375,8 @@ fn spawn_stop_listener(
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
swactor_process::spawn_os_stop_signal_wait(sender, actor); swactor_process::spawn_os_stop_signal_wait(sender, actor)
.map_err(|error| format!("install orchestrator stop signal handler: {error}"))?;
Ok(()) Ok(())
} }

View file

@ -147,19 +147,22 @@ fn spawn_stop_channel_wait_thread(
} }
/// Wait for SIGINT/SIGTERM and notify an actor once. /// Wait for SIGINT/SIGTERM and notify an actor once.
///
/// Signal handlers are installed before this function returns. A caller may
/// therefore publish readiness immediately after a successful return without
/// racing the operating system's default signal action.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn spawn_os_stop_signal_wait(sender: ExternalSender, actor: ActorAddress) { pub fn spawn_os_stop_signal_wait(sender: ExternalSender, actor: ActorAddress) -> io::Result<()> {
let mut signals = signal_hook::iterator::Signals::new([
signal_hook::consts::signal::SIGINT,
signal_hook::consts::signal::SIGTERM,
])?;
thread::spawn(move || { thread::spawn(move || {
let Ok(mut signals) = signal_hook::iterator::Signals::new([
signal_hook::consts::signal::SIGINT,
signal_hook::consts::signal::SIGTERM,
]) else {
return;
};
if signals.forever().next().is_some() { if signals.forever().next().is_some() {
let _ = sender.send_to(actor, ProcessStopSignal); let _ = sender.send_to(actor, ProcessStopSignal);
} }
}); });
Ok(())
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]

View file

@ -503,7 +503,8 @@ fn run_supervisor(args: &[String]) -> Result<(), String> {
}) })
.map_err(|error| format!("spawn supervisor stop actor: {error}"))?; .map_err(|error| format!("spawn supervisor stop actor: {error}"))?;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
swactor_process::spawn_os_stop_signal_wait(runtime.create_sender(), stop_actor); swactor_process::spawn_os_stop_signal_wait(runtime.create_sender(), stop_actor)
.map_err(|error| format!("install supervisor stop signal handler: {error}"))?;
println!("demo: dashboard on http://localhost:{port}"); println!("demo: dashboard on http://localhost:{port}");
println!(" /view/fleet — per-node cards (pid, lifecycle)"); println!(" /view/fleet — per-node cards (pid, lifecycle)");
println!(" /view/demo-control — Fleet Control: stages, feeds, kill / provision / edge"); println!(" /view/demo-control — Fleet Control: stages, feeds, kill / provision / edge");

View file

@ -256,7 +256,8 @@ pub fn run_node_role(supervisor_addr_json: &str, attempt: u64) -> Result<(), Str
}) })
.map_err(|error| format!("spawn node stop actor: {error}"))?; .map_err(|error| format!("spawn node stop actor: {error}"))?;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
swactor_process::spawn_os_stop_signal_wait(runtime.create_sender(), stop_actor); swactor_process::spawn_os_stop_signal_wait(runtime.create_sender(), stop_actor)
.map_err(|error| format!("install node stop signal handler: {error}"))?;
// The actor owns lifecycle; the entrypoint waits only for its terminal signal. // The actor owns lifecycle; the entrypoint waits only for its terminal signal.
completion.wait(); completion.wait();