Skip to content

fix(desktop): websocket keepalive, reconnect backoff and persistent logs - #177

Draft
henricook wants to merge 3 commits into
Sathvik-Rao:mainfrom
henricook:fix/ws-resilience
Draft

henricook wants to merge 3 commits into
Sathvik-Rao:mainfrom
henricook:fix/ws-resilience

Conversation

@henricook

@henricook henricook commented Sep 15, 2026

Copy link
Copy Markdown

Clipboard sync between my Linux and macOS clients regularly stops silently; the server shows 96 of 101 websocket sessions ending in "transport error" with zero clean disconnects, while the client keeps believing it's connected.

The client never sends anything proactively (STOMP client heartbeats are 0), so a half-open socket after sleep or a Wi-Fi blip is invisible to it, a failed send doesn't trigger a reconnect, and the one reconnect it attempts has a 3s timeout and no retry.

This PR:

  • Enables WebSocket keepalive so dead sockets are detected within ~40s: run_forever(ping_interval=30, ping_timeout=10).
  • Replaces the single-shot sleep(10); connect() in _on_close with a reconnect loop that backs off 10s, 20s, 40s then 60s until connected or explicitly disconnected, guarded by a non-blocking Lock so a dying zombie socket can't start a second loop.
  • disconnect() sets a threading.Event that breaks a sleeping backoff wait immediately, and connect() returns early when disconnected so a pending auto-reconnect can't undo a user's Disconnect.
  • A send() that fails with a WebSocketException/OSError now closes the socket so the existing on_close path reconnects, instead of just logging "Failed to send data" and staying stuck; a connect timeout detaches its timed-out attempt so it can't drive a reconnect.
  • Switches the log to an appending RotatingFileHandler (1MB x 3) and logs the connection lifecycle at INFO. It was truncated on every launch and logged nothing about lost/reconnected, so none of this was diagnosable after the fact.

The client never sends anything proactively (STOMP client heartbeats are
0), so a half-open socket after sleep or a network blip was invisible to
it: sends failed without triggering a reconnect and the single reconnect
attempt had a 3s timeout and no retry.

- run_forever(ping_interval=30, ping_timeout=10) so dead sockets are
  detected within ~40s
- replace the one-shot sleep/connect in _on_close with a backoff loop
  (10s to 60s) guarded by a lock; disconnect() wakes it via an Event
- a send() failing with WebSocketException/OSError closes the socket so
  the on_close path reconnects; connect timeout closes its leaked thread
- RotatingFileHandler in append mode and INFO-level lifecycle logging so
  the next failure is diagnosable
websocket-client already logs "Websocket connected" at INFO on its own thread when the socket opens; the app's line 250ms later used the identical phrase, which reads as a double connect in the log. It isn't: one connection, one subscription.
The connect-timeout path called ws.close(), which sets self.sock=None while that attempt's run_forever thread is still live; the thread then dereferences self.sock.sock and logs a spurious "'NoneType' object has no attribute 'sock'". Detaching on_close is enough to stop the timed-out attempt from driving a reconnect.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant