125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
import logging
|
||
import asyncio
|
||
import signal
|
||
from contextlib import suppress
|
||
from typing import Optional, Any
|
||
|
||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||
from telegram.ext import Application
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class BotApplication:
|
||
"""Класс для управления жизненным циклом бота и планировщика."""
|
||
|
||
def __init__(self):
|
||
self.app: Optional[Application] = None
|
||
self.scheduler: Optional[AsyncIOScheduler] = None
|
||
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
||
self._shutdown: bool = False
|
||
|
||
def signal_handler(self, signum: int, frame: Any) -> None:
|
||
"""Обработчик сигналов для корректного завершения."""
|
||
logger.info("Received shutdown signal")
|
||
self._shutdown = True
|
||
if self.loop and self.loop.is_running():
|
||
self.loop.create_task(self.shutdown())
|
||
|
||
async def shutdown(self) -> None:
|
||
"""Корректное завершение работы всех компонентов."""
|
||
logger.info("Starting graceful shutdown")
|
||
|
||
# Останавливаем планировщик
|
||
if self.scheduler and self.scheduler.running:
|
||
logger.debug("Shutting down scheduler")
|
||
with suppress(Exception):
|
||
self.scheduler.shutdown()
|
||
|
||
# Останавливаем приложение
|
||
if self.app:
|
||
logger.debug("Shutting down application")
|
||
with suppress(Exception):
|
||
await self.app.stop()
|
||
with suppress(Exception):
|
||
await self.app.shutdown()
|
||
|
||
# Ждем завершения всех задач
|
||
if self.loop:
|
||
tasks = [t for t in asyncio.all_tasks(self.loop)
|
||
if t is not asyncio.current_task(self.loop)]
|
||
if tasks:
|
||
logger.debug(f"Cancelling {len(tasks)} pending tasks")
|
||
for task in tasks:
|
||
task.cancel()
|
||
await asyncio.gather(*tasks, return_exceptions=True)
|
||
|
||
async def run_bot(self, app: Application, cleanup_job) -> None:
|
||
"""Запуск бота и планировщика."""
|
||
try:
|
||
self.app = app
|
||
if not self.app:
|
||
logger.critical("Failed to initialize application")
|
||
return
|
||
|
||
# Настройка планировщика
|
||
self.scheduler = AsyncIOScheduler()
|
||
self.scheduler.add_job(cleanup_job, 'interval', minutes=30)
|
||
|
||
# Инициализация приложения
|
||
await self.app.initialize()
|
||
await self.app.start()
|
||
|
||
# Запуск планировщика
|
||
self.scheduler.start()
|
||
|
||
# Запуск бота и ожидание завершения
|
||
stop_event = asyncio.Event()
|
||
|
||
def stop_polling():
|
||
stop_event.set()
|
||
|
||
# Устанавливаем обработчик для остановки
|
||
self.app.stop_signals = None # Отключаем встроенную обработку сигналов
|
||
|
||
try:
|
||
# Запускаем поллинг
|
||
await self.app.updater.start_polling(drop_pending_updates=True)
|
||
|
||
# Ждем сигнала остановки
|
||
await stop_event.wait()
|
||
except Exception as e:
|
||
if not self._shutdown:
|
||
logger.error(f"Polling error: {e}")
|
||
await asyncio.sleep(1)
|
||
|
||
except Exception as e:
|
||
logger.critical(f"Critical error: {e}")
|
||
finally:
|
||
await self.shutdown()
|
||
|
||
def run_bot(app: Application, cleanup_job) -> None:
|
||
"""Функция запуска бота с правильной обработкой сигналов и циклом событий."""
|
||
bot_app = BotApplication()
|
||
|
||
# Настройка обработчиков сигналов
|
||
signal.signal(signal.SIGINT, bot_app.signal_handler)
|
||
signal.signal(signal.SIGTERM, bot_app.signal_handler)
|
||
|
||
# Создание и настройка цикла событий
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
bot_app.loop = loop
|
||
|
||
try:
|
||
loop.run_until_complete(bot_app.run_bot(app, cleanup_job))
|
||
except KeyboardInterrupt:
|
||
pass
|
||
finally:
|
||
try:
|
||
if not loop.is_closed():
|
||
# Очистка
|
||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||
loop.close()
|
||
except Exception as e:
|
||
logger.error(f"Error during loop cleanup: {e}")
|