Compare commits

..

1 Commits

Author SHA1 Message Date
MechaCat02
156d9e427d 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) <noreply@anthropic.com>
2026-05-28 18:59:33 +02:00
4 changed files with 33 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "mangalord" name = "mangalord"
version = "0.34.0" version = "0.35.0"
edition = "2021" edition = "2021"
default-run = "mangalord" default-run = "mangalord"

View File

@@ -1,15 +0,0 @@
-- The original 0012 partial index covers `state IN ('pending','failed')`,
-- but `ack_failed` in src/crawler/jobs.rs only writes `dead` or
-- `pending` — `failed` is never set. The index branch on `failed`
-- never matches any row, so it's dead weight on every write.
--
-- Drop and recreate the index without the dead branch. The CHECK
-- constraint on `state` still allows `'failed'` so a future migration
-- can adopt that terminal-but-retryable state without a second
-- schema change.
DROP INDEX IF EXISTS crawler_jobs_ready_idx;
CREATE INDEX crawler_jobs_ready_idx
ON crawler_jobs (scheduled_at)
WHERE state = 'pending';

View File

@@ -17,10 +17,7 @@ async fn main() -> anyhow::Result<()> {
tracing::info!(%addr, "mangalord listening"); tracing::info!(%addr, "mangalord listening");
let listener = tokio::net::TcpListener::bind(addr).await?; let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, router) axum::serve(listener, router)
.with_graceful_shutdown(async { .with_graceful_shutdown(shutdown_signal())
let _ = tokio::signal::ctrl_c().await;
tracing::info!("ctrl-c received; shutting down");
})
.await?; .await?;
// Drain background tasks (crawler daemon) before exiting so Chromium // Drain background tasks (crawler daemon) before exiting so Chromium
@@ -30,3 +27,33 @@ async fn main() -> anyhow::Result<()> {
} }
Ok(()) 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");
}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "mangalord-frontend", "name": "mangalord-frontend",
"version": "0.34.0", "version": "0.35.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {