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