fix(qqbot): avoid closed websocket busy loop - #31333
Conversation
|
I rechecked this against current In The fix (raise |
|
+1, hit this on my production gateway (v2026.5.29-858). Same root cause: _read_events() guard Repro path (confirmed via logs + ss -tnp):
Local fix I applied (identical approach to this PR): adapter.py:682
This makes the guard consistent with line 685 and forces _read_events to raise on a closed Would love to see this merged — the bug is easy to hit and hard to diagnose (zero log output, |
|
This has been implemented on current Automated hermes-sweeper review evidence:
|
Summary
Fixes a QQBot gateway edge case where a closed WebSocket can make the gateway spin in a tight reconnect/read loop instead of backing off.
Changes:
QQAdapter._read_events()raiseRuntimeError("WebSocket closed")when the receive loop exits while the adapter is still running_listen_loopfrom treating an already-closed websocket as a clean read-loop returnreceive()How this was encountered
I hit this on a long-running local Hermes gateway with Discord, Weixin, and QQBot enabled. After a transient network/DNS disruption, QQBot reported WebSocket close/reconnect failures. The gateway process stayed alive under launchd, but it stopped responding normally:
heartbeat blockedfor many hourshermes gateway restartcould not cleanly recover the wedged process; launchd SIGKILL was needed to let the service relaunchRoot cause: if
_read_events()is entered whileself._ws.closedis already true, this condition is false immediately:Before this PR, that meant
_read_events()returned silently._listen_loop()then treated the return as a clean read-loop exit and immediately started the next loop iteration, which can create a no-sleep/no-backoff busy loop around the closed websocket state.How to reproduce
The new regression test captures the minimal reproducer:
QQAdapteradapter._running = Trueadapter._wsto an object whereclosed == Trueawait adapter._read_events()Before this fix:
_read_events()returned silentlyDID NOT RAISE RuntimeErrorAfter this fix:
_read_events()raisesRuntimeError("WebSocket closed")receive()is not awaited for an already-closed websocket_listen_loop()takes the existing exception/reconnect path instead of resetting disconnect counters as if the read loop ended cleanlySanitized logs from the incident
Gateway log samples:
gateway.error.logshowed the cross-platform symptom: Discord heartbeat warnings while the event loop traceback pointed into the QQBot adapter read loop:A later warning showed the same wedged gateway had been blocked for much longer:
All user paths and long identifiers above are redacted.
Fix result
With this PR, the already-closed websocket path now raises and flows into the existing
_listen_loop()exception handler/backoff logic. This avoids the observed busy-loop failure mode and allows QQBot reconnect handling to behave like other websocket-close errors.After applying the local patch and restarting the gateway, the service recovered:
The local gateway CPU dropped from ~100% back to idle after relaunch.
Test Plan
venv/bin/python -m pytest tests/gateway/test_qqbot.py tests/gateway/test_platform_http_client_limits.py -q -o 'addopts='Result:
The warnings are pre-existing coroutine warnings from unrelated QQBot tests; the new regression test passes.