Files
Solo_bot/core/periodic_manager.py
T
2026-03-19 18:27:04 +03:00

130 lines
4.4 KiB
Python

import asyncio
import fcntl
import os
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from aiogram import Bot
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.base import BaseTrigger
from sqlalchemy.ext.asyncio import async_sessionmaker
from logger import logger
LoopRunner = Callable[[Bot, async_sessionmaker], Awaitable[None]]
CronRunner = Callable[[], Awaitable[None]]
@dataclass
class ManagedLoopTask:
task_id: str
runner: LoopRunner
@dataclass
class ManagedCronTask:
task_id: str
runner: CronRunner
trigger: BaseTrigger
class PeriodicTaskManager:
def __init__(self, timezone_name: str = "Europe/Moscow") -> None:
self.timezone_name = timezone_name
self._loop_tasks: dict[str, ManagedLoopTask] = {}
self._cron_tasks: dict[str, ManagedCronTask] = {}
self._running_tasks: dict[str, asyncio.Task] = {}
self._scheduler: AsyncIOScheduler | None = None
self._started = False
self._lock = asyncio.Lock()
self._process_lock_file = None
self._process_lock_path = "/tmp/solo_bot_periodic_manager.lock"
def register_loop_task(self, task_id: str, runner: LoopRunner) -> None:
self._loop_tasks[task_id] = ManagedLoopTask(task_id=task_id, runner=runner)
def register_cron_task(self, task_id: str, runner: CronRunner, trigger: BaseTrigger) -> None:
self._cron_tasks[task_id] = ManagedCronTask(task_id=task_id, runner=runner, trigger=trigger)
def _acquire_process_lock(self) -> bool:
if self._process_lock_file is not None:
return True
lock_file = open(self._process_lock_path, "a+", encoding="utf-8")
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
lock_file.seek(0)
lock_file.truncate()
lock_file.write(str(os.getpid()))
lock_file.flush()
self._process_lock_file = lock_file
return True
except OSError:
lock_file.close()
return False
def _release_process_lock(self) -> None:
if self._process_lock_file is None:
return
try:
fcntl.flock(self._process_lock_file.fileno(), fcntl.LOCK_UN)
except OSError:
pass
try:
self._process_lock_file.close()
except OSError:
pass
self._process_lock_file = None
async def start(self, bot: Bot, sessionmaker: async_sessionmaker) -> None:
async with self._lock:
if self._started:
return
if not self._acquire_process_lock():
logger.info("[PeriodicManager] Уже запущен в другом процессе, текущий запуск пропущен")
return
scheduler = AsyncIOScheduler(timezone=self.timezone_name)
for cron_task in self._cron_tasks.values():
scheduler.add_job(
cron_task.runner,
cron_task.trigger,
id=cron_task.task_id,
replace_existing=True,
max_instances=1,
coalesce=True,
)
scheduler.start()
self._scheduler = scheduler
for loop_task in self._loop_tasks.values():
self._running_tasks[loop_task.task_id] = asyncio.create_task(loop_task.runner(bot, sessionmaker))
self._started = True
logger.info(
"[PeriodicManager] Запущен: loop=%s cron=%s",
len(self._loop_tasks),
len(self._cron_tasks),
)
async def stop(self) -> None:
async with self._lock:
if not self._started:
return
if self._scheduler is not None:
try:
self._scheduler.shutdown(wait=False)
except Exception:
pass
self._scheduler = None
tasks = list(self._running_tasks.values())
self._running_tasks.clear()
for task in tasks:
task.cancel()
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
self._started = False
self._release_process_lock()
logger.info("[PeriodicManager] Остановлен")
periodic_task_manager = PeriodicTaskManager()