285 lines
9.2 KiB
Python
285 lines
9.2 KiB
Python
"""
|
|
APScheduler 集成模块
|
|
提供持久化任务调度服务
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from contextlib import suppress
|
|
from typing import Dict, List, Optional, Callable, AsyncGenerator
|
|
from datetime import datetime
|
|
|
|
from fastapi import Request
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
from apscheduler.executors.asyncio import AsyncIOExecutor
|
|
from apscheduler.job import Job
|
|
from apscheduler.triggers.interval import IntervalTrigger
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from apscheduler.triggers.date import DateTrigger
|
|
|
|
|
|
def acquire_scheduler_lock():
|
|
lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock")
|
|
lock_file = open(lock_path, "w")
|
|
try:
|
|
if sys.platform == "win32":
|
|
import msvcrt
|
|
|
|
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
|
|
else:
|
|
import fcntl
|
|
|
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_NB | fcntl.LOCK_EX)
|
|
return lock_file, True
|
|
except (IOError, OSError):
|
|
lock_file.close()
|
|
return None, False
|
|
|
|
|
|
class TaskService:
|
|
"""任务调度服务"""
|
|
|
|
def __init__(self, config, logger, async_session_maker=None):
|
|
self.async_session_maker = async_session_maker
|
|
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
|
|
self.job_store_db_path = config.get("scheduler", {}).get(
|
|
"job_store_path", "scheduler_jobs.db"
|
|
)
|
|
job_store_url = f"sqlite:///{self.job_store_db_path}"
|
|
|
|
# 配置 job stores 和 executors
|
|
jobstores = {"default": SQLAlchemyJobStore(url=job_store_url)}
|
|
executors = {"default": AsyncIOExecutor()}
|
|
job_defaults = {
|
|
"coalesce": True, # 合并错过的任务
|
|
"max_instances": 3, # 最大并发实例数
|
|
"misfire_grace_time": 30, # 错过任务的宽限时间(秒)
|
|
}
|
|
|
|
self.scheduler = AsyncIOScheduler(
|
|
jobstores=jobstores,
|
|
executors=executors,
|
|
job_defaults=job_defaults,
|
|
timezone="Asia/Shanghai",
|
|
)
|
|
self._started = False
|
|
self.lock_file, self.is_scheduler_worker = acquire_scheduler_lock()
|
|
self.logger = logger
|
|
|
|
async def start(self, init_tasks_callback: Optional[Callable] = None):
|
|
"""启动调度器"""
|
|
if self.is_scheduler_worker:
|
|
if not self._started:
|
|
self.scheduler.start()
|
|
self._started = True
|
|
self.logger.info("任务调度器已启动")
|
|
if init_tasks_callback:
|
|
await init_tasks_callback(self)
|
|
|
|
async def shutdown(self, wait: bool = True):
|
|
"""关闭调度器"""
|
|
if self.lock_file:
|
|
with suppress(Exception):
|
|
for job in await self.get_jobs():
|
|
await self.pause_job(job.get("id", ""))
|
|
|
|
if self._started:
|
|
self.scheduler.shutdown(wait=wait)
|
|
self._started = False
|
|
self.logger.info("任务调度器已关闭")
|
|
if sys.platform == "win32":
|
|
import msvcrt
|
|
|
|
msvcrt.locking(self.lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
|
else:
|
|
import fcntl
|
|
|
|
fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN)
|
|
self.lock_file.close()
|
|
|
|
async def add_interval_job(
|
|
self,
|
|
func: Callable,
|
|
seconds: int = 0,
|
|
minutes: int = 0,
|
|
hours: int = 0,
|
|
days: int = 0,
|
|
job_id: Optional[str] = None,
|
|
args: Optional[List] = None,
|
|
kwargs: Optional[Dict] = None,
|
|
replace_existing: bool = True,
|
|
) -> Job:
|
|
"""添加间隔执行任务"""
|
|
trigger = IntervalTrigger(
|
|
seconds=seconds, minutes=minutes, hours=hours, days=days
|
|
)
|
|
job = self.scheduler.add_job(
|
|
func,
|
|
trigger=trigger,
|
|
id=job_id,
|
|
args=args or [],
|
|
kwargs=kwargs or {},
|
|
replace_existing=replace_existing,
|
|
)
|
|
self.logger.info(f"添加间隔任务: {job.id}")
|
|
return job
|
|
|
|
async def add_cron_job(
|
|
self,
|
|
func: Callable,
|
|
job_id: Optional[str] = None,
|
|
year: Optional[int] = None,
|
|
month: Optional[int] = None,
|
|
day: Optional[int] = None,
|
|
week: Optional[int] = None,
|
|
day_of_week: Optional[int] = None,
|
|
hour: Optional[int] = None,
|
|
minute: Optional[int] = None,
|
|
second: Optional[int] = None,
|
|
args: Optional[List] = None,
|
|
kwargs: Optional[Dict] = None,
|
|
replace_existing: bool = True,
|
|
) -> Job:
|
|
"""添加 cron 表达式任务"""
|
|
trigger = CronTrigger(
|
|
year=year,
|
|
month=month,
|
|
day=day,
|
|
week=week,
|
|
day_of_week=day_of_week,
|
|
hour=hour,
|
|
minute=minute,
|
|
second=second,
|
|
)
|
|
job = self.scheduler.add_job(
|
|
func,
|
|
trigger=trigger,
|
|
id=job_id,
|
|
args=args or [],
|
|
kwargs=kwargs or {},
|
|
replace_existing=replace_existing,
|
|
)
|
|
self.logger.info(f"添加 cron 任务: {job.id}")
|
|
return job
|
|
|
|
async def add_date_job(
|
|
self,
|
|
func: Callable,
|
|
run_date: Optional[datetime] = None,
|
|
job_id: Optional[str] = None,
|
|
args: Optional[List] = None,
|
|
kwargs: Optional[Dict] = None,
|
|
replace_existing: bool = True,
|
|
) -> Job:
|
|
"""添加一次性任务"""
|
|
trigger = DateTrigger(run_date=run_date)
|
|
job = self.scheduler.add_job(
|
|
func,
|
|
trigger=trigger,
|
|
id=job_id,
|
|
args=args or [],
|
|
kwargs=kwargs or {},
|
|
replace_existing=replace_existing,
|
|
)
|
|
self.logger.info(f"添加一次性任务: {job.id}")
|
|
return job
|
|
|
|
async def remove_job(self, job_id: str) -> bool:
|
|
"""删除任务"""
|
|
try:
|
|
self.scheduler.remove_job(job_id)
|
|
self.logger.info(f"删除任务: {job_id}")
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"删除任务失败 {job_id}: {e}")
|
|
return False
|
|
|
|
async def pause_job(self, job_id: str) -> bool:
|
|
"""暂停任务"""
|
|
try:
|
|
self.scheduler.pause_job(job_id)
|
|
self.logger.info(f"暂停任务: {job_id}")
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"暂停任务失败 {job_id}: {e}")
|
|
return False
|
|
|
|
async def resume_job(self, job_id: str) -> bool:
|
|
"""恢复任务"""
|
|
try:
|
|
self.scheduler.resume_job(job_id)
|
|
self.logger.info(f"恢复任务: {job_id}")
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"恢复任务失败 {job_id}: {e}")
|
|
return False
|
|
|
|
async def get_job(self, job_id: str) -> Optional[Dict]:
|
|
"""获取任务信息"""
|
|
job = self.scheduler.get_job(job_id)
|
|
if job:
|
|
return self._job_to_dict(job)
|
|
return None
|
|
|
|
async def get_jobs(self) -> List[Dict]:
|
|
"""获取所有任务"""
|
|
jobs = self.scheduler.get_jobs()
|
|
return [self._job_to_dict(job) for job in jobs]
|
|
|
|
async def reschedule_job(
|
|
self, job_id: str, trigger_type: str, **trigger_args
|
|
) -> Optional[Job]:
|
|
"""重新调度任务(修改触发时间)"""
|
|
try:
|
|
job = self.scheduler.get_job(job_id)
|
|
if not job:
|
|
return None
|
|
|
|
if trigger_type == "interval":
|
|
trigger = IntervalTrigger(**trigger_args)
|
|
elif trigger_type == "cron":
|
|
trigger = CronTrigger(**trigger_args)
|
|
elif trigger_type == "date":
|
|
trigger = DateTrigger(**trigger_args)
|
|
else:
|
|
raise ValueError(f"不支持的 trigger 类型: {trigger_type}")
|
|
|
|
job.reschedule(trigger=trigger)
|
|
self.logger.info(f"重新调度任务: {job_id}")
|
|
return job
|
|
except Exception as e:
|
|
self.logger.error(f"重新调度任务失败 {job_id}: {e}")
|
|
return None
|
|
|
|
def _job_to_dict(self, job: Job) -> Dict:
|
|
"""将 Job 对象转换为字典"""
|
|
return {
|
|
"id": job.id,
|
|
"name": job.name,
|
|
"next_run_time": (
|
|
job.next_run_time.isoformat() if job.next_run_time else None
|
|
),
|
|
"trigger": str(job.trigger),
|
|
"pending": job.pending,
|
|
"args": job.args,
|
|
"kwargs": job.kwargs,
|
|
}
|
|
|
|
|
|
_task_service: Optional[TaskService] = None
|
|
|
|
|
|
def get_task_service_instance(config=None, logger=None, async_session_maker=None):
|
|
global _task_service
|
|
if _task_service is None:
|
|
if config is None or logger is None:
|
|
raise ValueError("请传入 config 和 logger")
|
|
_task_service = TaskService(config, logger, async_session_maker)
|
|
return _task_service
|
|
|
|
|
|
async def get_task_service(request: Request) -> AsyncGenerator:
|
|
return request.app.state.task_service
|