fix(websocket): configurable WS read idle timeout (WS-01)

This commit is contained in:
2026-08-29 13:45:54 +00:00
parent fa73684ebe
commit 25975ac2a8
6 changed files with 241 additions and 15 deletions
+17 -4
View File
@@ -25,7 +25,7 @@ use axum::response::{IntoResponse, Response};
use parking_lot::Mutex;
use super::byte_adapter::{
split_ws_to_bytes, WsPumps, INBOUND_WS_FRAME_CAP, INBOUND_WS_MESSAGE_CAP,
split_ws_to_bytes_idle, WsPumps, INBOUND_WS_FRAME_CAP, INBOUND_WS_MESSAGE_CAP,
};
/// Registry of live WS session pump handles (WS-08). The upgrade
@@ -66,6 +66,8 @@ pub struct SessionState {
/// Session cap (WS-09): acquired post-auth, pre-upgrade; a caller
/// over the cap is rejected with 503.
session_slots: Arc<tokio::sync::Semaphore>,
/// Idle-read timeout (WS-01): `None` disables the knob.
idle_timeout: Option<std::time::Duration>,
}
impl SessionState {
@@ -78,6 +80,7 @@ impl SessionState {
registry: Arc::clone(registry),
sessions: Arc::new(WsSessions::new()),
session_slots: Arc::new(tokio::sync::Semaphore::new(DEFAULT_WS_MAX_SESSIONS)),
idle_timeout: Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT),
}
}
@@ -89,11 +92,13 @@ impl SessionState {
registry: Arc<OperationRegistry>,
sessions: Arc<WsSessions>,
session_slots: Arc<tokio::sync::Semaphore>,
idle_timeout: Option<std::time::Duration>,
) -> Self {
Self {
registry,
sessions,
session_slots,
idle_timeout,
}
}
@@ -104,6 +109,10 @@ impl SessionState {
pub(crate) fn sessions(&self) -> &Arc<WsSessions> {
&self.sessions
}
pub(crate) fn idle_timeout(&self) -> Option<std::time::Duration> {
self.idle_timeout
}
}
impl axum::extract::FromRef<SessionState> for Arc<OperationRegistry> {
@@ -157,15 +166,18 @@ impl WsSessions {
/// (identity attached) → `ChannelsAdapter::handle`. `policy` gates
/// data-channel opens (ADR-041). When `sessions` is `Some`, the
/// session's pump handle is registered for the session's lifetime —
/// the WS-08 eviction lever ([`WsSessions::abort`]).
/// the WS-08 eviction lever ([`WsSessions::abort`]). `idle_timeout`
/// bounds the read stall (WS-01): `None` disables the knob, `Some(d)`
/// closes the read with 1001 after `d` without an inbound WS message.
pub async fn run_channels_session(
socket: axum::extract::ws::WebSocket,
registry: Arc<OperationRegistry>,
identity: Identity,
policy: Arc<dyn ChannelLifecyclePolicy>,
sessions: Option<WsSessions>,
idle_timeout: Option<std::time::Duration>,
) {
let (byte_stream, pumps) = split_ws_to_bytes(socket);
let (byte_stream, pumps) = split_ws_to_bytes_idle(socket, idle_timeout);
let pumps = Arc::new(pumps);
let _guard = sessions.map(|sessions| {
@@ -316,12 +328,13 @@ pub async fn ws_upgrade_handler(
.map(|axum::Extension(p)| p.0)
.unwrap_or_else(|| Arc::new(NoCap));
let registry = Arc::clone(state.registry());
let idle_timeout = state.idle_timeout();
ws_upgrade
.max_frame_size(INBOUND_WS_FRAME_CAP)
.max_message_size(INBOUND_WS_MESSAGE_CAP)
.on_upgrade(move |socket| async move {
let _permit = permit;
run_channels_session(socket, registry, identity, policy, sessions).await
run_channels_session(socket, registry, identity, policy, sessions, idle_timeout).await
})
}