fix(net): detect half-open control connection via independent keepalive #101
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/VA-101-keepalive-half-open-detection"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The control-channel keepalive in
idle_loopderived liveness entirely from read-silence: thetokio::time::timeout(idle_timeout, ...)was rebuilt inside theselect!on every iteration, so any inbound frame or any outboundoutbox_rxsend re-armed the window and the ping only fired after a fullidle_timeoutof silence in both directions. On a half-open socketself.ws.send(...)succeeds locally without a reply, so a session with periodic outbound traffic re-armed the window forever and never ran the health check, leaving the agent believing a dead connection was healthy for hours while the server had torn it down.Replace the read-silence probe with an independent
tokio::time::intervalarm that emitsMessage::Pingeverykeepalive_intervalregardless of other traffic. A newKeepalivetracker records the instant of the last inbound frame; only a received frame (Pong or any other inbound message) resets it, never an outbound send. When no inbound frame arrives withinkeepalive_timeout, the loop returnsNetError::Disconnected("keepalive timeout")and the existing reconnect/backoff loop takes over.idle_timeoutis retained only as an inbound-read backstop and no longer gates the ping.Add
keepaliveIntervalSecs(default 30s) andkeepaliveTimeoutSecs(default 90s) asConnectionSettingstunables, thread them throughConnectConfig, and log them with the other connection tunables at startup. Unit tests cover the tracker, including the outbound-traffic-masking case: a session pushing periodic outbound frames on a half-open socket still expires withinkeepalive_timeoutbecause outbound sends never reset the deadline.#VA-101
The control-channel keepalive in `idle_loop` derived liveness entirely from read-silence: the `tokio::time::timeout(idle_timeout, ...)` was rebuilt inside the `select!` on every iteration, so any inbound frame or any outbound `outbox_rx` send re-armed the window and the ping only fired after a full `idle_timeout` of silence in both directions. On a half-open socket `self.ws.send(...)` succeeds locally without a reply, so a session with periodic outbound traffic re-armed the window forever and never ran the health check, leaving the agent believing a dead connection was healthy for hours while the server had torn it down. Replace the read-silence probe with an independent `tokio::time::interval` arm that emits `Message::Ping` every `keepalive_interval` regardless of other traffic. A new `Keepalive` tracker records the instant of the last inbound frame; only a received frame (Pong or any other inbound message) resets it, never an outbound send. When no inbound frame arrives within `keepalive_timeout`, the loop returns `NetError::Disconnected("keepalive timeout")` and the existing reconnect/backoff loop takes over. `idle_timeout` is retained only as an inbound-read backstop and no longer gates the ping. Add `keepaliveIntervalSecs` (default 30s) and `keepaliveTimeoutSecs` (default 90s) as `ConnectionSettings` tunables, thread them through `ConnectConfig`, and log them with the other connection tunables at startup. Unit tests cover the tracker, including the outbound-traffic-masking case: a session pushing periodic outbound frames on a half-open socket still expires within `keepalive_timeout` because outbound sends never reset the deadline. #VA-101