## Summary
In backend/app/routers/collaboration.py, the CollaborationManager.disconnect() method deletes a room from self.rooms only when the last user disconnects cleanly via WebSocketDisconnect. However, when clients drop abruptly (network failure, tab close, browser crash), their sockets become stale and are only removed lazily inside broadcast() via the stale_clients cleanup path. If all clients in a room drop abruptly without anyone sending a message that triggers a broadcast, the room is never deleted — it stays in self.rooms forever with an empty sockets dict but retaining its full code, comments, and users state in memory.
## Steps To Reproduce
- Start the backend locally.
- Connect two clients to
WS /ws/test-room.
- Kill both clients abruptly (close the browser tab or kill the process) without sending any further messages.
- No broadcast is triggered, so
stale_clients cleanup in broadcast() never runs.
- Inspect
manager.rooms - test-room still exists with empty sockets and users but retains all its code and comments in memory.
- Repeat with many unique session IDs -
manager.rooms grows without bound, leaking memory for the lifetime of the server process.
## Expected Behavior
Rooms with no active sockets should be deleted from manager.rooms promptly. Either:
- A periodic background task should sweep
manager.rooms and remove any room where len(room.sockets) == 0, or
- The stale socket cleanup path inside
broadcast() should also trigger a room deletion check after removing the last client.
## Actual Behavior
The disconnect() method correctly deletes a room on a clean disconnect only when should_delete is True:
# backend/app/routers/collaboration.py - disconnect()
async with room.lock:
room.sockets.pop(client_id, None)
room.users.pop(client_id, None)
users = self._users_payload(room)
should_delete = not room.sockets # ← True only on clean WebSocketDisconnect
if should_delete:
self.rooms.pop(session_id, None) # ← never reached on abrupt disconnect
return
But the stale client cleanup inside broadcast() calls disconnect() per stale client — which itself only deletes the room if should_delete is True after each removal. If all clients are stale and drop simultaneously without a broadcast being triggered, disconnect() is never called at all:
# backend/app/routers/collaboration.py — broadcast()
stale_clients: list[str] = []
for client_id, socket in list(room.sockets.items()):
try:
await socket.send_json(message)
except RuntimeError:
stale_clients.append(client_id) # ← detected only during a broadcast
for client_id in stale_clients:
await self.disconnect(session_id, client_id) # ← never called if no broadcast fires
So if all clients in a room drop silently with no subsequent broadcast, the room leaks permanently with empty sockets and users but full code and comments retained.
## Expected Fix
Add a periodic sweep task to scheduler.py that removes empty rooms:
def _purge_empty_collab_rooms() -> None:
from ..routers.collaboration import manager
empty = [sid for sid, room in list(manager.rooms.items()) if not room.sockets]
for sid in empty:
manager.rooms.pop(sid, None)
if empty:
log.info("Purged %d empty collaboration room(s)", len(empty))
And register it in start_scheduler():
scheduler.add_job(
_purge_empty_collab_rooms,
trigger="interval",
minutes=30,
id="purge_empty_collab_rooms",
replace_existing=True,
)
## Environment
- OS: Linux (backend/server-side bug — not OS-specific)
- Browser (if frontend): N/A
- Python version (if backend): 3.12+
## Logs/Screenshots
No runtime logs needed — the leak is confirmed by reading the code. Verified with:
grep -n "should_delete\|stale_clients\|rooms.pop\|disconnect" backend/app/routers/collaboration.py
# should_delete — only set True on clean WebSocketDisconnect path
# stale_clients — only cleaned up if a broadcast fires
# rooms.pop — never called if no broadcast triggers after abrupt drop
I would like to work on this issue as part of GSSoC '26.Could you please assign this issue to me
## Summary
In
backend/app/routers/collaboration.py, theCollaborationManager.disconnect()method deletes a room fromself.roomsonly when the last user disconnects cleanly viaWebSocketDisconnect. However, when clients drop abruptly (network failure, tab close, browser crash), their sockets become stale and are only removed lazily insidebroadcast()via thestale_clientscleanup path. If all clients in a room drop abruptly without anyone sending a message that triggers a broadcast, the room is never deleted — it stays inself.roomsforever with an emptysocketsdict but retaining its fullcode,comments, andusersstate in memory.## Steps To Reproduce
WS /ws/test-room.stale_clientscleanup inbroadcast()never runs.manager.rooms-test-roomstill exists with emptysocketsandusersbut retains all itscodeandcommentsin memory.manager.roomsgrows without bound, leaking memory for the lifetime of the server process.## Expected Behavior
Rooms with no active sockets should be deleted from
manager.roomspromptly. Either:manager.roomsand remove any room wherelen(room.sockets) == 0, orbroadcast()should also trigger a room deletion check after removing the last client.## Actual Behavior
The
disconnect()method correctly deletes a room on a clean disconnect only whenshould_deleteisTrue:But the stale client cleanup inside
broadcast()callsdisconnect()per stale client — which itself only deletes the room ifshould_deleteisTrueafter each removal. If all clients are stale and drop simultaneously without a broadcast being triggered,disconnect()is never called at all:So if all clients in a room drop silently with no subsequent broadcast, the room leaks permanently with empty
socketsandusersbut fullcodeandcommentsretained.## Expected Fix
Add a periodic sweep task to
scheduler.pythat removes empty rooms:And register it in
start_scheduler():## Environment
## Logs/Screenshots
No runtime logs needed — the leak is confirmed by reading the code. Verified with:
I would like to work on this issue as part of GSSoC '26.Could you please assign this issue to me