3.9 KiB
0011. A dedicated authority stream for server-pushed state
- Status: Accepted
- Date: 2026-07-31
Context
The simulation is server-authoritative (ADR-0004), so a category of traffic exists that the client never asks for: state the server pushes on its own cadence. Simulation snapshots are the eventual bulk of it; periodic server diagnostics were the first concrete instance.
Two existing streams could have absorbed that traffic, and both are a poor fit:
- The control stream (stream 0) carries the handshake and disconnect. It is request/response and effectively one-shot per connection. Adding a recurring push to it mixes lifecycle negotiation with steady-state traffic, and a burst of pushed state would sit in the same ordered stream as a disconnect notice that should arrive promptly.
- The chunk stream (stream 3) is bidirectional and carries large frames (a 1 MiB cap). Head-of-line blocking is per-stream in QUIC, so a small, time-sensitive state push queued behind a multi-hundred-kilobyte chunk delivery would inherit that chunk's latency. That is precisely the coupling separate streams exist to avoid.
Stream assignment is a wire contract shared by both peers: StreamLayout fixes the ids, and changing one is a protocol break. The decision is therefore made once, ahead of the snapshot work that will depend on it, rather than discovered later.
Decision
Server-pushed authoritative state travels on its own unidirectional-in-practice stream, StreamLayout::authority (stream 2), carrying shared::protocol::authority::AuthorityMessage.
- The stream is server => client only. Nothing the client sends belongs on it; client input gets its own stream when it lands.
AuthorityMessageis an enum, so new pushed payloads are added as variants rather than as new streams.ServerStatsis the first variant; simulation snapshots will join it.- Frames use the existing length-prefixed
postcardcodec withMAX_AUTHORITY_FRAME_LEN(64 KiB), well above a fixed-shape diagnostics record, and set to bound what a malformed length prefix can make a peer allocate. - The async/sync bridge follows the pattern established for chunk delivery (ADR-0010): the simulation loop holds an
AuthoritySink, a synchronous non-blocking handle wrapping atokioMPSC sender, so neitherservernorclientnames atokiotype. - A send on a departed connection is logged at debug and dropped. The simulation loop cannot act on a disconnected client, and pushed state is by definition unsolicited, so failure to deliver it is not an error condition for the sender.
Consequences
- Latency of pushed state is independent of chunk delivery volume. A client pulling its initial region at full rate still receives snapshots on time.
- Adding a pushed payload is one enum variant, with no new stream to negotiate on either peer and no
StreamLayoutchange. - The stream layout now commits four ids (control 0, reserved 1, authority 2, chunk LOD0 3). Reassigning any of them is a
PROTOCOL_VERSIONbump. - Loss and ordering semantics are per-stream: authority messages are ordered relative to each other and unordered relative to chunk deliveries. Anything requiring a snapshot to be interpreted against a specific delivered chunk must carry its own correlation (a tick number or chunk version), rather than relying on arrival order across streams.
- The sink is fire-and-forget and unbounded. That is appropriate for a low-rate diagnostics push, but snapshots at tick rate will need a bound and a drop policy: a slow client must not be allowed to grow the server's queue without limit. This is the known follow-up before snapshots ship.
- Diagnostics being on the authority stream rather than beside it means they are subject to the same server-authoritative framing: the client reports what the server measured, never what it inferred. See
docs/diagnostics.md.