Low-probability, low-impact edge case: no notification data is in flight during the pre-connect startup window, so the concrete harm is an ungraceful exit code (143/130) and lost clean-shutdown logging rather than data loss. Filed mainly because the in-code comment explicitly claims this window is covered when it is not.
Summary
ShutdownHandler::new() does not install any OS signal handler — it only creates a watch::channel. Tokio registers the SIGTERM/SIGINT handlers lazily, the first time signal::unix::signal(SIGTERM) / signal::ctrl_c() are polled, which only happens when wait_for_signal_or_trigger() is first awaited at app.rs:583. Everything constructed before that poll runs under the default signal disposition (immediate process termination).
Location
src/app.rs:544-548 — comment claims "Initialize shutdown handler before connecting so a SIGTERM/SIGINT during startup exits promptly."
src/shutdown.rs:47-50 — ShutdownHandler::new() only builds watch::channel(false); installs nothing.
src/shutdown.rs:70-76, 152-167 — signal handlers are created lazily inside wait_for_signal() → wait_for_sigterm_signal() / wait_for_ctrl_c_signal().
Root cause
The construction awaits that precede the first wait_for_signal_or_trigger() poll — ApnsClient::with_metrics(), FcmClient::with_metrics(), RelayClient::with_metrics_and_server_config(), and health_server.bind() — all run before any handler is registered. A SIGTERM/SIGINT delivered during that window hits the kernel default disposition (terminate), so the intended graceful path (staged_teardown, clean exit 0, shutdown logging) never runs.
Trigger scenario
An orchestrator sends SIGTERM while the process is still constructing push/relay clients (e.g. a slow FCM OAuth mint or slow relay-client construction). The process is killed immediately (exit 143) instead of the graceful exit the comment promises.
Suggested fix
Either install the signal handlers eagerly in ShutdownHandler::new() (spawn a task that owns the signal::unix::signal(...) streams and forwards to the watch channel), or correct the comment to state that the pre-poll window is not covered.
Why this is not a duplicate
#238 covers the post-connect subscribe/publish window; #311 covers select-bias misclassification of signal vs internal trigger; #104 was about a .expect() on install and second-signal force-quit. None address the pre-connect init window, the lazy-install timing, or the misleading comment.
Low-probability, low-impact edge case: no notification data is in flight during the pre-connect startup window, so the concrete harm is an ungraceful exit code (143/130) and lost clean-shutdown logging rather than data loss. Filed mainly because the in-code comment explicitly claims this window is covered when it is not.
Summary
ShutdownHandler::new()does not install any OS signal handler — it only creates awatch::channel. Tokio registers the SIGTERM/SIGINT handlers lazily, the first timesignal::unix::signal(SIGTERM)/signal::ctrl_c()are polled, which only happens whenwait_for_signal_or_trigger()is first awaited atapp.rs:583. Everything constructed before that poll runs under the default signal disposition (immediate process termination).Location
src/app.rs:544-548— comment claims "Initialize shutdown handler before connecting so a SIGTERM/SIGINT during startup exits promptly."src/shutdown.rs:47-50—ShutdownHandler::new()only buildswatch::channel(false); installs nothing.src/shutdown.rs:70-76, 152-167— signal handlers are created lazily insidewait_for_signal()→wait_for_sigterm_signal()/wait_for_ctrl_c_signal().Root cause
The construction awaits that precede the first
wait_for_signal_or_trigger()poll —ApnsClient::with_metrics(),FcmClient::with_metrics(),RelayClient::with_metrics_and_server_config(), andhealth_server.bind()— all run before any handler is registered. A SIGTERM/SIGINT delivered during that window hits the kernel default disposition (terminate), so the intended graceful path (staged_teardown, clean exit 0, shutdown logging) never runs.Trigger scenario
An orchestrator sends SIGTERM while the process is still constructing push/relay clients (e.g. a slow FCM OAuth mint or slow relay-client construction). The process is killed immediately (exit 143) instead of the graceful exit the comment promises.
Suggested fix
Either install the signal handlers eagerly in
ShutdownHandler::new()(spawn a task that owns thesignal::unix::signal(...)streams and forwards to the watch channel), or correct the comment to state that the pre-poll window is not covered.Why this is not a duplicate
#238 covers the post-connect subscribe/publish window; #311 covers select-bias misclassification of signal vs internal trigger; #104 was about a
.expect()on install and second-signal force-quit. None address the pre-connect init window, the lazy-install timing, or the misleading comment.