From 156d9e427dadb11ddb884b84743642bcb65124c8 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Thu, 28 May 2026 18:59:33 +0200 Subject: [PATCH] feat: handle SIGTERM for graceful container stops (0.35.0) `docker compose stop` and Kubernetes / Podman / systemd all send SIGTERM first; SIGINT is for interactive shells. Without a SIGTERM listener the container's stop-grace period elapses with the API still running, then SIGKILL skips the daemon shutdown path and leaks Chromium until the OS reaps the parent. Replace the bare `tokio::signal::ctrl_c()` with a select over ctrl_c and SignalKind::terminate() so the daemon.shutdown().await path runs in both cases. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/Cargo.toml | 2 +- backend/src/main.rs | 35 +++++++++++++++++++++++++++++++---- frontend/package.json | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c091570..1209b66 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.34.0" +version = "0.35.0" edition = "2021" default-run = "mangalord" diff --git a/backend/src/main.rs b/backend/src/main.rs index 2d21685..b760157 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -17,10 +17,7 @@ async fn main() -> anyhow::Result<()> { tracing::info!(%addr, "mangalord listening"); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, router) - .with_graceful_shutdown(async { - let _ = tokio::signal::ctrl_c().await; - tracing::info!("ctrl-c received; shutting down"); - }) + .with_graceful_shutdown(shutdown_signal()) .await?; // Drain background tasks (crawler daemon) before exiting so Chromium @@ -30,3 +27,33 @@ async fn main() -> anyhow::Result<()> { } Ok(()) } + +/// Wait for either Ctrl-C (interactive shell) or SIGTERM (Docker / +/// Kubernetes / Podman / systemd stop) and log which arrived. Without +/// the SIGTERM branch, `docker compose stop` runs out its grace period +/// and skips straight to SIGKILL — the daemon never gets the +/// `daemon.shutdown().await` path, leaking Chromium. +async fn shutdown_signal() { + use tokio::signal::unix::{signal, SignalKind}; + let mut sigterm = match signal(SignalKind::terminate()) { + Ok(s) => s, + Err(e) => { + // SignalKind::terminate() is supported on every Unix the + // tokio runtime runs on; if registration fails we still + // honour Ctrl-C so the process is at least + // interactive-shutdownable. + tracing::warn!(error = %e, "could not install SIGTERM handler; falling back to ctrl_c only"); + let _ = tokio::signal::ctrl_c().await; + tracing::info!("ctrl-c received; shutting down"); + return; + } + }; + tokio::select! { + _ = tokio::signal::ctrl_c() => { + tracing::info!("ctrl-c received; shutting down"); + } + _ = sigterm.recv() => { + tracing::info!("SIGTERM received; shutting down"); + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 159dad9..6a36df7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.34.0", + "version": "0.35.0", "private": true, "type": "module", "scripts": {