Skip to content

fix(server): handle SIGTERM so docker stop is graceful - #122

Merged
plusky merged 1 commit into
mainfrom
fix/issue-114
Aug 18, 2026
Merged

fix(server): handle SIGTERM so docker stop is graceful#122
plusky merged 1 commit into
mainfrom
fix/issue-114

Conversation

@plusky

@plusky plusky commented Aug 18, 2026

Copy link
Copy Markdown
Owner

What

Install a SIGTERM handler next to SIGINT so a container running as PID 1 exits on docker stop / podman stop / Kubernetes instead of waiting out the runtime grace period and dying to SIGKILL (issue #114).

  • HTTP: shutdown_signal() feeds axum's graceful shutdown and then cancels the streamable-HTTP cancellation token, so a live MCP session cannot pin the listener.
  • stdio: the same waiter covers both serve (handshake; where an unused stdio container sits) and waiting. After the signal the process exit(0)s. rmcp reads stdin via spawn_blocking; that read cannot be cancelled while a client holds the pipe, and returning from main drops the runtime onto that blocking thread.
  • Tests spawn the shipped binary and require status.code() == Some(0) (an unhandled SIGTERM still kills a non-PID-1 child, but with code() == None). Stdio tests keep the child's stdin open so wait_with_output cannot EOF-unblock a missing process::exit.
  • README / compose.yaml / DESIGN.md updated. --init stays as optional defense in depth.

Why

PID 1 does not get SIGTERM's default terminate action. Only ctrl_c() (SIGINT) was installed, so docker stop measured 13s + exit 137. Kubernetes has no --init equivalent.

Invariants

No guard change. I1–I16 untouched. process::exit(0) does not skip persist-before-response (I15): call_tool / initialize await the sink write before any client-visible result. An in-flight call that is killed produces no response. I12: new log lines carry no key material.

Verification

  • cargo fmt --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo clippy -p bugwarden --features gen --all-targets -- -D warnings
  • cargo test --workspace --all-targets --locked
  • cargo deny check
  • Adversarial review (three parallel passes: PID-1 correctness, DESIGN/I-invariants, test mutations). Handshake return Ok(()) was a real miss: the first stdio test used wait_with_output, which drops stdin and unblocks the hang. Both arms now process::exit(0) and the handshake test holds stdin.
  • As PID 1 via unshare --user --pid --fork, SIGTERM now exits 0 in ~11ms (was 13s / 137).

Closes #114

As PID 1 the kernel ignores SIGTERM unless a handler is installed, so
tokio::signal::ctrl_c() alone made docker/podman/k8s stop wait out
the grace period and SIGKILL (issue #114). Listen for SIGTERM as well
as SIGINT.

HTTP takes the existing graceful-shutdown path and cancels the
streamable-HTTP token so a live session cannot pin the listener.

Stdio cannot drain that way: rmcp reads stdin via spawn_blocking, and
that read does not unblock while a client holds the pipe. Both the
handshake wait and waiting() therefore process::exit(0) after the
signal. Completed audit records are already on disk.

Verified: cargo fmt --check, clippy -D warnings, cargo test
--workspace --all-targets --locked, cargo deny check. As PID 1 via
unshare, SIGTERM now exits 0 in ~10ms (was 13s / 137).
@plusky plusky added the ai-assisted Authored or substantially written with an AI coding agent label Aug 18, 2026
@plusky
plusky merged commit b3c259b into main Aug 18, 2026
13 checks passed
@plusky
plusky deleted the fix/issue-114 branch August 18, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted Authored or substantially written with an AI coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

main.rs handles only SIGINT: as PID 1 a docker stop takes the 13s SIGKILL path

1 participant