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
+14 -5
View File
@@ -218,11 +218,20 @@ impl HttpAdapter {
self
}
/// The WS idle-read timeout (WS-01): the read pump closes the
/// connection with a 1001 (GoingAway) close frame after this long
/// without an inbound WS message — bounding the demux stall a
/// dribbling (or silently-stalled) peer can pin. `None` disables
/// the knob (not recommended: the stall window is then unbounded).
/// The WS idle-read timeout (WS-01, WS-13 semantics): the read
/// pump closes the connection with a 1001 (GoingAway) close frame
/// after this long producing **no completed inbound chunk** — the
/// deadline resets on demux progress (complete chunks forwarded
/// into the byte stream), not on WS message arrival, so a
/// dribbling peer (slow message arrivals inside a declared chunk)
/// is bounded while productive-but-slow peers survive. This is an
/// intentional no-progress eviction line, not a transport-idle
/// bound: there is no WS ping/pong keepalive, and app-silence that
/// outlasts the window (a quiet subscription) is evicted by design
/// — see `websocket::byte_adapter`'s module doc. `None` disables
/// the knob (not recommended: the stall window is then unbounded;
/// long-lived silent subscriptions are the intended `None` case,
/// leaning on `WsSessions::abort` and the write-side caps).
///
/// Default: [`crate::websocket::DEFAULT_WS_IDLE_TIMEOUT`] (60 s).
pub fn with_ws_idle_timeout(mut self, idle_timeout: Option<Duration>) -> Self {