Skip to content

Make server shutdown awaitable and report abnormal termination #657

Description

@leynos

Summary

Give WireframeServer an awaitable shutdown contract: an explicit stop that waits for the
accept loops to drain, a retained JoinHandle so abnormal termination is observed rather
than swallowed, and a typed error distinguishing a crash from a clean stop.

This is the server-side mirror of the pattern ADR 013 sections 9 and 10 adopt for the
client pool.

Problem

Today the only way to stop a WireframeServer from outside is to resolve the shutdown
future passed to run_with_shutdown, or to abort the task. #656 makes an aborted server
eventually release its listener, but the caller still cannot observe when that has
happened:

  • run_with_shutdown owns its CancellationToken and TaskTracker as locals
    (src/server/runtime.rs:157-158), so no caller can cancel and then drain;
  • awaiting the supervisor's JoinHandle after an abort yields
    Err(JoinError::cancelled), which says nothing about whether the accept loops have
    exited;
  • a supervisor panic is visible only if the caller happens to retain and inspect the
    JoinHandle;
  • there is no error that distinguishes "stopped cleanly" from "the server task died".

The practical consequence is that a caller that needs a hard guarantee — a test harness
reclaiming a port, a supervisor restarting a listener, an orchestrator rolling a deploy —
has to poll the socket and hope.

Prior art in this repository

ADR 013 section 9 ("Make shutdown awaitable") and section 10 ("Detect and report scheduler
failure") already settle this shape for WireframeClientPool:

  • close delivers a shutdown command, waits for acknowledgement and task completion, then
    releases resources;
  • shutdown delivery must be guaranteed non-blocking — reserved mailbox capacity, a
    dedicated channel, or a CancellationToken — so a saturated component cannot deadlock
    its own shutdown;
  • the task's JoinHandle is retained so a panic is observed rather than swallowed;
  • abnormal termination maps to a dedicated error variant, distinct from clean shutdown, so
    callers can programmatically tell a crash from a planned stop;
  • abnormal termination emits an error-level structured log event.

This issue applies the same five properties to the server. It should not invent a second
vocabulary: where ADR 013 names a concept, reuse the name.

Proposed shape

Exact API to be settled during design, but it must satisfy the ADR 013 properties. A
sketch:

/// Returned alongside the running server; cloneable control, single-owner state.
pub struct ServerShutdown { /* cancellation handle + drain acknowledgement */ }

impl ServerShutdown {
    /// Non-blocking. Requests that accept loops stop.
    pub fn stop(&self);
    /// Waits until every accept loop has exited and the listener is released.
    pub async fn drained(&self) -> Result<(), ServerError>;
}

with run_with_shutdown either accepting a caller-supplied CancellationToken or
returning the handle. Both satisfy ADR 011 R6, which explicitly retains "legitimate shared
handles" — a cloneable control handle over single-owner state is the R6 shape, and R5's
actor guidance applies if the drain state acquires coupled invariants.

Abnormal termination gets a dedicated ServerError variant, distinct from Bind and from
whatever #642 adds for application build and preparation failure.

Sequencing

This restructures run_with_shutdown, and so does #642. They should not race.

ADR disposition

ADR 013 governs the client pool, so it cannot be cited directly as authority for a server
change. This issue therefore needs either:

  • a new ADR recording the server runtime's shutdown-ownership contract as the symmetric
    application of ADR 013 sections 9 and 10; or
  • an agreed amendment generalizing those sections to both runtimes.

The next free ADR number is 014, ADRs 011 to 013 being taken by #653. Record the choice
before implementation begins.

Acceptance criteria

  • A caller can request shutdown and then await a guarantee that every accept loop has
    exited and the listener is released.
  • Shutdown delivery is non-blocking and cannot deadlock against a saturated server.
  • The server task's JoinHandle is retained so a panic is observed, not swallowed.
  • Abnormal termination maps to a dedicated ServerError variant, distinct from clean
    shutdown and from the Prepare the application before server readiness and share it across connection tasks #642 startup variants, preserving the source error.
  • Abnormal termination emits an error-level structured log event.
  • Repeated stop requests are safe and converge to one drain.
  • run() behaviour and graceful-drain semantics for in-flight connections are unchanged.
  • The ADR disposition above is recorded before merge.

Tests

  • Request shutdown, await the drain, then assert the address refuses connections
    immediately — with no polling and no sleep.
  • Request shutdown on a server with an in-flight connection and assert the existing
    graceful-drain semantics are preserved.
  • Panic inside the supervisor and assert the dedicated error variant plus the log event.
  • Issue several concurrent stop requests and assert one drain and one terminal outcome.
  • Assert shutdown delivery succeeds while the server is saturated.

Non-goals

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    concurrencyConcurrency, parallelism, and synchronization work, including races and deadlocks.enhancementNew feature or requestmediumRoadmap items to schedule within the current quarter. Clear scope, normal review cycles.testingTest coverage, test infrastructure, and verification tooling work.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions