feat(shutdown): wait for background tasks before closing the pool - #1209
Open
bilhokista wants to merge 3 commits into
Open
feat(shutdown): wait for background tasks before closing the pool#1209bilhokista wants to merge 3 commits into
bilhokista wants to merge 3 commits into
Conversation
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1126.
Requirement 1 was already done
main.rsalready hasshutdown_signal()handling bothsignal::ctrl_c()and Unix SIGTERM, wired intoaxum::serve(...).with_graceful_shutdown(...). I did not touch it.Requirement 2 was missing, and there was a bug behind it
The grace window did not exist, and the shutdown sequence had a concrete flaw:
Signalling and waiting are not the same thing.
drop(shutdown_tx)wakes the watchdog and the webhook dispatcher, but nothing waits for them, sodb_pool.close()runs while a task may be mid-transaction — exactly the "in-flight database transactions" the issue names.It could not have waited even if it wanted to: both
InactivityWatchdogService::startandWebhookDispatcherService::startdiscarded theJoinHandlefromtokio::spawn. They now return it,maincollects all three tasks (the metrics loop included, under its feature flag), andawait_background_tasksjoins them under a 15-second timeout before the pool closes.Three decisions worth review
The signal is now sent explicitly.
drop(shutdown_tx)happens to wake receivers, but it says nothing about intent and stops working the moment any task holds a sender clone.shutdown_tx.send(true)says what it means. The result is deliberately discarded with a comment, becausemainstill holds a receiver so the send cannot fail — I would rather note that than write an error branch that can never run.The grace window is bounded, not infinite. Kubernetes and Docker both send SIGKILL 30 seconds after SIGTERM by default, so waiting forever converts a clean exit into a killed one. Fifteen seconds leaves room for an in-flight transaction and still lands well inside that budget. There is a test asserting the constant stays under 30s.
A panicking task does not abandon the others. Each handle is awaited individually and a
JoinErroris logged rather than propagated, so one bad task cannot cause the rest to be dropped mid-work. Tested.On timeout the pool closes anyway, with a warning — the alternative is hanging past the orchestrator's patience and being killed, which is strictly worse. The warning matters: it is the trace to look for if a deploy later shows odd data.
Verification
This one I could genuinely execute. I lifted
await_background_tasksandSHUTDOWN_GRACEbyte-for-byte into a scratch crate with real tokio and ran them withcargo test. All 5 pass:Honest note: what I could not run is the full
cargo testfor the real crate (it needs the whole dependency graph and sqlx's database or offline metadata), so the twostart()signature changes and the wiring inmainare unverified beyond review. They are mechanical — adding a return type and removing a trailing semicolon so the spawn becomes the return expression — but CI is the check.🤖 Generated with Claude Code
https://claude.ai/code/session_01CrfEY1tvXrbeMDAUzxfuk7