modify: 更新日志配置
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user