fix(hub): reassign pending_owners on WS reconnect — prevents approval response loss
This commit is contained in:
69
hub.py
69
hub.py
@@ -269,6 +269,19 @@ class WSHub:
|
|||||||
self.project_connections[conn.project] = set()
|
self.project_connections[conn.project] = set()
|
||||||
self.project_connections[conn.project].add(conn.conn_id)
|
self.project_connections[conn.project].add(conn.conn_id)
|
||||||
|
|
||||||
|
# FIX: Reassign orphaned pending_owners (from dead conn_ids) to this new connection.
|
||||||
|
# When Extension reconnects, old conn_id entries become stale.
|
||||||
|
reassigned = 0
|
||||||
|
for rid, cid in list(self.pending_owners.items()):
|
||||||
|
if cid not in self.connections:
|
||||||
|
self.pending_owners[rid] = conn.conn_id
|
||||||
|
reassigned += 1
|
||||||
|
if reassigned:
|
||||||
|
logger.info(
|
||||||
|
f"[HUB] Reassigned {reassigned} orphaned pending_owners "
|
||||||
|
f"to new conn {conn.conn_id} (project={conn.project})"
|
||||||
|
)
|
||||||
|
|
||||||
# Broadcast instance update to all project connections
|
# Broadcast instance update to all project connections
|
||||||
asyncio.create_task(self._broadcast_instance_update(conn.project))
|
asyncio.create_task(self._broadcast_instance_update(conn.project))
|
||||||
|
|
||||||
@@ -292,10 +305,26 @@ class WSHub:
|
|||||||
if not self.project_connections[project]:
|
if not self.project_connections[project]:
|
||||||
del self.project_connections[project]
|
del self.project_connections[project]
|
||||||
|
|
||||||
# Clean up pending ownership
|
# Reassign pending ownership to another connection in same project
|
||||||
|
# (instead of deleting — prevents approval responses from being lost)
|
||||||
stale = [rid for rid, cid in self.pending_owners.items() if cid == conn_id]
|
stale = [rid for rid, cid in self.pending_owners.items() if cid == conn_id]
|
||||||
for rid in stale:
|
if stale:
|
||||||
del self.pending_owners[rid]
|
remaining = self.project_connections.get(project, set()) - {conn_id}
|
||||||
|
new_owner = None
|
||||||
|
for cid in remaining:
|
||||||
|
c = self.connections.get(cid)
|
||||||
|
if c and c.authenticated:
|
||||||
|
new_owner = cid
|
||||||
|
break
|
||||||
|
for rid in stale:
|
||||||
|
if new_owner:
|
||||||
|
self.pending_owners[rid] = new_owner
|
||||||
|
logger.info(
|
||||||
|
f"[HUB] Reassigned pending {rid[:12]} → {new_owner} "
|
||||||
|
f"(disconnected {conn_id})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
del self.pending_owners[rid]
|
||||||
|
|
||||||
# Close WebSocket if still open
|
# Close WebSocket if still open
|
||||||
if not conn.ws.closed:
|
if not conn.ws.closed:
|
||||||
@@ -374,13 +403,39 @@ class WSHub:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_response_to_pending_owner(self, request_id: str, message: dict):
|
async def send_response_to_pending_owner(self, request_id: str, message: dict):
|
||||||
"""Route a response to the Extension that created the pending request."""
|
"""Route a response to the Extension that created the pending request.
|
||||||
|
|
||||||
|
Falls back to any active connection in the same project if the
|
||||||
|
original owner disconnected (e.g. Extension WS reconnected with
|
||||||
|
a new conn_id).
|
||||||
|
"""
|
||||||
conn_id = self.pending_owners.get(request_id)
|
conn_id = self.pending_owners.get(request_id)
|
||||||
if conn_id:
|
if conn_id:
|
||||||
await self.send_to_connection(conn_id, message)
|
conn = self.connections.get(conn_id)
|
||||||
# Clean up after response delivered
|
if conn and conn.authenticated and not conn.ws.closed:
|
||||||
|
await self.send_to_connection(conn_id, message)
|
||||||
|
self.pending_owners.pop(request_id, None)
|
||||||
|
return True
|
||||||
|
# Original owner dead — try to find any active connection in same project
|
||||||
|
project = conn.project if conn else None
|
||||||
|
if project:
|
||||||
|
for cid in self.project_connections.get(project, set()):
|
||||||
|
c = self.connections.get(cid)
|
||||||
|
if c and c.authenticated and not c.ws.closed:
|
||||||
|
await self.send_to_connection(cid, message)
|
||||||
|
self.pending_owners.pop(request_id, None)
|
||||||
|
logger.info(
|
||||||
|
f"[HUB] Rerouted response {request_id[:12]} → {cid} "
|
||||||
|
f"(original {conn_id} dead)"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
# No active connection found — clean up
|
||||||
self.pending_owners.pop(request_id, None)
|
self.pending_owners.pop(request_id, None)
|
||||||
return True
|
logger.warning(
|
||||||
|
f"[HUB] Response {request_id[:12]} lost: owner {conn_id} dead, "
|
||||||
|
f"no active connections in project"
|
||||||
|
)
|
||||||
|
return False
|
||||||
logger.warning(f"[HUB] No owner for pending {request_id[:12]}")
|
logger.warning(f"[HUB] No owner for pending {request_id[:12]}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user