import asyncio import importlib import json from asyncio import iscoroutinefunction from collections.abc import Callable from datetime import datetime, timedelta from typing import Any from apscheduler.events import EVENT_ALL, SchedulerEvent from apscheduler.executors.asyncio import AsyncIOExecutor from apscheduler.executors.pool import ProcessPoolExecutor from apscheduler.job import Job from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.jobstores.redis import RedisJobStore from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from redis import asyncio as aioredis from sqlalchemy.engine import Engine from sqlalchemy.ext.asyncio import AsyncEngine import module_task # noqa: F401 from common.constant import LockConstant from config.database import ( SYNC_SQLALCHEMY_DATABASE_URL, create_async_db_engine, create_async_session_local, create_sync_db_engine, create_sync_session_local, ) from config.env import AppConfig, LogConfig, RedisConfig from module_admin.dao.job_dao import JobDao from module_admin.entity.vo.job_vo import JobLogModel, JobModel from module_admin.service.job_log_service import JobLogService from utils.log_util import logger from utils.server_util import StartupUtil, WorkerIdUtil # 重写Cron定时 class MyCronTrigger(CronTrigger): CRON_EXPRESSION_LENGTH_MIN = 6 CRON_EXPRESSION_LENGTH_MAX = 7 WEEKDAY_COUNT = 5 @classmethod def from_crontab(cls, expr: str, timezone: str | None = None) -> 'MyCronTrigger': values = expr.split() if len(values) != cls.CRON_EXPRESSION_LENGTH_MIN and len(values) != cls.CRON_EXPRESSION_LENGTH_MAX: raise ValueError(f'Wrong number of fields; got {len(values)}, expected 6 or 7') second = values[0] minute = values[1] hour = values[2] if '?' in values[3]: day = None elif 'L' in values[5]: day = f'last {values[5].replace("L", "")}' elif 'W' in values[3]: day = cls.__find_recent_workday(int(values[3].split('W')[0])) else: day = values[3].replace('L', 'last') month = values[4] if '?' in values[5] or 'L' in values[5]: week = None elif '#' in values[5]: week = int(values[5].split('#')[1]) else: week = values[5] day_of_week = int(values[5].split('#')[0]) - 1 if '#' in values[5] else None year = values[6] if len(values) == cls.CRON_EXPRESSION_LENGTH_MAX else None return cls( second=second, minute=minute, hour=hour, day=day, month=month, week=week, day_of_week=day_of_week, year=year, timezone=timezone, ) @classmethod def __find_recent_workday(cls, day: int) -> int: now = datetime.now() date = datetime(now.year, now.month, day) if date.weekday() < cls.WEEKDAY_COUNT: return date.day diff = 1 while True: previous_day = date - timedelta(days=diff) if previous_day.weekday() < cls.WEEKDAY_COUNT: return previous_day.day diff += 1 redis_config = { 'host': RedisConfig.redis_host, 'port': RedisConfig.redis_port, 'username': RedisConfig.redis_username, 'password': RedisConfig.redis_password, 'db': RedisConfig.redis_database, } job_defaults = {'coalesce': False, 'max_instance': 1} scheduler = AsyncIOScheduler() class SchedulerUtil: """ 定时任务相关方法 """ # 分布式锁相关类变量 _is_leader: bool = False _worker_id: str = WorkerIdUtil.get_worker_id(LogConfig.log_worker_id) _redis: aioredis.Redis | None = None _job_update_time_cache: dict[str, datetime] = {} _sync_channel: str = 'scheduler:sync:request' _sync_listener_task: asyncio.Task | None = None _lock_lost_task: asyncio.Task | None = None _sync_task: asyncio.Task | None = None _sync_pending: bool = False _sync_lock: asyncio.Lock = asyncio.Lock() _last_sync_at: datetime | None = None _sync_debounce_seconds: float = 0.5 _sync_min_interval_seconds: float = 2.0 _reacquire_task: asyncio.Task | None = None _reacquire_interval_seconds: float = 5.0 _sync_async_engine: AsyncEngine | None = None _sync_async_sessionmaker: Any | None = None _disposed_sync_engines: bool = False # 懒加载的同步 Engine 和 SessionLocal _jobstore_engine: Engine | None = None _listener_engine: Engine | None = None _session_local: Any | None = None _scheduler_configured: bool = False @classmethod def _get_jobstore_engine(cls) -> Engine: """ 懒加载获取 jobstore 使用的同步 Engine :return: 同步 Engine """ if cls._jobstore_engine is None: cls._jobstore_engine = create_sync_db_engine(echo=False) return cls._jobstore_engine @classmethod def _get_listener_engine(cls) -> Engine: """ 懒加载获取 listener 使用的同步 Engine :return: 同步 Engine """ if cls._listener_engine is None: cls._listener_engine = create_sync_db_engine() return cls._listener_engine @classmethod def _get_session_local(cls) -> Any: """ 懒加载获取同步 SessionLocal :return: SessionLocal """ if cls._session_local is None: cls._session_local = create_sync_session_local(cls._get_listener_engine()) return cls._session_local @classmethod def _configure_scheduler(cls) -> None: """ 配置 scheduler(懒加载 jobstore) :return: None """ if cls._scheduler_configured: return job_stores = { 'default': MemoryJobStore(), 'sqlalchemy': SQLAlchemyJobStore(url=SYNC_SQLALCHEMY_DATABASE_URL, engine=cls._get_jobstore_engine()), 'redis': RedisJobStore(**redis_config), } executors = {'default': AsyncIOExecutor(), 'processpool': ProcessPoolExecutor(5)} scheduler.configure(jobstores=job_stores, executors=executors, job_defaults=job_defaults) cls._scheduler_configured = True @classmethod def _should_enable_scheduler_sync(cls) -> bool: """ 判断是否需要启用多 worker 的任务状态同步机制 :return: 是否开启定时同步与监听 """ return not AppConfig.app_reload and AppConfig.app_workers > 1 @classmethod async def init_system_scheduler(cls, redis: aioredis.Redis) -> None: """ 应用启动时初始化定时任务(使用分布式锁确保只有一个worker启动scheduler) :param redis: Redis连接对象 :return: """ cls._redis = redis logger.info(f'🔎 Worker {cls._worker_id} 尝试获取 Application 锁...') acquired = await StartupUtil.acquire_startup_log_gate( redis=redis, lock_key=LockConstant.APP_STARTUP_LOCK_KEY, worker_id=cls._worker_id, lock_expire_seconds=LockConstant.LOCK_EXPIRE_SECONDS, ) if acquired: await cls._start_scheduler_as_leader(redis) else: cls._is_leader = False logger.info(f'⏸️ Worker {cls._worker_id} 未持有 Application 锁,跳过 Scheduler 启动') @classmethod async def _start_scheduler_as_leader(cls, redis: aioredis.Redis) -> None: """ 以 Leader 身份启动 Scheduler(内部方法,调用前需确保已持有锁) :param redis: Redis连接对象 :return: None """ cls._is_leader = True cls._disposed_sync_engines = False logger.info(f'🎯 Worker {cls._worker_id} 持有 Application 锁,开始启动定时任务...') # 懒加载配置 scheduler cls._configure_scheduler() scheduler.start() # 加载数据库中的定时任务 async with cls._get_sync_async_session() as session: job_list = await JobDao.get_job_list_for_scheduler(session) for item in job_list: cls._add_job_to_scheduler(item) # 添加事件监听器 scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL) if cls._should_enable_scheduler_sync(): # 添加任务状态同步任务(每30秒从数据库同步一次任务状态) scheduler.add_job( func=cls.request_scheduler_sync, trigger='interval', seconds=30, id='_scheduler_job_sync', name='Scheduler任务同步', replace_existing=True, ) cls._sync_listener_task = asyncio.create_task(cls._listen_sync_channel(redis)) logger.info('✅️ 系统初始定时任务加载成功') @classmethod def on_lock_lost(cls) -> None: """ 锁丢失处理入口 :return: None """ if not cls._is_leader: return cls._is_leader = False logger.warning(f'⚠️ Worker {cls._worker_id} 失去 Application 锁') if cls._lock_lost_task: cls._lock_lost_task.cancel() cls._lock_lost_task = asyncio.create_task(cls._handle_lock_lost()) @classmethod async def _handle_lock_lost(cls) -> None: """ 处理锁丢失后的资源释放 :return: None """ if cls._sync_listener_task: cls._sync_listener_task.cancel() try: await cls._sync_listener_task except asyncio.CancelledError: pass cls._sync_listener_task = None if cls._sync_task: cls._sync_task.cancel() try: await cls._sync_task except asyncio.CancelledError: pass cls._sync_task = None cls._sync_pending = False if getattr(scheduler, 'running', False): scheduler.shutdown() await cls._dispose_sync_async_engine() cls._dispose_sync_engines() cls._ensure_reacquire_task() @classmethod async def _sync_jobs_from_database(cls) -> None: """ 从数据库同步任务状态,确保多worker环境下任务状态一致 """ if not cls._is_leader: return try: async with cls._get_sync_async_session() as session: db_jobs_all = await JobDao.get_all_job_list_for_scheduler(session) db_jobs_enabled = [job for job in db_jobs_all if job.status == '0'] db_enabled_ids = {str(job.job_id) for job in db_jobs_enabled} db_job_map = {str(job.job_id): job for job in db_jobs_enabled} db_job_update_time_map = { str(job.job_id): job.update_time for job in db_jobs_enabled if job.update_time is not None } scheduler_jobs = scheduler.get_jobs() scheduler_job_map = {job.id: job for job in scheduler_jobs if not job.id.startswith('_')} scheduler_job_ids = set(scheduler_job_map.keys()) jobs_to_remove = scheduler_job_ids - db_enabled_ids for job_id in jobs_to_remove: scheduler.remove_job(job_id=job_id) logger.info(f'🗑️ 同步移除任务: {job_id}') cls._refresh_job_update_cache(job_id, None) jobs_to_add = db_enabled_ids - scheduler_job_ids for job_id in jobs_to_add: job_info = db_job_map.get(job_id) if job_info: cls._add_job_to_scheduler(job_info) logger.info(f'➕ 同步添加任务: {job_info.job_name}') cls._refresh_job_update_cache(job_id, job_info.update_time) jobs_to_update = db_enabled_ids & scheduler_job_ids for job_id in jobs_to_update: job_info = db_job_map.get(job_id) scheduler_job = scheduler_job_map.get(job_id) job_update_time = db_job_update_time_map.get(job_id) cls._sync_update_job(job_id, job_info, scheduler_job, job_update_time) except Exception as e: logger.error(f'❌ 任务同步异常: {e}') @classmethod def _is_job_config_in_sync(cls, scheduler_job: Job, job_info: JobModel) -> bool: """ 判断任务配置是否一致 :param scheduler_job: 调度器任务对象 :param job_info: 数据库任务对象 :return: 是否一致 """ job_state = scheduler_job.__getstate__() job_kwargs = json.loads(job_info.job_kwargs) if job_info.job_kwargs else None job_args = job_info.job_args.split(',') if job_info.job_args else None job_executor = job_info.job_executor if iscoroutinefunction(cls._import_function(job_info.invoke_target)): job_executor = 'default' expected = { 'name': job_info.job_name, 'executor': job_executor, 'jobstore': job_info.job_group, 'misfire_grace_time': 1000000000000 if job_info.misfire_policy == '3' else None, 'coalesce': job_info.misfire_policy == '2', 'max_instances': 3 if job_info.concurrent == '0' else 1, 'trigger': str(MyCronTrigger.from_crontab(job_info.cron_expression)), 'args': tuple(job_args) if job_args else None, 'kwargs': job_kwargs if job_kwargs else None, 'func': str(cls._import_function(job_info.invoke_target)), } current = { 'name': job_state.get('name'), 'executor': job_state.get('executor'), 'jobstore': scheduler_job._jobstore_alias, 'misfire_grace_time': job_state.get('misfire_grace_time'), 'coalesce': job_state.get('coalesce'), 'max_instances': job_state.get('max_instances'), 'trigger': str(job_state.get('trigger')), 'args': job_state.get('args'), 'kwargs': job_state.get('kwargs'), 'func': str(job_state.get('func')), } return expected == current @classmethod def _sync_update_job( cls, job_id: str, job_info: JobModel | None, scheduler_job: Job | None, job_update_time: datetime | None ) -> None: """ 同步更新任务配置 :param job_id: 任务ID :param job_info: 数据库任务对象 :param scheduler_job: 调度器任务对象 :param job_update_time: 任务更新时间 :return: None """ if not job_info or not scheduler_job: return if cls._should_skip_job_update(job_id, job_update_time): return if not cls._is_job_config_in_sync(scheduler_job, job_info): scheduler.remove_job(job_id=job_id) cls._add_job_to_scheduler(job_info) logger.info(f'♻️ 同步更新任务: {job_info.job_name}') cls._refresh_job_update_cache(job_id, job_update_time) @classmethod def _should_skip_job_update(cls, job_id: str, job_update_time: datetime | None) -> bool: """ 判断是否跳过同步更新 :param job_id: 任务ID :param job_update_time: 任务更新时间 :return: 是否跳过 """ if job_update_time is None: return False return cls._job_update_time_cache.get(job_id) == job_update_time @classmethod def _refresh_job_update_cache(cls, job_id: str, job_update_time: datetime | None) -> None: """ 刷新任务更新时间缓存 :param job_id: 任务ID :param job_update_time: 任务更新时间 :return: None """ if job_update_time is not None: cls._job_update_time_cache[job_id] = job_update_time else: cls._job_update_time_cache.pop(job_id, None) @classmethod async def request_scheduler_sync(cls) -> None: """ 请求调度器同步任务状态 :return: None """ if cls._is_leader: cls._sync_pending = True cls._ensure_sync_task() return if cls._redis: await cls._redis.publish(cls._sync_channel, cls._worker_id) @classmethod def _ensure_sync_task(cls) -> None: """ 启动同步调度任务 :return: None """ if cls._sync_task and not cls._sync_task.done(): return cls._sync_task = asyncio.create_task(cls._run_sync_loop()) @classmethod def _get_sync_async_session(cls) -> Any: """ 获取同步任务使用的异步 Session :return: 异步 Session """ if not cls._sync_async_sessionmaker: cls._sync_async_engine = create_async_db_engine(echo=False) cls._sync_async_sessionmaker = create_async_session_local(cls._sync_async_engine) return cls._sync_async_sessionmaker() @classmethod async def _dispose_sync_async_engine(cls) -> None: """ 释放同步任务使用的异步 Engine :return: None """ if cls._sync_async_engine: await cls._sync_async_engine.dispose() cls._sync_async_engine = None cls._sync_async_sessionmaker = None @classmethod def _dispose_sync_engines(cls) -> None: """ 释放 Scheduler 使用的同步 Engine :return: None """ if cls._disposed_sync_engines: return if cls._jobstore_engine: cls._jobstore_engine.dispose() cls._jobstore_engine = None if cls._listener_engine: cls._listener_engine.dispose() cls._listener_engine = None cls._session_local = None cls._disposed_sync_engines = True @classmethod def _ensure_reacquire_task(cls) -> None: """ 启动锁重新竞争任务 :return: None """ if not cls._redis: return if cls._reacquire_task and not cls._reacquire_task.done(): return cls._reacquire_task = asyncio.create_task(cls._run_reacquire_loop()) @classmethod async def _run_reacquire_loop(cls) -> None: """ 循环尝试重新获取锁并恢复调度器 :return: None """ try: while not cls._is_leader: if not cls._redis: await asyncio.sleep(cls._reacquire_interval_seconds) continue acquired = await StartupUtil.acquire_startup_log_gate( redis=cls._redis, lock_key=LockConstant.APP_STARTUP_LOCK_KEY, worker_id=cls._worker_id, lock_expire_seconds=LockConstant.LOCK_EXPIRE_SECONDS, ) if acquired: # 直接调用 _start_scheduler_as_leader,避免重复获取锁 await cls._start_scheduler_as_leader(cls._redis) return await asyncio.sleep(cls._reacquire_interval_seconds) except asyncio.CancelledError: raise finally: cls._reacquire_task = None @classmethod async def _run_sync_loop(cls) -> None: """ 执行同步调度循环 :return: None """ try: while True: if not cls._sync_pending: break cls._sync_pending = False await asyncio.sleep(cls._sync_debounce_seconds) await cls._sync_with_throttle() except asyncio.CancelledError: raise finally: cls._sync_task = None @classmethod async def _sync_with_throttle(cls) -> None: """ 按节流规则执行同步 :return: None """ async with cls._sync_lock: if not cls._is_leader: return if cls._last_sync_at: elapsed = datetime.now() - cls._last_sync_at min_interval = timedelta(seconds=cls._sync_min_interval_seconds) if elapsed < min_interval: await asyncio.sleep((min_interval - elapsed).total_seconds()) await cls._sync_jobs_from_database() cls._last_sync_at = datetime.now() @classmethod async def _listen_sync_channel(cls, redis: aioredis.Redis) -> None: """ 监听同步请求通道 :param redis: Redis连接对象 :return: None """ while True: pubsub = redis.pubsub() try: await pubsub.subscribe(cls._sync_channel) async for message in pubsub.listen(): if not cls._is_leader: continue if message.get('type') != 'message': continue await cls.request_scheduler_sync() except asyncio.CancelledError: await pubsub.unsubscribe(cls._sync_channel) await pubsub.close() raise except Exception as e: logger.error(f'❌ Scheduler 同步监听异常: {e},5秒后重试...') await pubsub.close() await asyncio.sleep(5) finally: try: await pubsub.close() except Exception: pass @classmethod async def _execute_async_job_with_log( cls, job_func: Callable[..., Any], job_info: JobModel, args: list, kwargs: dict ) -> None: """ 执行异步任务并记录日志 :param job_func: 任务函数 :param job_info: 任务对象信息 :param args: 位置参数 :param kwargs: 关键字参数 :return: None """ status = '0' exception_info = '' job_executor = job_info.job_executor if iscoroutinefunction(job_func): job_executor = 'default' try: await job_func(*args, **kwargs) except Exception as e: status = '1' exception_info = str(e) logger.error(f'❌ 异步执行任务 {job_info.job_name} 失败: {e}') finally: cls._record_job_execution_log(job_info, job_executor, status, exception_info) @classmethod def _record_job_execution_log(cls, job_info: JobModel, job_executor: str, status: str, exception_info: str) -> None: """ 记录任务执行日志(用于非 Leader Worker 直接执行任务时) :param job_info: 任务对象信息 :param job_executor: 任务执行器 :param status: 执行状态 0-成功 1-失败 :param exception_info: 异常信息 :return: None """ try: job_args = job_info.job_args if job_info.job_args else '' job_kwargs = job_info.job_kwargs if job_info.job_kwargs else '{}' job_trigger = str(MyCronTrigger.from_crontab(job_info.cron_expression)) if job_info.cron_expression else '' job_message = ( f'事件类型: DirectExecution(非Leader), 任务ID: {job_info.job_id}, ' f'任务名称: {job_info.job_name}, 执行于{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' ) job_log = JobLogModel( jobName=job_info.job_name, jobGroup=job_info.job_group, jobExecutor=job_executor, invokeTarget=job_info.invoke_target, jobArgs=job_args, jobKwargs=job_kwargs, jobTrigger=job_trigger, jobMessage=job_message, status=status, exceptionInfo=exception_info, createTime=datetime.now(), ) session = cls._get_session_local()() try: JobLogService.add_job_log_services(session, job_log) finally: session.close() except Exception as e: logger.error(f'❌ 记录任务执行日志失败: {e}') @classmethod def _prepare_scheduler_job_add(cls, job_info: JobModel) -> dict[str, Any]: """ 构建调度器任务参数 :param job_info: 任务对象信息 :return: 调度器任务参数 """ job_func = cls._import_function(job_info.invoke_target) job_executor = job_info.job_executor if iscoroutinefunction(job_func): job_executor = 'default' return { 'func': job_func, 'trigger': MyCronTrigger.from_crontab(job_info.cron_expression), 'args': job_info.job_args.split(',') if job_info.job_args else None, 'kwargs': json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, 'id': str(job_info.job_id), 'name': job_info.job_name, 'misfire_grace_time': 1000000000000 if job_info.misfire_policy == '3' else None, 'coalesce': job_info.misfire_policy == '2', 'max_instances': 3 if job_info.concurrent == '0' else 1, 'jobstore': job_info.job_group, 'executor': job_executor, } @classmethod def _add_job_to_scheduler(cls, job_info: JobModel) -> None: """ 内部方法:将任务添加到调度器(不检查应用锁状态,仅供内部使用) :param job_info: 任务对象信息 """ try: # 先移除已存在的同ID任务 existing_job = scheduler.get_job(job_id=str(job_info.job_id)) if existing_job: scheduler.remove_job(job_id=str(job_info.job_id)) scheduler.add_job(**cls._prepare_scheduler_job_add(job_info)) except Exception as e: logger.error(f'❌ 添加任务 {job_info.job_name} 失败: {e}') @classmethod async def close_system_scheduler(cls) -> None: """ 应用关闭时关闭定时任务 :return: """ if cls._sync_listener_task: cls._sync_listener_task.cancel() try: await cls._sync_listener_task except asyncio.CancelledError: pass cls._sync_listener_task = None if cls._sync_task: cls._sync_task.cancel() try: await cls._sync_task except asyncio.CancelledError: pass cls._sync_task = None cls._sync_pending = False if cls._reacquire_task: cls._reacquire_task.cancel() try: await cls._reacquire_task except asyncio.CancelledError: pass cls._reacquire_task = None await cls._dispose_sync_async_engine() cls._dispose_sync_engines() if cls._lock_lost_task: cls._lock_lost_task.cancel() try: await cls._lock_lost_task except asyncio.CancelledError: pass cls._lock_lost_task = None if getattr(scheduler, 'running', False): scheduler.shutdown() logger.info('✅️ 关闭定时任务成功') # 释放锁 if cls._redis: current_holder = await cls._redis.get(LockConstant.APP_STARTUP_LOCK_KEY) if current_holder == cls._worker_id: await cls._redis.delete(LockConstant.APP_STARTUP_LOCK_KEY) logger.info(f'🔓 Worker {cls._worker_id} 释放 Application 锁') @classmethod def _import_function(cls, func_path: str) -> Callable[..., Any]: """ 动态导入函数 :param func_path: 函数字符串,如module_task.scheduler_test.job :return: 导入的函数对象 """ module_path, func_name = func_path.rsplit('.', 1) module = importlib.import_module(module_path) return getattr(module, func_name) @classmethod def get_scheduler_job(cls, job_id: str | int) -> Job: """ 根据任务id获取任务对象 :param job_id: 任务id :return: 任务对象 """ query_job = scheduler.get_job(job_id=str(job_id)) return query_job @classmethod def add_scheduler_job(cls, job_info: JobModel) -> None: """ 根据输入的任务对象信息添加任务 :param job_info: 任务对象信息 :return: """ # 非应用锁 worker 跳过操作(数据库状态是持久化的,持有应用锁时会加载) if not cls._is_leader: return scheduler.add_job(**cls._prepare_scheduler_job_add(job_info)) @classmethod def execute_scheduler_job_once(cls, job_info: JobModel) -> None: """ 根据输入的任务对象执行一次任务 :param job_info: 任务对象信息 :return: """ job_func = cls._import_function(job_info.invoke_target) job_executor = job_info.job_executor if iscoroutinefunction(job_func): job_executor = 'default' # 非应用锁 worker:直接执行函数(不通过 scheduler) if not cls._is_leader: logger.info(f'📍 当前 Worker 未持有 Application 锁,直接执行任务 {job_info.job_name}') args = job_info.job_args.split(',') if job_info.job_args else [] kwargs = json.loads(job_info.job_kwargs) if job_info.job_kwargs else {} status = '0' exception_info = '' try: if iscoroutinefunction(job_func): asyncio.create_task(cls._execute_async_job_with_log(job_func, job_info, args, kwargs)) # noqa: RUF006 else: job_func(*args, **kwargs) except Exception as e: status = '1' exception_info = str(e) logger.error(f'❌ 直接执行任务 {job_info.job_name} 失败: {e}') finally: # 同步任务记录日志(异步任务在 _execute_async_job_with_log 中记录) if not iscoroutinefunction(job_func): cls._record_job_execution_log(job_info, job_executor, status, exception_info) return # 应用锁 worker:通过 scheduler 执行 job_trigger = DateTrigger() if job_info.status == '0': job_trigger = OrTrigger(triggers=[DateTrigger(), MyCronTrigger.from_crontab(job_info.cron_expression)]) scheduler.add_job( func=job_func, trigger=job_trigger, args=job_info.job_args.split(',') if job_info.job_args else None, kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, id=str(job_info.job_id), name=job_info.job_name, misfire_grace_time=1000000000000 if job_info.misfire_policy == '3' else None, coalesce=job_info.misfire_policy == '2', max_instances=3 if job_info.concurrent == '0' else 1, jobstore=job_info.job_group, executor=job_executor, ) @classmethod def remove_scheduler_job(cls, job_id: str | int) -> None: """ 根据任务id移除任务 :param job_id: 任务id :return: """ # 非应用锁 worker 跳过操作(数据库状态是持久化的,持有应用锁时会根据状态加载) if not cls._is_leader: return query_job = cls.get_scheduler_job(job_id=job_id) if query_job: scheduler.remove_job(job_id=str(job_id)) @classmethod def scheduler_event_listener(cls, event: SchedulerEvent) -> None: """ 调度器事件监听器,记录任务执行日志 """ try: # 获取事件类型和任务ID event_type = event.__class__.__name__ # 获取任务执行异常信息 status = '0' exception_info = '' if event_type == 'JobExecutionEvent' and event.exception: exception_info = str(event.exception) status = '1' if hasattr(event, 'job_id'): job_id = event.job_id # 跳过内部系统任务(以 _ 开头的任务ID),不记录日志 if str(job_id).startswith('_'): return query_job = cls.get_scheduler_job(job_id=job_id) if query_job: query_job_info = query_job.__getstate__() # 获取任务名称 job_name = query_job_info.get('name') # 获取任务组名 job_group = query_job._jobstore_alias # 获取任务执行器 job_executor = query_job_info.get('executor') # 获取调用目标字符串 invoke_target = query_job_info.get('func') # 获取调用函数位置参数(安全处理) args = query_job_info.get('args') job_args = ','.join(str(arg) for arg in args) if args else '' # 获取调用函数关键字参数 kwargs = query_job_info.get('kwargs') job_kwargs = json.dumps(kwargs) if kwargs else '{}' # 获取任务触发器 job_trigger = str(query_job_info.get('trigger')) # 构造日志消息 job_message = f'事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 执行于{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' job_log = JobLogModel( jobName=job_name, jobGroup=job_group, jobExecutor=job_executor, invokeTarget=invoke_target, jobArgs=job_args, jobKwargs=job_kwargs, jobTrigger=job_trigger, jobMessage=job_message, status=status, exceptionInfo=exception_info, createTime=datetime.now(), ) session = cls._get_session_local()() try: JobLogService.add_job_log_services(session, job_log) finally: session.close() except Exception as e: logger.error(f'❌ 调度任务事件监听器异常: {e}')