From 3eeca4613d618618093db416b564a2b9ef8dbe6a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:44:44 -0700 Subject: [PATCH] fix(qqbot): stop 100% CPU spin when WebSocket is closed but not None (#31193, #31771) (#40574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _read_events() returned normally when self._ws was closed-but-non-None (the while-condition is false on entry). _listen_loop treats a normal return as a clean read, resets backoff to 0, and immediately retries — a tight busy-loop pinning CPU. Raising on entry routes it through the reconnect/backoff path instead. Co-authored-by: xushibo Co-authored-by: cnfi --- gateway/platforms/qqbot/adapter.py | 6 ++++++ tests/gateway/test_qqbot.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/gateway/platforms/qqbot/adapter.py b/gateway/platforms/qqbot/adapter.py index 5b4a396ed2f..9532662131d 100644 --- a/gateway/platforms/qqbot/adapter.py +++ b/gateway/platforms/qqbot/adapter.py @@ -681,6 +681,12 @@ class QQAdapter(BasePlatformAdapter): """Read WebSocket frames until connection closes.""" if not self._ws: raise RuntimeError("WebSocket not connected") + if self._ws.closed: + # A closed-but-non-None ws makes the while-condition false on entry, + # so this would return normally — which _listen_loop treats as a + # clean read and immediately retries with backoff reset to 0, + # producing a 100% CPU spin. Raise so the reconnect/backoff path runs. + raise RuntimeError("WebSocket closed") while self._running and self._ws and not self._ws.closed: msg = await self._ws.receive() diff --git a/tests/gateway/test_qqbot.py b/tests/gateway/test_qqbot.py index e1f41aeccdc..816bb5f1601 100644 --- a/tests/gateway/test_qqbot.py +++ b/tests/gateway/test_qqbot.py @@ -2196,3 +2196,27 @@ class TestCloseCodeClassification: assert 4014 in fatal_codes assert 4001 in fatal_codes assert 4915 in fatal_codes + + +class TestReadEventsClosedWsGuard: + """Regression: a closed-but-non-None ws must raise on entry, not return + normally, so _listen_loop goes through reconnect/backoff instead of + busy-looping at 100% CPU (issues #31193 / #31771).""" + + def _make_adapter(self, **extra): + from gateway.platforms.qqbot import QQAdapter + return QQAdapter(_make_config(app_id="a", client_secret="b", **extra)) + + def test_read_events_raises_when_ws_closed_on_entry(self): + adapter = self._make_adapter() + adapter._running = True + adapter._ws = SimpleNamespace(closed=True) + with pytest.raises(RuntimeError): + asyncio.run(adapter._read_events()) + + def test_read_events_raises_when_ws_none(self): + adapter = self._make_adapter() + adapter._running = True + adapter._ws = None + with pytest.raises(RuntimeError): + asyncio.run(adapter._read_events())