docs(websocket): detached task lifetime semantics (WS-10)

This commit is contained in:
2026-08-29 13:46:57 +00:00
parent 25975ac2a8
commit c93f7387ff
+35 -9
View File
@@ -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<OperationRegistry>` (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<OperationRegistry>` (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<OperationRegistry>,
@@ -122,7 +148,7 @@ impl axum::extract::FromRef<SessionState> for Arc<OperationRegistry> {
}
/// `FromRef` chain: a bare `Arc<OperationRegistry>` 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<Arc<OperationRegistry>> for SessionState {
fn from_ref(registry: &Arc<OperationRegistry>) -> Self {