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
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
Summary
Give
WireframeServeran awaitable shutdown contract: an explicit stop that waits for theaccept loops to drain, a retained
JoinHandleso abnormal termination is observed ratherthan 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
WireframeServerfrom outside is to resolve the shutdownfuture passed to
run_with_shutdown, or to abort the task. #656 makes an aborted servereventually release its listener, but the caller still cannot observe when that has
happened:
run_with_shutdownowns itsCancellationTokenandTaskTrackeras locals(
src/server/runtime.rs:157-158), so no caller can cancel and then drain;JoinHandleafter an abort yieldsErr(JoinError::cancelled), which says nothing about whether the accept loops haveexited;
JoinHandle;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:closedelivers a shutdown command, waits for acknowledgement and task completion, thenreleases resources;
dedicated channel, or a
CancellationToken— so a saturated component cannot deadlockits own shutdown;
JoinHandleis retained so a panic is observed rather than swallowed;callers can programmatically tell a crash from a planned stop;
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:
with
run_with_shutdowneither accepting a caller-suppliedCancellationTokenorreturning 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
ServerErrorvariant, distinct fromBindand fromwhatever #642 adds for application build and preparation failure.
Sequencing
This restructures
run_with_shutdown, and so does #642. They should not race.guarantee builds on the cancellation it installs.
startup error model that this issue's termination error must sit alongside.
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:
application of ADR 013 sections 9 and 10; or
The next free ADR number is 014, ADRs 011 to 013 being taken by #653. Record the choice
before implementation begins.
Acceptance criteria
exited and the listener is released.
JoinHandleis retained so a panic is observed, not swallowed.ServerErrorvariant, distinct from cleanshutdown and from the Prepare the application before server readiness and share it across connection tasks #642 startup variants, preserving the source error.
run()behaviour and graceful-drain semantics for in-flight connections are unchanged.Tests
immediately — with no polling and no sleep.
graceful-drain semantics are preserved.
Non-goals
is out of scope.
wireframe_testinglifecycle harness, which consumes this.References
src/server/runtime.rs,src/server/error.rs