diff --git a/backend/src/main.rs b/backend/src/main.rs index 50317a04..c19adb38 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -6,6 +6,7 @@ //! [`predifi_backend::run_server`]. use predifi_backend::config::Config; +use predifi_backend::run_server; /// Build the CORS middleware layer from the validated origin list in `config`. /// diff --git a/backend/src/shutdown.rs b/backend/src/shutdown.rs index a04fcac6..578f5147 100644 --- a/backend/src/shutdown.rs +++ b/backend/src/shutdown.rs @@ -48,6 +48,38 @@ use tracing::{info, warn}; /// On non-Unix platforms only `SIGINT` is registered. #[cfg(unix)] pub async fn wait_for_signal() { + // Build the three arms: a missing handler becomes a future that never + // resolves, leaving the surviving handlers in charge of triggering the + // `select!`. No early-return paths are taken because a single failed + // installation must never prevent the other signals from working. + let mut terminate_signal = match tokio::signal::unix::signal( + tokio::signal::unix::SignalKind::terminate(), + ) { + Ok(signal) => Some(signal), + Err(error) => { + warn!(error = %error, "failed to install SIGTERM handler; skipping"); + None + } + }; + + let mut hangup_signal = match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) { + Ok(signal) => Some(signal), + Err(error) => { + warn!(error = %error, "failed to install SIGHUP handler; skipping"); + None + } + }; + + let ctrl_c_block = async { + match tokio::signal::ctrl_c().await { + Ok(()) => info!("received Ctrl+C, beginning graceful shutdown"), + Err(error) => warn!(error = %error, "Ctrl+C handler failed; relying on SIGTERM/SIGHUP"), + } + }; + + let terminate_block = async { + match terminate_signal.as_mut() { + Some(signal) => { async fn wait_sigterm() { match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { Ok(mut signal) => { @@ -61,6 +93,9 @@ pub async fn wait_for_signal() { } } + let hangup_block = async { + match hangup_signal.as_mut() { + Some(signal) => { async fn wait_sighup() { match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) { Ok(mut signal) => { @@ -162,6 +197,12 @@ mod tests { /// the future would take longer than the deadline. #[tokio::test] async fn shutdown_timeout_returns_after_deadline_when_future_is_slow() { + // The inner future sleeps for 2 s; the timeout deadline is 100 ms. + // The helper must return after ~100 ms, well before the 2-second sleep + // would complete. + let start = tokio::time::Instant::now(); + with_shutdown_timeout(Duration::from_millis(100), "slow-unit", async { + tokio::time::sleep(Duration::from_secs(2)).await; let start = tokio::time::Instant::now(); with_shutdown_timeout(Duration::from_millis(50), "slow-unit", async { tokio::time::sleep(Duration::from_secs(10)).await; @@ -169,6 +210,11 @@ mod tests { .await; let elapsed = start.elapsed(); assert!( + elapsed >= Duration::from_millis(100), + "helper should have waited at least the deadline (got {elapsed:?})" + ); + assert!( + elapsed < Duration::from_secs(2), elapsed >= Duration::from_millis(50), "helper should have waited at least the deadline (got {elapsed:?})" );