From 14e89af96a801bd723068467db540fcfbfd1219a Mon Sep 17 00:00:00 2001 From: Alan Cruz Date: Wed, 2 Sep 2026 14:49:33 -0400 Subject: [PATCH] fix(hfs): drain on SIGTERM as well as Ctrl-C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The graceful-shutdown future only waited on Ctrl-C, so a SIGTERM from a process supervisor, container runtime, or a load balancer's rolling deploy killed the server after the grace period with requests in flight and the audit sink unflushed — the normal way an instance in a cluster is stopped. `serve()` now waits on a `shutdown_signal()` that selects over Ctrl-C and, on Unix, SIGTERM, and logs which signal fired; the drain, audit flush and OTLP flush that follow are unchanged. A failed SIGTERM handler install warns and keeps Ctrl-C only; non-Unix builds keep Ctrl-C only. Tests: verified by hand — a running instance answers /health, receives `kill -TERM`, logs `Shutdown signal received … signal="SIGTERM"` and exits within a second. The two-instance smoke's stop step switches to SIGTERM in the next cluster PR so this stays covered in CI. --- crates/hfs/src/main.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/hfs/src/main.rs b/crates/hfs/src/main.rs index 3c6116d1e..375c2ba62 100644 --- a/crates/hfs/src/main.rs +++ b/crates/hfs/src/main.rs @@ -660,6 +660,39 @@ async fn start_mongodb( ) } +/// Resolves when the process is asked to stop, returning the signal's name. +/// +/// Ctrl-C (SIGINT) is what a developer sends from a terminal; SIGTERM is what +/// a process supervisor, container runtime, or load-balanced rolling deploy +/// sends. Both must drain in-flight requests and flush the audit sink the +/// same way — an instance that ignores SIGTERM is killed after the runtime's +/// grace period with requests still in flight. +async fn shutdown_signal() -> &'static str { + let ctrl_c = async { + let _ = tokio::signal::ctrl_c().await; + }; + + #[cfg(unix)] + let terminate = async { + match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { + Ok(mut stream) => { + stream.recv().await; + } + Err(error) => { + warn!(%error, "failed to install the SIGTERM handler; only Ctrl-C will stop this instance"); + std::future::pending::<()>().await; + } + } + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => "SIGINT", + _ = terminate => "SIGTERM", + } +} + /// Starts the Axum HTTP server. async fn serve( app: axum::Router, @@ -742,8 +775,8 @@ async fn serve( app.into_make_service_with_connect_info::(), ) .with_graceful_shutdown(async move { - let _ = tokio::signal::ctrl_c().await; - info!("Shutdown signal received, draining connections"); + let signal = shutdown_signal().await; + info!(signal, "Shutdown signal received, draining connections"); if let Some(state) = audit_state { lifecycle::record_shutdown(&*state.sink, &state.config.source_observer).await; state.sink.flush().await;