Summary
When the server's socket dies silently (TCP connection blackholed — no FIN, no RST, no WebSocket close frame), the client detects nothing. The connection stays admitted as current and is_connected() stays true indefinitely, so the client sits in a half-open state firing time-sync bursts that all time out and repeat forever. There is no liveness detection at any layer, and no path ever tears down the stale connection.
Expected behavior
A connection that stops producing any traffic (or stops acknowledging time messages) for a bounded period should be treated as dead: dropped and reconnected. The client should not block/hang indefinitely on a silent peer.
Observed behavior
In the sendspin-client this has been reported as an issue (TermeHansen/sendspin-client#14), with a log output similar to:
W sendspin.time_burst: Time message 8/8 timed out
W sendspin.time_burst: Time message 1/8 timed out (+20s)
W sendspin.time_burst: Time message 2/8 timed out ... repeats indefinitely
Burst completes (8/8), restarts after the inter-burst interval, and never gives up. No "connection lost" log is ever emitted. Once the kernel send buffer fills on the dead socket, the time-message send blocks the main loop for minutes at a time — the client hangs between bursts.
Root cause
Liveness is never enforced on an established connection:
- No application-level heartbeat anywhere. No ping/keepalive is configured on the host or ESP transport (sendspin-cpp never calls IXWebSocket's ping/closeOnPongTimeout or any other keepalive). The server dying silently therefore produces no Close event, so SendspinClientConnection::connected_ stays true and the connection stays managed.
- The existing periodic traffic — the time burst — is not used as a liveness signal. SendspinTimeBurst::loop() increments burst_index_ and restarts at 8/8 with no give-up (src/time_burst.cpp:57), regardless of whether the peer ever answers.
- ConnectionManager::loop() only reaps nursery (pre-handshake) connections via NURSERY_ESTABLISH_TIMEOUT_US; there is no equivalent timeout for an admitted/established connection, so nothing ever drops it. And once the dead socket's TX window fills, time_burst_'s send_time_message() → ws_->send() blocks the main loop (client.loop()), stalling the whole client.
Summary
When the server's socket dies silently (TCP connection blackholed — no FIN, no RST, no WebSocket close frame), the client detects nothing. The connection stays admitted as current and is_connected() stays true indefinitely, so the client sits in a half-open state firing time-sync bursts that all time out and repeat forever. There is no liveness detection at any layer, and no path ever tears down the stale connection.
Expected behavior
A connection that stops producing any traffic (or stops acknowledging time messages) for a bounded period should be treated as dead: dropped and reconnected. The client should not block/hang indefinitely on a silent peer.
Observed behavior
In the sendspin-client this has been reported as an issue (TermeHansen/sendspin-client#14), with a log output similar to:
Root cause
Liveness is never enforced on an established connection: