Skip to content

OS signal handlers are installed only when wait_for_signal is first polled (after client + health-server init), so a SIGTERM during startup exits ungracefully — contradicting the comment at app.rs:544 #324

Description

@erskingardner

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-50ShutdownHandler::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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions