From 7d10483961c384fd6d5f094dff2880e91bd0c8c6 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:28:12 +0800 Subject: [PATCH] fix(discord): preserve cleanup during startup stop (#6426) --- app/modules/discord/discord.py | 5 ---- tests/test_discord_lifecycle.py | 48 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/modules/discord/discord.py b/app/modules/discord/discord.py index 1f77ddacc..cc4a0c2cd 100644 --- a/app/modules/discord/discord.py +++ b/app/modules/discord/discord.py @@ -264,11 +264,6 @@ class Discord: ).result(timeout=10) except Exception as err: logger.error(f"关闭 Discord Bot 失败:{err}") - elif not loop.is_closed(): - try: - loop.call_soon_threadsafe(loop.stop) - except RuntimeError: - pass self._ready_event.clear() thread.join(timeout=5) if thread.is_alive(): diff --git a/tests/test_discord_lifecycle.py b/tests/test_discord_lifecycle.py index bb845ff18..c5476c604 100644 --- a/tests/test_discord_lifecycle.py +++ b/tests/test_discord_lifecycle.py @@ -27,6 +27,19 @@ class _DiscordClientStub: self._release.set() +class _YieldingCloseDiscordClientStub(_DiscordClientStub): + """关闭协程至少让出一次执行权,用于覆盖启动窗口内的停止竞态。""" + + def __init__(self) -> None: + super().__init__() + self.closed = threading.Event() + + async def close(self) -> None: + self.close_calls += 1 + await asyncio.sleep(0) + self.closed.set() + + def _discord(client: _DiscordClientStub) -> Discord: """构造只包含线程与事件循环生命周期状态的 Discord 实例。""" instance = Discord.__new__(Discord) @@ -81,3 +94,38 @@ def test_start_failure_closes_discord_thread_loop() -> None: assert instance._loop.is_closed() finally: _cleanup(instance) + + +def test_stop_during_thread_bootstrap_preserves_runner_cleanup(monkeypatch) -> None: + """线程已登记但循环尚未运行时,停止请求不得打断 runner 的关闭流程。""" + client = _YieldingCloseDiscordClientStub() + instance = _discord(client) + runner_entered = threading.Event() + release_runner = threading.Event() + original_set_event_loop = asyncio.set_event_loop + + def block_runner(loop: asyncio.AbstractEventLoop | None) -> None: + if loop is instance._loop: + runner_entered.set() + assert release_runner.wait(timeout=1) + original_set_event_loop(loop) + + monkeypatch.setattr( + "app.modules.discord.discord.asyncio.set_event_loop", + block_runner, + ) + instance._start() + assert runner_entered.wait(timeout=1) + stop_thread = threading.Thread(target=instance.stop) + stop_thread.start() + assert instance._stop_requested.wait(timeout=1) + release_runner.set() + + try: + stop_thread.join(timeout=2) + assert not stop_thread.is_alive() + assert client.closed.is_set() + assert instance._loop.is_closed() + finally: + release_runner.set() + _cleanup(instance)