Files

125 lines
4.2 KiB
Python

from __future__ import annotations
from typing import Any, Callable, Optional
import pytz
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from app.core.logging import get_logger
logger = get_logger(__name__)
class Scheduler:
def __init__(self, name: str = "scheduler", timezone: Optional[str] = None) -> None:
self._name = name
self._scheduler: Optional[AsyncIOScheduler] = None
# 使用指定的时区,如果没有指定则使用本地时区
self._timezone = pytz.timezone(timezone) if timezone else None
def start(self) -> AsyncIOScheduler:
if self._scheduler is None:
# 配置调度器使用本地时区
from tzlocal import get_localzone
local_tz = get_localzone()
self._scheduler = AsyncIOScheduler(timezone=local_tz)
self._scheduler.start()
logger.info(f"{self._name} started with timezone: {local_tz}")
return self._scheduler
def shutdown(self) -> None:
if self._scheduler is not None:
self._scheduler.shutdown(wait=False)
self._scheduler = None
logger.info(f"{self._name} stopped")
def add_cron_job(
self,
job_id: str,
cron_expression: str,
func: Callable[..., Any],
args: Optional[list[Any]] = None,
kwargs: Optional[dict[str, Any]] = None,
**job_kwargs: Any,
) -> None:
scheduler = self._get_scheduler()
# 使用调度器的时区创建 CronTrigger
trigger = CronTrigger.from_crontab(cron_expression, timezone=scheduler.timezone)
scheduler.add_job(
func,
trigger=trigger,
args=args or [],
kwargs=kwargs or {},
id=job_id,
replace_existing=job_kwargs.pop("replace_existing", True),
max_instances=job_kwargs.pop("max_instances", 1),
coalesce=job_kwargs.pop("coalesce", True),
misfire_grace_time=job_kwargs.pop("misfire_grace_time", 60),
**job_kwargs,
)
def add_interval_job(
self,
job_id: str,
seconds: int,
func: Callable[..., Any],
args: Optional[list[Any]] = None,
kwargs: Optional[dict[str, Any]] = None,
**job_kwargs: Any,
) -> None:
scheduler = self._get_scheduler()
trigger = IntervalTrigger(seconds=seconds, timezone=scheduler.timezone)
scheduler.add_job(
func,
trigger=trigger,
args=args or [],
kwargs=kwargs or {},
id=job_id,
replace_existing=job_kwargs.pop("replace_existing", True),
max_instances=job_kwargs.pop("max_instances", 1),
coalesce=job_kwargs.pop("coalesce", True),
misfire_grace_time=job_kwargs.pop("misfire_grace_time", 60),
**job_kwargs,
)
def remove_job(self, job_id: str) -> None:
if self._scheduler is None:
return
if self._scheduler.get_job(job_id):
self._scheduler.remove_job(job_id)
def has_job(self, job_id: str) -> bool:
if self._scheduler is None:
return False
return self._scheduler.get_job(job_id) is not None
def reschedule_job(
self,
job_id: str,
cron_expression: str,
func: Callable[..., Any],
args: Optional[list[Any]] = None,
kwargs: Optional[dict[str, Any]] = None,
**job_kwargs: Any,
) -> None:
self.remove_job(job_id)
self.add_cron_job(
job_id=job_id,
cron_expression=cron_expression,
func=func,
args=args,
kwargs=kwargs,
**job_kwargs,
)
@staticmethod
def validate_cron_expression(cron_expression: str) -> None:
CronTrigger.from_crontab(cron_expression)
def _get_scheduler(self) -> AsyncIOScheduler:
if self._scheduler is None:
raise RuntimeError(f"{self._name} not initialized")
return self._scheduler