docs(websocket): record WS-13 no-keepalive decision + progress semantics (WS-13)

Decides the WS-13 legitimate-silence question as option (b): 60s of no
chunk progress is an intentional eviction line even for silent
subscriptions; no WS ping/pong keepalive is added because a keepalive
can only rescue app-silence by re-arming the deadline, which reopens
the dribble hole the knob exists to seal. Documented in the byte_adapter
module doc, on DEFAULT_WS_IDLE_TIMEOUT, on the unchanged
HttpAdapter::with_ws_idle_timeout knob, and in websocket.md (new
'Idle-read timeout' section, including the FWD-15 SSE-keepalive
layering note). Deployment posture for long-lived silent sessions:
with_ws_idle_timeout(None) + WsSessions abort + write-side caps.
This commit is contained in:
2026-08-30 12:09:30 +00:00
parent f834835b8b
commit 3f1d5913e7
3 changed files with 103 additions and 18 deletions
+44 -1
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-08-27
last_updated: 2026-08-30
---
# WebSocket — the Browser Bidirectional Path (Channels over WS)
@@ -176,6 +176,49 @@ The adapter is shared with the `from_wss` consumer path
([ADR-070](decisions/070-from-wss-consumer-adapter.md)) — one
implementation, both directions.
#### Idle-read timeout: progress semantics + the no-keepalive decision (WS-01, WS-13)
The WS pumps carry an **idle-read eviction knob**
(`HttpAdapter::with_ws_idle_timeout`, default
`DEFAULT_WS_IDLE_TIMEOUT` = 60 s, disable with `None`). Its semantics,
decided in review-002 (WS-13), are **progress-based, deliberately
strict**:
- The deadline resets on **demux progress** — bytes actually forwarded
into the byte stream that *complete* an inbound chunk (a full
8-byte header + its declared payload). WS message arrival resets
nothing.
- Therefore the dribble stall (declare a chunk, deliver its payload a
byte per message) hits the deadline and is evicted with a `1001`
(GoingAway) close — even though messages keep arriving — while a
peer delivering complete chunks, however slowly per message, re-arms
the window with each chunk and survives.
- **There is no WS ping/pong keepalive, on purpose.** A keepalive
rescues app-silence only by re-arming the deadline — and a pong is
indistinguishable from the dribble's almost-invisible arrivals, so
adding one would reopen the stall it exists to seal. The recorded
decision (module doc of `src/websocket/byte_adapter.rs`, option (b)
of the two WS-13 alternatives): **60 s of no chunk progress is an
intentional eviction line, even for a silent subscription.**
- A deployment running long-lived silent-but-alive sessions (quiet
subscriptions that outlast the window) disables the knob with
`with_ws_idle_timeout(None)` and leans on the remaining levers:
the session registry's forced-eviction
([`WsSessions::abort`](#connection-local-overlay)), the
inbound caps (WS-06), and the write-side caps (WS-04/05/06). This is
the same deployment posture as `from_wss`'s drop monitor
([ADR-070](decisions/070-from-wss-consumer-adapter.md)): the idle
knob bounds *demux parking*, not app liveness.
- Layered note (the WS-13/FWD-15 interaction recorded here): the
*HTTP* SSE path (`/subscribe`) sends server-side keep-alive comment
frames every 15 s — see
[http-server.md](http-server.md)) — because its idle enemy is
LB/proxy timeouts, and its keep-alive does not reset any
progress deadline, it cannot reopen the WS-13 hole. The two live at
different layers: SSE keep-alive fights transport fires; the WS idle
knob bounds demux parking. Both documented in the same pass per
review-002 Unit-2 sequencing.
### Dispatch: channel 0 = the shared `Dispatcher`, unchanged
Channel 0's session is the alknet design's native session, verbatim: