synvael/docs/adr/0011-authority-stream-for-server-pushed-state.md

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.
  • AuthorityMessage is an enum, so new pushed payloads are added as variants rather than as new streams. ServerStats is the first variant; simulation snapshots will join it.
  • Frames use the existing length-prefixed postcard codec with MAX_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 a tokio MPSC sender, so neither server nor client names a tokio type.
  • 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 StreamLayout change.
  • The stream layout now commits four ids (control 0, reserved 1, authority 2, chunk LOD0 3). Reassigning any of them is a PROTOCOL_VERSION bump.
  • 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.