From e5126c913d8af2201852745ae7107f41c973ce4d Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Sun, 8 Feb 2026 15:36:04 -0600 Subject: [PATCH] fix: SSE reconnect with exponential backoff and terminal state Replace the infinite fixed-delay retry loop with exponential backoff (1s, 2s, 4s, ... capped at 60s) and a max retry limit of 10. Changes to SiloEventListener: - Expand connection_status signal to (status, retry_count, error_message) - Add exponential backoff: min(BASE_DELAY * 2^retries, MAX_DELAY) - Add terminal "gave_up" state after MAX_RETRIES exhausted - Capture and forward error messages from failed connection attempts Changes to SiloAuthDockWidget._on_sse_status: - Show retry count: "Reconnecting (3/10)..." - Show "Disconnected" (red) on gave_up state - Log each attempt to FreeCAD console (PrintWarning/PrintError) - Set tooltip with last error message on the status label Closes #2 --- freecad/silo_commands.py | 51 +++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/freecad/silo_commands.py b/freecad/silo_commands.py index aad817c..649b9d2 100644 --- a/freecad/silo_commands.py +++ b/freecad/silo_commands.py @@ -2365,13 +2365,13 @@ class SiloEventListener(QtCore.QThread): item_updated = QtCore.Signal(str) # part_number revision_created = QtCore.Signal(str, int) # part_number, revision connection_status = QtCore.Signal( - str - ) # "connected" / "disconnected" / "unsupported" + str, int, str + ) # (status, retry_count, error_message) server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded" - _MAX_FAST_RETRIES = 3 - _FAST_RETRY_SECS = 5 - _SLOW_RETRY_SECS = 30 + _MAX_RETRIES = 10 + _BASE_DELAY = 1 # seconds, doubles each retry + _MAX_DELAY = 60 # seconds, backoff cap def __init__(self, parent=None): super().__init__(parent) @@ -2394,6 +2394,7 @@ class SiloEventListener(QtCore.QThread): def run(self): retries = 0 + last_error = "" while not self._stop_flag: try: self._listen() @@ -2401,21 +2402,24 @@ class SiloEventListener(QtCore.QThread): if self._stop_flag: return retries += 1 + last_error = "connection closed" except _SSEUnsupported: - self.connection_status.emit("unsupported") + self.connection_status.emit("unsupported", 0, "") return - except Exception: + except Exception as exc: retries += 1 + last_error = str(exc) or "unknown error" - self.connection_status.emit("disconnected") + if retries > self._MAX_RETRIES: + self.connection_status.emit("gave_up", retries - 1, last_error) + return - if retries <= self._MAX_FAST_RETRIES: - delay = self._FAST_RETRY_SECS - else: - delay = self._SLOW_RETRY_SECS + self.connection_status.emit("disconnected", retries, last_error) + + delay = min(self._BASE_DELAY * (2 ** (retries - 1)), self._MAX_DELAY) # Interruptible sleep - for _ in range(delay): + for _ in range(int(delay)): if self._stop_flag: return self.msleep(1000) @@ -2439,7 +2443,7 @@ class SiloEventListener(QtCore.QThread): except urllib.error.URLError: raise - self.connection_status.emit("connected") + self.connection_status.emit("connected", 0, "") event_type = "" data_buf = "" @@ -2705,13 +2709,28 @@ class SiloAuthDockWidget: self._event_listener.stop() self._sse_label.setText("") - def _on_sse_status(self, status): + def _on_sse_status(self, status, retry, error): if status == "connected": self._sse_label.setText("Listening") self._sse_label.setStyleSheet("font-size: 11px; color: #4CAF50;") + self._sse_label.setToolTip("") + FreeCAD.Console.PrintMessage("Silo: SSE connected\n") elif status == "disconnected": - self._sse_label.setText("Reconnecting...") + self._sse_label.setText( + f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})..." + ) self._sse_label.setStyleSheet("font-size: 11px; color: #FF9800;") + self._sse_label.setToolTip(error or "Connection lost") + FreeCAD.Console.PrintWarning( + f"Silo: SSE reconnecting ({retry}/{SiloEventListener._MAX_RETRIES}): {error}\n" + ) + elif status == "gave_up": + self._sse_label.setText("Disconnected") + self._sse_label.setStyleSheet("font-size: 11px; color: #F44336;") + self._sse_label.setToolTip(error or "Max retries reached") + FreeCAD.Console.PrintError( + f"Silo: SSE gave up after {retry} retries: {error}\n" + ) elif status == "unsupported": self._sse_label.setText("Not available") self._sse_label.setStyleSheet("font-size: 11px; color: #888;")