Исправление: замена 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 asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@ -106,31 +105,15 @@ class BotRunner:
|
|||||||
"""Повторяет текст после !msg"""
|
"""Повторяет текст после !msg"""
|
||||||
await ctx.send(text)
|
await ctx.send(text)
|
||||||
|
|
||||||
def _signal_handler(self, signum: int, frame) -> None:
|
def _on_shutdown(self) -> None:
|
||||||
"""Обработать сигнал завершения для graceful shutdown.
|
"""Остановить планировщик при завершении работы бота."""
|
||||||
|
|
||||||
Останавливает планировщик и корректно закрывает бота,
|
|
||||||
чтобы подключения к gateway завершались штатно.
|
|
||||||
"""
|
|
||||||
sig_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM"
|
|
||||||
logger.info("Получен сигнал %s, graceful shutdown...", sig_name)
|
|
||||||
self.stop_event.set()
|
|
||||||
|
|
||||||
# Остановить планировщик
|
|
||||||
if self.scheduler:
|
if self.scheduler:
|
||||||
self.scheduler.stop()
|
self.scheduler.stop()
|
||||||
|
logger.info("Планировщик остановлен")
|
||||||
|
|
||||||
# Закрыть бота асинхронно
|
async def _on_shutdown_async(self) -> None:
|
||||||
loop = asyncio.new_event_loop()
|
"""Асинхронный хук завершения (discord.py on_shutdown)."""
|
||||||
asyncio.set_event_loop(loop)
|
self._on_shutdown()
|
||||||
try:
|
|
||||||
loop.run_until_complete(self.bot.close())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def run(self, token: str) -> None:
|
def run(self, token: str) -> None:
|
||||||
"""Запустить бота с graceful shutdown."""
|
"""Запустить бота с graceful shutdown."""
|
||||||
@ -139,6 +122,8 @@ class BotRunner:
|
|||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
try:
|
try:
|
||||||
async with self.bot:
|
async with self.bot:
|
||||||
|
# Регистрируем on_shutdown внутри контекста бота
|
||||||
|
self.bot.add_listener(self._on_shutdown_async, "on_shutdown")
|
||||||
await self.bot.start(token, reconnect=True)
|
await self.bot.start(token, reconnect=True)
|
||||||
except discord.LoginFailure as e:
|
except discord.LoginFailure as e:
|
||||||
logger.critical("Ошибка авторизации бота: %s", e, exc_info=True)
|
logger.critical("Ошибка авторизации бота: %s", e, exc_info=True)
|
||||||
@ -158,15 +143,14 @@ class BotRunner:
|
|||||||
)
|
)
|
||||||
logger.error("Критическая ошибка при запуске. Код ошибки: %s", type(e).__name__)
|
logger.error("Критическая ошибка при запуске. Код ошибки: %s", type(e).__name__)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
signal.signal(signal.SIGINT, self._signal_handler)
|
# Context manager (async with self.bot) закрывает бота автоматически
|
||||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
self._on_shutdown()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# asyncio.run() выбрасывает KeyboardInterrupt при Ctrl+C,
|
# Ctrl+C прерывает bot.start(), context manager закрывает бота
|
||||||
# но сигнал уже обработан _signal_handler и бот закрыт
|
|
||||||
logger.info("Bot shutdown complete")
|
logger.info("Bot shutdown complete")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|||||||
@ -62,28 +62,24 @@ class TestBotErrorHandling:
|
|||||||
runner.run("fake_token")
|
runner.run("fake_token")
|
||||||
mock_exit.assert_called_once_with(1)
|
mock_exit.assert_called_once_with(1)
|
||||||
|
|
||||||
def test_signal_handlers_installed(self):
|
def test_shutdown_uses_on_shutdown_listener(self):
|
||||||
"""BotRunner.run() устанавливает обработчики SIGINT и SIGTERM."""
|
"""BotRunner.run() регистрирует on_shutdown вместо signal handlers.
|
||||||
import asyncio
|
|
||||||
|
Signal handlers с asyncio.new_event_loop() создают race condition
|
||||||
|
с основным loop. Вместо них используется:
|
||||||
|
- discord.py on_shutdown событие для остановки планировщика
|
||||||
|
- async with self.bot (context manager) для graceful shutdown
|
||||||
|
"""
|
||||||
import bot
|
import bot
|
||||||
import signal
|
|
||||||
|
|
||||||
runner = bot.BotRunner()
|
runner = bot.BotRunner()
|
||||||
|
# Проверяем, что _on_shutdown и _on_shutdown_async методы существуют
|
||||||
def fake_run(coro):
|
assert hasattr(runner, "_on_shutdown"), "Метод _on_shutdown должен существовать"
|
||||||
"""Поддельный asyncio.run, который утилизирует корутину."""
|
assert hasattr(runner, "_on_shutdown_async"), "Метод _on_shutdown_async должен существовать"
|
||||||
try:
|
# Проверяем, что signal модуль НЕ импортирован в bot.py
|
||||||
coro.close()
|
with open(ROOT_DIR / "bot.py", encoding="utf-8") as f:
|
||||||
except RuntimeError:
|
content = f.read()
|
||||||
pass # корутина уже закрыта
|
assert "signal.signal" not in content, "Не должно быть signal.signal — используется on_shutdown"
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def test_code_uses_async_bot_pattern(self):
|
def test_code_uses_async_bot_pattern(self):
|
||||||
"""Проверка, что bot.py использует async with / asyncio.run."""
|
"""Проверка, что bot.py использует async with / asyncio.run."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user