Files
postbot/app/bots/bot_runner.py

125 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}")