Исправление: замена signal handler на on_shutdown (устранён race condition event loop)
This commit is contained in:
parent
07af928654
commit
5a11fb2b5d
40
bot.py
40
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)
|
||||
|
||||
|
||||
@ -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."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user