From c93f7387ff8a9ee28d753baf8d82f855d59f3379 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 29 Aug 2026 13:46:57 +0000 Subject: [PATCH] docs(websocket): detached task lifetime semantics (WS-10) --- src/websocket/upgrade.rs | 44 ++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/websocket/upgrade.rs b/src/websocket/upgrade.rs index b339dc1..3f77dd6 100644 --- a/src/websocket/upgrade.rs +++ b/src/websocket/upgrade.rs @@ -9,6 +9,32 @@ //! path). The `install_channel_zero` hook constructs channel 0's //! `CallConnection` (the identity rides the channels-layer //! `Connection`) and runs the shared `Dispatcher::run_loop_single_stream`. +//! +//! ## Detached task lifetime semantics (WS-10) +//! +//! Two task families spawned here are **detached by design** and +//! outlive the session task that spawned them: +//! +//! - the WS pump tasks (the `WsPumps` pair behind the byte adapter, +//! re-exported from `crate::websocket`), and +//! - the channel-0 dispatcher task spawned by `install_channel_zero` +//! per accepted channels connection. +//! +//! If the channels session task (`run_channels_session`) dies — +//! upgrade-time early return, an adapter fault, or its own task being +//! cancelled — these tasks keep running **self-healing**: each ends on +//! its own when its stream half closes (peer disconnect, peer close +//! frame, read error, or the local teardown arms — +//! `WsSessions::abort`, the idle-read timeout, `AsyncWrite::shutdown`). +//! They are never leaked unconditionally: the leak window is bounded +//! by peer behavior (a peer that holds the socket open keeps the pump +//! and dispatcher tasks alive with it) and additionally by the WS-01 +//! idle-read timeout when configured, and every session's pumps stay +//! force-evictable through the WS-08 registry +//! ([`WsSessions::abort`]) for the session's whole lifetime. The +//! dispatcher task specifically ends when channel 0's `BiStream` read +//! side hits EOF (its only exit condition), i.e. when the underlying +//! WS connection ends by any of the paths above. use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; @@ -35,9 +61,9 @@ use super::byte_adapter::{ /// halves close and the channels session unwinds). The handle for a /// session is removed when the session task finishes (self-removing /// guard), so the registry only holds live sessions. Assembly layers -/// share one instance via [`crate::server::state::RouterState`] (the -/// upgrade handler registers against it) or per-route request -/// extensions (an extension clone takes precedence). +/// share one instance via the adapter's `RouterState` (the upgrade +/// handler registers against it) or per-route request extensions (an +/// extension clone takes precedence). /// /// Un-registered (default) — the upgrade runs fine and simply keeps no /// eviction lever, matching the pre-WS-08 behavior. @@ -54,11 +80,11 @@ pub const DEFAULT_WS_MAX_SESSIONS: usize = 64; /// The upgrade handler's state slice: what it needs beyond the request /// itself. Axum lifts it via `FromRef` from either full router state — -/// the adapter's [`crate::server::state::RouterState`] (carrying the -/// shared [`WsSessions`] instance and the configured session cap) or a -/// bare `Arc` (custom upgrade routes / integration -/// tests get a handler-private registry and the default cap; eviction -/// still works in-crate, just not shared). +/// the adapter's `RouterState` (carrying the shared [`WsSessions`] +/// instance and the configured session cap) or a bare +/// `Arc` (custom upgrade routes / integration tests +/// get a handler-private registry and the default cap; eviction still +/// works in-crate, just not shared). #[derive(Clone)] pub struct SessionState { registry: Arc, @@ -122,7 +148,7 @@ impl axum::extract::FromRef for Arc { } /// `FromRef` chain: a bare `Arc` router state lifts -/// into the handler's [`SessionState`]; a full [`RouterState`] carries +/// into the handler's [`SessionState`]; a full `RouterState` carries /// the shared [`WsSessions`] instance and lifts through its own impl. impl axum::extract::FromRef> for SessionState { fn from_ref(registry: &Arc) -> Self {