From 7e235ffd3e88c66c3072dc499146378efc47cc9a Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Thu, 4 Jun 2026 21:42:55 +0200 Subject: [PATCH] feat(server): graceful shutdown on SIGINT/SIGTERM (#1) --- crates/server/src/lib.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index a355cd6..ff2d3bc 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -64,6 +64,34 @@ pub async fn run(config: Config) -> anyhow::Result<()> { serve(listener, state).await } +/// Resolves when the process receives SIGINT (Ctrl-C) or SIGTERM, so the server can +/// drain in-flight requests before exiting. +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("install Ctrl-C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("install SIGTERM handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + tracing::info!("shutdown signal received; draining"); +} + /// Serve the API on an already-bound listener (used by `run` and tests). pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> { let app = build_app(state); @@ -72,6 +100,7 @@ pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> let app = app.merge(web_assets::routes()); axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) .await .context("running the HTTP server")?;