From 5a11fb2b5d4ac1039578644297b4e9b99892e98a Mon Sep 17 00:00:00 2001 From: deadzilla Date: Tue, 7 Jul 2026 22:51:23 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5:=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20signal=20handler=20=D0=BD=D0=B0=20on=5Fshutdown=20(?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=80=D0=B0=D0=BD=D1=91=D0=BD=20race=20condi?= =?UTF-8?q?tion=20event=20loop)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 40 ++++++++++++---------------------------- tests/test_bot.py | 34 +++++++++++++++------------------- 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/bot.py b/bot.py index d6ee391..d3c57c1 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,6 @@ import asyncio import logging import os -import signal import sys import threading from typing import TYPE_CHECKING @@ -106,31 +105,15 @@ class BotRunner: """Повторяет текст после !msg""" await ctx.send(text) - def _signal_handler(self, signum: int, frame) -> None: - """Обработать сигнал завершения для graceful shutdown. - - Останавливает планировщик и корректно закрывает бота, - чтобы подключения к gateway завершались штатно. - """ - sig_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM" - logger.info("Получен сигнал %s, graceful shutdown...", sig_name) - self.stop_event.set() - - # Остановить планировщик + def _on_shutdown(self) -> None: + """Остановить планировщик при завершении работы бота.""" if self.scheduler: self.scheduler.stop() + logger.info("Планировщик остановлен") - # Закрыть бота асинхронно - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - loop.run_until_complete(self.bot.close()) - except Exception: - pass - finally: - loop.close() - - sys.exit(0) + async def _on_shutdown_async(self) -> None: + """Асинхронный хук завершения (discord.py on_shutdown).""" + self._on_shutdown() def run(self, token: str) -> None: """Запустить бота с graceful shutdown.""" @@ -139,6 +122,8 @@ class BotRunner: async def main() -> None: try: async with self.bot: + # Регистрируем on_shutdown внутри контекста бота + self.bot.add_listener(self._on_shutdown_async, "on_shutdown") await self.bot.start(token, reconnect=True) except discord.LoginFailure as e: logger.critical("Ошибка авторизации бота: %s", e, exc_info=True) @@ -158,15 +143,14 @@ class BotRunner: ) logger.error("Критическая ошибка при запуске. Код ошибки: %s", type(e).__name__) sys.exit(1) - - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) + finally: + # Context manager (async with self.bot) закрывает бота автоматически + self._on_shutdown() try: asyncio.run(main()) except KeyboardInterrupt: - # asyncio.run() выбрасывает KeyboardInterrupt при Ctrl+C, - # но сигнал уже обработан _signal_handler и бот закрыт + # Ctrl+C прерывает bot.start(), context manager закрывает бота logger.info("Bot shutdown complete") sys.exit(0) diff --git a/tests/test_bot.py b/tests/test_bot.py index 697ea9f..cdc031c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -62,28 +62,24 @@ class TestBotErrorHandling: runner.run("fake_token") mock_exit.assert_called_once_with(1) - def test_signal_handlers_installed(self): - """BotRunner.run() устанавливает обработчики SIGINT и SIGTERM.""" - import asyncio + def test_shutdown_uses_on_shutdown_listener(self): + """BotRunner.run() регистрирует on_shutdown вместо signal handlers. + + Signal handlers с asyncio.new_event_loop() создают race condition + с основным loop. Вместо них используется: + - discord.py on_shutdown событие для остановки планировщика + - async with self.bot (context manager) для graceful shutdown + """ import bot - import signal runner = bot.BotRunner() - - def fake_run(coro): - """Поддельный asyncio.run, который утилизирует корутину.""" - try: - coro.close() - except RuntimeError: - pass # корутина уже закрыта - - with patch("signal.signal") as mock_signal: - with patch("asyncio.run", side_effect=fake_run): - runner.run("fake_token") - # signal.signal вызван для SIGINT и SIGTERM - call_args = [call[0][0] for call in mock_signal.call_args_list] - assert signal.SIGINT in call_args - assert signal.SIGTERM in call_args + # Проверяем, что _on_shutdown и _on_shutdown_async методы существуют + assert hasattr(runner, "_on_shutdown"), "Метод _on_shutdown должен существовать" + assert hasattr(runner, "_on_shutdown_async"), "Метод _on_shutdown_async должен существовать" + # Проверяем, что signal модуль НЕ импортирован в bot.py + with open(ROOT_DIR / "bot.py", encoding="utf-8") as f: + content = f.read() + assert "signal.signal" not in content, "Не должно быть signal.signal — используется on_shutdown" def test_code_uses_async_bot_pattern(self): """Проверка, что bot.py использует async with / asyncio.run."""