Fix WSConnection reconnect stall: don't block the event loop during the reconnect delay#252
Fix WSConnection reconnect stall: don't block the event loop during the reconnect delay#252kojira wants to merge 2 commits into
Conversation
…he reconnect delay WSConnection::doConnect() slept the hub thread with std::this_thread::sleep_for() for the reconnect delay. That thread also runs the uv event loop, so the sleep stalls the loop's cached clock by `delay` ms. The subsequent hub.connect() then arms its connection-timeout timer against the stale clock, so the timer is already expired and fires immediately on the next loop tick — tearing the freshly-connected socket down before the WS handshake completes. The first connect uses doConnect(0) (no sleep) so it succeeds, but every reconnect after a disconnect wedges permanently: the client keeps retrying and failing forever (each attempt: "Attempting to connect" -> "Websocket connection error"), never exiting, so a supervisor never restarts it. This silently stalls stream/sync/router/mesh whenever a connection drops. Fix: defer the reconnect with a non-blocking uS::Timer instead of sleeping, so the loop clock stays accurate and the connection-timeout works as intended. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Background / how this was hit in production (multi-day silent ingest stall): #253 |
…he reconnect delay doConnect() slept the hub thread with std::this_thread::sleep_for() for the reconnect delay. That thread also runs the uv event loop, so the sleep stalls the loop's cached clock; the subsequent hub.connect() then arms its connection-timeout against the stale clock, so it fires immediately and tears down the freshly-connected socket before the WS handshake completes. The first connect (no delay) works, but every reconnect after a disconnect wedges forever. Defer the reconnect via a non-blocking uS::Timer instead of sleeping the loop. Submitted upstream as hoytech#252. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Read through the full One gap I noticed: To be clear about severity: I checked Didn't build/run this (no easy way to reproduce the exact reconnect-timing race here), so take the above as code-reading rather than a confirmed repro — but the gap seems real from tracing through the code. |
|
Thank you for this! It looks correct. I will do a bit more testing and then merge. |
|
Sounds good, thanks for taking a look — glad the diagnosis lined up. |
Addresses review feedback: the pending reconnect uS::Timer created by scheduleReconnect() was not tracked, so terminate() (run on shutdown via close()) had no handle on it. A pending timer keeps the uv loop's handle count above zero, so a close() racing with a pending reconnect delayed hub.run()'s return by up to reconnectDelayMilliseconds before the process could exit. Store it in a reconnectTimer member; close+null it in terminate() alongside hubGroup/hubTrigger/currWs; null it when the timer fires (before connectNow(), so a synchronous connect failure can re-arm); guard scheduleReconnect() against stacking a second pending timer; and include it in the destroyed-before-close warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch, @Gooh456 — you're right. The pending reconnect |
Follow-up to the reconnect-stall fix: track the pending reconnect uS::Timer in a member so terminate() (run on shutdown) can cancel it, guard scheduleReconnect() against stacking, null it when it fires, and include it in the destroyed-before-close warning. Matches hoytech#252 (commit 5e1376f). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
That's a clean fix, thanks for turning it around so quickly — makes sense to fold the timer into the same terminate() cleanup as the other handles. |
Summary
strfry streamandstrfry sync— the clients built onWSConnection— cannot reconnect after their first disconnect. The client keeps retrying and failing forever (Attempting to connect→Websocket connection errorevery 5s) even when the remote is perfectly reachable, and never recovers without a process restart. Since the process never exits, a supervisor (systemd/launchdRestart=) never restarts it — so a brief blip becomes an indefinite stall. (strfry routeris not affected: it has its own cron-scheduled reconnect that does not block the loop.)Root cause
WSConnection::doConnect()implemented the reconnect delay withstd::this_thread::sleep_for():That thread also runs the uWS/uv event loop, so the sleep blocks the loop and stalls its cached clock by
delayms (default 5000). The followinghub.connect(..., 5000, ...)arms a 5000 ms connection-timeout timer whose deadline is computed against the now-stale clock — already expired in real time — so it fires on the very next loop tick andonEnds the freshly-TCP-connected socket before the WebSocket handshake completes.The first connection uses
doConnect(0)(no sleep → fresh clock) so it works; every reconnect (doConnect(reconnectDelayMilliseconds)) wedges.Fix
Defer the reconnect with a non-blocking
uS::Timeron the hub's event loop instead of sleeping the loop thread, so the loop clock stays accurate and the connection-timeout behaves as intended.The same principle is why
cmd_router.cppdoesn't have this bug: it never sleeps the loop thread — its reconnect delay comes from a separate cron timer that just posts an event to the loop (viahubTrigger), and the actualhub.connect()runs on the loop thread. This patch doesn't copy the router's cron/inbox machinery; a one-shotuS::Timeron the loop is enough forWSConnection.Reproduce (deterministic)
Connected.Attempting to connect/Websocket connection errorevery 5s forever and never reconnects, even though the relay is back and a freshly-started client connects to it instantly.After this patch: the client reconnects on its next attempt.
Extra confirmation
Lowering
reconnectDelayMillisecondsbelow the 5000 ms connection-timeout (e.g. 250) also makes reconnect succeed with the old code — confirming the delay-vs-timeout clock interaction is the cause.