From 9498adc7c4fc2fafbb03d6e30aa3207a2dfd3fbb Mon Sep 17 00:00:00 2001 From: Ari Date: Sun, 2 Aug 2026 11:26:28 -0300 Subject: [PATCH] fix: close ws_bridge send loop on client disconnect Previously the ws_bridge send loop blocked forever on queue.get(), never learning the client left. When the server service stopped, uvicorn waited for open WebSocket tasks until systemd SIGKILLed after TimeoutStopSec=90s. Now each iteration races recv+send; a completed receive means the client disconnected => break the loop. --- src/openjarvis/server/ws_bridge.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/openjarvis/server/ws_bridge.py b/src/openjarvis/server/ws_bridge.py index a8a422cb..94c912af 100644 --- a/src/openjarvis/server/ws_bridge.py +++ b/src/openjarvis/server/ws_bridge.py @@ -81,8 +81,27 @@ def create_ws_router(event_bus: EventBus) -> Any: clients[websocket] = (queue, loop) try: while True: - payload = await queue.get() - await websocket.send_json(payload) + recv = asyncio.create_task(websocket.receive()) + payload = asyncio.create_task(queue.get()) + done, _ = await asyncio.wait( + {recv, payload}, return_when=asyncio.FIRST_COMPLETED + ) + for task in (recv, payload): + if task not in done: + task.cancel() + if recv in done: + # A completed receive surfaces the client's disconnect — + # Starlette delivers it as a message (or raises + # WebSocketDisconnect for a clean close frame) only when + # the app actually reads from the socket. Without this the + # handler never learns the client left, its task stays + # open forever, and uvicorn's graceful shutdown blocks on + # it until systemd SIGKILLs the unit (TimeoutStopSec). + message = await recv + if message.get("type") == "websocket.disconnect": + break + else: + await websocket.send_json(payload.result()) except WebSocketDisconnect: pass finally: