基于 Flask-APScheduler 实现的自定义监听器

基于 Flask-APScheduler 自定义监听器实现原理

概述

在构建动态定时任务管理系统时,监听器(Event Listener)是一个核心组件,它负责捕获和处理任务调度过程中的各种事件。本文将深入探讨基于 Flask-APScheduler 的监听器实现,包括其设计思路、核心原理和关键技术细节。

监听器的核心用途

1. 任务执行监控

监听器能够实时监控任务的整个生命周期,包括:

  • 任务提交:当任务被提交给调度器时
  • 任务执行:任务开始执行的时刻
  • 任务完成:任务成功执行完成
  • 任务失败:任务执行过程中出现异常
  • 任务错过:任务因各种原因未能按时执行

2. 执行历史记录

通过监听器,我们可以:

  • 记录每次任务执行的详细信息
  • 追踪任务执行时间、持续时间和状态
  • 保存任务返回值和异常信息
  • 构建完整的任务执行审计日志

3. 运行时行为分析

监听器提供了深入了解系统运行时行为的能力:

  • 分析任务执行模式
  • 识别性能瓶颈
  • 监控资源使用情况
  • 检测异常执行模式

架构设计

整体架构

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   APScheduler   │───▶│  Event Listener  │───▶│   Database      │
│   Scheduler     │    │  (监听器)         │    │  (SQLite)       │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Job Store     │    │  Log Capture     │    │  ExecutedJob    │
│  (任务存储)      │    │  (日志捕获)        │    │  (执行记录表)     │
└─────────────────┘    └──────────────────┘    └─────────────────┘

核心组件

1. 事件监听器 (SchedulerEventListener)

采用单例模式设计,确保在整个应用生命周期中只有一个监听器实例:

class SchedulerEventListener:
    _instance: Optional[Any] = None
    _lock: Lock = Lock()

    def __new__(cls, *args, **kwargs):
        """单例模式实现"""
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
2. 事件处理映射

使用字典映射将事件类型与处理函数关联:

self._event_handlers = {
    events.SchedulerEvent: self._on_scheduler_event,
    events.JobEvent: self._on_job_event,
    events.JobSubmissionEvent: self._on_job_submission_event,
    events.JobExecutionEvent: self._on_job_executed_event
}
3. 事件描述映射

为不同类型的事件提供人类可读的描述:

self._event_mappings = {
    events.EVENT_SCHEDULER_STARTED: 'Scheduler started',
    events.EVENT_JOB_EXECUTED: 'Job executed',
    events.EVENT_JOB_ERROR: 'Job exception',
    events.EVENT_JOB_MISSED: 'Job missed'
    # ... 更多事件映射
}

关键技术实现

1. 猴子补丁(Monkey Patching)

核心原理

猴子补丁是一种在运行时动态修改代码行为的技术。在我们的监听器实现中,通过猴子补丁拦截了 APScheduler 的核心执行函数,从而捕获任务执行过程中的日志信息。

实现细节
def _intercept_job_logging(self):
    """在实例化时进行作业日志拦截替换"""
    import apscheduler.executors.base
    import apscheduler.executors.pool

    # 保存原始函数
    if not hasattr(apscheduler.executors.base, '_run_job_original'):
        apscheduler.executors.base._run_job_original = apscheduler.executors.base.run_job

    # 替换base模块中的函数
    apscheduler.executors.base.run_job = self._run_job_enhanced

    # 关键:同时替换pool模块中的引用
    apscheduler.executors.pool.run_job = self._run_job_enhanced
增强的执行函数
def _run_job_enhanced(self, job, jobstore_alias, run_times, logger_name):
    """增强版的run_job函数,用于拦截日志"""
    import apscheduler.executors.base

    # 创建独立的日志流
    stream = StringIO()
    handler = logging.StreamHandler(stream)

    # 获取根日志器并添加处理器
    logger = logging.getLogger()
    logger.addHandler(handler)

    try:
        # 调用原始函数,避免递归
        original_func = getattr(apscheduler.executors.base, '_run_job_original')
        retval = original_func(job, jobstore_alias, run_times, logger_name)
    except Exception as e:
        logger.error(f"Error in job execution: {e}")
        raise
    finally:
        # 移除日志处理器
        logger.removeHandler(handler)

        # 记录日志到实例的_logs属性中
        content = stream.getvalue().strip()
        if content:
            self._logs[job.id] = content

    return retval

2. 日志流捕获机制

设计思路

通过创建独立的 StringIO 流和 StreamHandler,将任务执行过程中的所有日志输出重定向到内存中的字符串流,从而实现日志的捕获和存储。

技术优势
  • 非侵入式:不需要修改任务函数的代码
  • 完整性:捕获任务执行过程中的所有日志输出
  • 性能优化:使用内存流,避免磁盘 I/O 开销
  • 线程安全:每个任务执行都有独立的日志流

3. 数据持久化策略

执行记录模型
class ExecutedJob(db.Model):
    __tablename__ = 'executed_job'

    id = db.Column(db.Integer, primary_key=True)
    task_id = db.Column(db.String(128), nullable=False)
    task_name = db.Column(db.String(128), nullable=False)
    task_trigger = db.Column(db.JSON, nullable=False)
    task_func = db.Column(db.String(128), nullable=False)
    task_params = db.Column(db.JSON)
    status = db.Column(db.String(32), nullable=False)  # submitted, success, failed, missed
    run_time = db.Column(db.DateTime)
    end_time = db.Column(db.DateTime)
    duration = db.Column(db.Integer)
    traceback = db.Column(db.Text)
    message = db.Column(db.Text)  # 存储捕获的日志
    _retval = db.Column('retval', db.LargeBinary)  # 序列化的返回值
智能记录更新

监听器实现了智能的记录更新机制:

  • 首次提交:创建 “submitted” 状态的初始记录
  • 执行完成:更新记录状态、执行结果和日志信息
  • 异常处理:捕获并存储异常信息和堆栈跟踪

4. 触发器解析

支持的触发器类型

监听器能够解析和记录多种类型的触发器:

def _parsing_triggers(self, trigger: BaseTrigger) -> Dict[str, Any]:
    """解析触发器"""
    data = {}
    if isinstance(trigger, DateTrigger):
        data['trigger'] = 'date'
        if trigger.run_date:
            data['run_date'] = trigger.run_date.isoformat()

    elif isinstance(trigger, CronTrigger):
        data['trigger'] = 'cron'
        for field in trigger.fields:
            if not field.is_default:
                data[field.name] = str(field)

    elif isinstance(trigger, IntervalTrigger):
        data['trigger'] = 'interval'
        w, d, hh, mm, ss = self._extract_timedelta(trigger.interval)
        if w > 0: data["weeks"] = w
        if d > 0: data["days"] = d
        if hh > 0: data["hours"] = hh
        if mm > 0: data["minutes"] = mm
        if ss > 0: data["seconds"] = ss

    return data

性能优化

1. 缓存机制

监听器使用内存缓存来存储任务信息,减少对调度器的频繁查询:

# 初始化任务信息缓存
self._job_cache = {}

# 缓存任务信息
def _get_job_info(self, job_id: str, event: Optional[events.JobExecutionEvent] = None) -> dict:
    job = self._scheduler.get_job(job_id)

    if job is None:
        # 使用缓存的信息或默认值
        cached_job = self._job_cache.get(job_id)
        if cached_job:
            return cached_job
        # ... 返回默认信息

    # 缓存新获取的任务信息
    job_info = {
        'id': job.id,
        'name': job.name if job.name else 'unnamed',
        'func': job.func_ref if job.func_ref else 'unknown',
        # ... 其他信息
    }
    self._job_cache[job_id] = job_info
    return job_info

2. 数据库连接优化

使用应用上下文管理数据库会话,确保连接的正确关闭:

def _save_record_to_db(self, record: ExecutedJob, operation: str = 'add') -> bool:
    with self._app.app_context():
        session = self._db.session
        try:
            if operation == 'merge':
                session.merge(record)
            else:
                session.add(record)
            session.commit()
            return True
        except Exception as e:
            session.rollback()
            return False
        finally:
            session.close()

3. 内存管理

  • 及时清理已完成的任务缓存
  • 使用生成器处理大量数据
  • 实现连接池复用

错误处理与恢复

1. 异常捕获策略

监听器实现了多层次的异常捕获:

def event_listener(self, event):
    """事件监听器主方法"""
    try:
        handler = self._event_handlers.get(type(event))
        if not handler:
            self._logger.warning(f"No event handler found: {event.code}")
            return

        handler(event)
    except Exception as e:
        self._logger.error(f"Event handling failed: {e}")
        # 不抛出异常,确保监听器继续运行

2. 数据完整性保证

  • 使用数据库事务确保记录的一致性
  • 实现记录更新而非重复创建
  • 提供数据修复机制

3. 容错机制

  • 调度器锁机制防止重复初始化
  • 任务信息缓存应对任务删除场景
  • 默认值处理缺失信息

实际应用场景

1. 生产环境监控

在生产环境中,监听器可以帮助我们:

  • 实时监控:了解任务执行状态和性能
  • 故障排查:快速定位问题任务和异常
  • 性能分析:识别执行时间过长的任务
  • 资源优化:基于执行历史调整任务配置

2. 运维自动化

结合监听器数据,可以实现:

  • 自动告警:任务失败时发送通知
  • 自动重试:对失败任务进行智能重试
  • 负载均衡:根据执行情况动态调整任务分布
  • 容量规划:基于历史数据预测资源需求

3. 业务分析

监听器收集的数据可用于:

  • 业务指标统计:任务成功率、执行频率等
  • 趋势分析:识别任务执行模式的变化
  • 异常检测:发现异常的任务行为
  • 报告生成:生成详细的任务执行报告

最佳实践

1. 配置建议

# 推荐的调度器配置
SCHEDULER_JOB_DEFAULTS = {
    'coalesce': True,           # 合并错过的任务
    'max_instances': 1,         # 限制并发实例数
    'replace_existing': True,   # 替换已存在的任务
    'misfire_grace_time': 300   # 任务错过容忍时间(秒)
}

2. 任务设计原则

  • 幂等性:确保任务可以安全重复执行
  • 错误处理:在任务内部实现适当的异常处理
  • 日志记录:使用标准日志接口记录执行信息
  • 资源清理:确保任务完成后释放资源

3. 监控策略

  • 关键指标监控:成功率、执行时间、失败率
  • 告警机制:及时通知异常情况
  • 定期审计:检查历史执行记录
  • 容量规划:基于历史数据调整系统配置

当前实现的缺陷与改进建议

虽然当前的监听器实现功能强大,但在实际生产环境中仍存在一些需要改进的问题:

1. 日志捕获的局限性

问题描述

当前实现使用根日志记录器(logging.getLogger())来捕获任务执行日志,这会导致以下问题:

def _run_job_enhanced(self, job, jobstore_alias, run_times, logger_name):
    # 获取根日志器 - 这是问题的根源
    logger = logging.getLogger()
    logger.addHandler(handler)  # 会捕获所有日志,而不仅仅是当前任务的
具体缺陷
  1. 日志混杂:多个任务同时执行时,它们的日志会相互交织在一起
  2. 无关日志捕获:会捕获系统中其他组件的日志信息
  3. 日志重复:如果其他地方也添加了相同的处理器,可能导致日志重复
  4. 性能影响:大量无关日志的处理会影响系统性能
实际案例

假设有两个任务同时执行:

  • 任务A:数据同步任务,产生大量数据库操作日志
  • 任务B:简单的HTTP请求任务

结果可能是这样的日志混合:

[2024-01-01 10:00:01] INFO - task_a:sync_data:连接数据库
[2024-01-01 10:00:02] INFO - task_b:http_request:发送请求
[2024-01-01 10:00:03] INFO - task_a:sync_data:查询数据表
[2024-01-01 10:00:04] ERROR - task_b:http_request:请求超时
[2024-01-01 10:00:05] INFO - task_a:sync_data:数据处理完成

2. 并发执行时的日志污染

问题分析

当多个任务并发执行时,由于共享同一个根日志记录器,日志信息会完全混杂:

# 任务1执行时:
logger.addHandler(handler1)
# 任务2同时执行:
logger.addHandler(handler2)  # 这会影响到任务1的日志捕获!
影响后果
  1. 日志分析困难:无法准确分离各个任务的日志
  2. 调试复杂化:问题排查时需要手动分离日志
  3. 数据不准确:存储的日志可能包含其他任务的信息
  4. 审计问题:执行记录中的日志可能不完整或不准确

3. 改进方案与最佳实践

方案一:使用独立的日志记录器

为每个任务创建独立的日志记录器,避免使用根记录器:

def _run_job_enhanced(self, job, jobstore_alias, run_times, logger_name):
    """改进版:使用独立的日志记录器"""
    import apscheduler.executors.base

    # 创建独立的日志记录器,而不是使用根记录器
    job_logger = logging.getLogger(f"apscheduler.job.{job.id}")

    # 创建独立的日志流
    stream = StringIO()
    handler = logging.StreamHandler(stream)
    formatter = logging.Formatter(
        fmt='[%(asctime)s] %(levelname)s - %(name)s:%(funcName)s in %(lineno)d : %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    handler.setFormatter(formatter)

    # 只添加到当前任务的日志记录器
    job_logger.addHandler(handler)
    job_logger.setLevel(logging.DEBUG)

    try:
        # 调用原始函数
        original_func = getattr(apscheduler.executors.base, '_run_job_original')
        retval = original_func(job, jobstore_alias, run_times, logger_name)
    except Exception as e:
        job_logger.error(f"Error in job execution: {e}")
        raise
    finally:
        # 移除日志处理器
        job_logger.removeHandler(handler)

        # 记录日志到实例的_logs属性中
        content = stream.getvalue().strip()
        if content:
            self._logs[job.id] = content

    return retval
方案二:使用上下文管理器

创建上下文管理器来确保日志捕获的隔离性:

from contextlib import contextmanager

@contextmanager
def job_log_capture(job_id):
    """上下文管理器:隔离任务日志捕获"""
    stream = StringIO()
    handler = logging.StreamHandler(stream)

    # 创建独立的日志记录器
    job_logger = logging.getLogger(f"apscheduler.job.{job_id}")
    job_logger.addHandler(handler)
    job_logger.setLevel(logging.DEBUG)

    # 临时替换任务函数中的日志记录器
    original_loggers = {}

    try:
        yield stream
    finally:
        job_logger.removeHandler(handler)
        # 恢复原始日志记录器

# 使用方式
def _run_job_enhanced(self, job, jobstore_alias, run_times, logger_name):
    with job_log_capture(job.id) as stream:
        # 执行任务...
        pass
方案三:基于日志过滤器的隔离

使用自定义的日志过滤器来实现更精细的日志隔离:

class JobLogFilter(logging.Filter):
    """任务日志过滤器"""
    def __init__(self, job_id):
        self.job_id = job_id

    def filter(self, record):
        # 只接受与当前任务相关的日志记录
        return getattr(record, 'job_id', None) == self.job_id

def _run_job_enhanced(self, job, jobstore_alias, run_times, logger_name):
    """使用过滤器的改进版"""
    import apscheduler.executors.base

    stream = StringIO()
    handler = logging.StreamHandler(stream)
    handler.addFilter(JobLogFilter(job.id))

    # 获取任务特定的日志记录器
    job_logger = logging.getLogger(f"job.{job.id}")
    job_logger.addHandler(handler)

    try:
        original_func = getattr(apscheduler.executores.base, '_run_job_original')
        retval = original_func(job, jobstore_alias, run_times, logger_name)
    finally:
        job_logger.removeHandler(handler)
        content = stream.getvalue().strip()
        if content:
            self._logs[job.id] = content

    return retval

4. 项目改进建议

  1. 修改日志记录器名称:在任务函数中使用固定的日志记录器名称
# 推荐的做法
def example_task():
    logger = logging.getLogger('apscheduler.jobs.example')
    logger.info('Task started')
    # 任务逻辑
  1. 配置日志隔离:在任务配置中指定日志记录器名称
JOBS = [
    {
        'id': 'example-task',
        'name': 'Example Task',
        'func': 'src.tasks:example_task',
        'kwargs' : {'logger_name': 'apscheduler.jobs.example'}  # 指定日志记录器
        'trigger': 'interval',
        'seconds': 30
    }
]

5. 最佳实践总结

日志记录最佳实践
# ✅ 推荐:使用明确的日志记录器名称
task_logger = logging.getLogger('apscheduler.jobs.data_sync')

# ❌ 避免:使用根日志记录器
logger = logging.getLogger()

# ✅ 推荐:在日志中包含任务标识
task_logger.info(f"[{job_id}] Task started")

# ✅ 推荐:使用结构化日志
task_logger.info("Task executed", extra={
    'job_id': job_id,
    'task_name': task_name,
    'duration': duration
})
配置建议
# 推荐的日志配置
LOGGING_CONFIG = {
    'version': 1,
    'loggers': {
        'apscheduler.jobs': {
            'level': 'INFO',
            'handlers': ['job_handler'],
            'propagate': False  # 防止日志传播到根记录器
        }
    }
}

6. 完善代码

import pickle

from src.extensions import db


class ExecutedJob(db.Model):
    __tablename__ = 'executed_job'

    # 任务执行记录主键
    id = db.Column(db.Integer, primary_key=True)
    # 任务主键
    task_id = db.Column(db.String(128), nullable=False)
    # 任务名称
    task_name = db.Column(db.String(128), nullable=False)
    # 任务触发器 (JSON)
    task_trigger = db.Column(db.JSON, nullable=False)
    # 任务函数
    task_func = db.Column(db.String(128), nullable=False)
    # 任务参数 (JSON)
    task_params = db.Column(db.JSON)
    # 任务执行状态: submitted, success, failed, missed, unknown
    status = db.Column(db.String(32), nullable=False)
    # 任务开始执行时间
    run_time = db.Column(db.DateTime)
    # 任务结束执行时间
    end_time = db.Column(db.DateTime)
    # 任务执行时长
    duration = db.Column(db.Integer)
    # 任务执行中异常的格式化回溯
    traceback = db.Column(db.Text)
    # 任务中执行日志
    message = db.Column(db.Text)
    # 任务执行结果
    _retval = db.Column('retval', db.LargeBinary)

    @property
    def retval(self):
        return pickle.loads(self._retval) if self._retval else None

    @retval.setter
    def retval(self, value):
        self._retval = pickle.dumps(value) if value else None

    def getter_attrs(self):
        """ 获取属性 """
        return {
            'id': self.id,
            'task_id': self.task_id,
            'task_name': self.task_name,
            'task_func': self.task_func,
            'status': self.status if self.status else 'unknown',
            'run_time': self.run_time.strftime('%Y-%m-%d %H:%M:%S') if self.run_time else None,
            'end_time': self.end_time.strftime('%Y-%m-%d %H:%M:%S') if self.end_time else None,
            'duration': round(self.duration, 3) if self.duration else None,
            'traceback': self.traceback,
            'message': self.message,
            'task_params': self.task_params,
            'task_trigger': self.task_trigger
        }
import json
import logging
from io import StringIO
from threading import Lock
from typing import Tuple, Dict, Any, Optional
from datetime import datetime, timedelta, timezone

from flask import Flask
from apscheduler import events
from apscheduler.triggers.base import BaseTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger

from src.models import ExecutedJob


class SchedulerEventListener:
    """
    APScheduler Job事件监听类
    """

    _instance: Optional[Any] = None
    _lock: Lock = Lock()

    def __new__(cls, *args, **kwargs):
        """
        单例模式
        """
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, app: Flask):
        """
        初始化事件监听器
        """
        if not hasattr(self, 'initialized'):
            self._initialized(app)

    def _initialized(self, app: Flask) -> None:
        """
        初始化调度器事件监听器
        """
        self._app = app
        _apscheduler = getattr(self._app, 'apscheduler')
        if not _apscheduler: raise RuntimeError('APScheduler not found in Flask Current Application')
        self._scheduler = _apscheduler
        _db = self._app.extensions.get('sqlalchemy')
        if not _db: raise RuntimeError('SQLAlchemy not found in Flask Current Application')
        self._db = _db
        self._logger = logging.getLogger(self.__class__.__name__)

        # 初始化作业日志存储
        self._logs = {}

        # 初始化任务信息缓存,用于存储已提交任务的基本信息
        self._job_cache = {}

        # 在初始化时进行作业日志拦截替换
        self._intercept_job_logging()

        # event handler function mapping
        self._event_handlers = {
            events.SchedulerEvent: self._on_scheduler_event,
            events.JobEvent: self._on_job_event,
            events.JobSubmissionEvent: self._on_job_submission_event,
            events.JobExecutionEvent: self._on_job_executed_event
        }

        # event describe mapping
        self._event_mappings = {
            events.EVENT_SCHEDULER_STARTED: 'Scheduler started',
            events.EVENT_SCHEDULER_SHUTDOWN: 'Scheduler shutdown',
            events.EVENT_SCHEDULER_PAUSED: 'Scheduler paused',
            events.EVENT_SCHEDULER_RESUMED: 'Scheduler resumed',
            events.EVENT_EXECUTOR_ADDED: 'Executor added',
            events.EVENT_EXECUTOR_REMOVED: 'Executor removed',
            events.EVENT_JOBSTORE_ADDED: 'Job storage added',
            events.EVENT_JOBSTORE_REMOVED: 'Job storage removed',
            events.EVENT_ALL_JOBS_REMOVED: 'All jobs removed',
            events.EVENT_JOB_ADDED: 'Job added',
            events.EVENT_JOB_REMOVED: 'Job removed',
            events.EVENT_JOB_MODIFIED: 'Job modified',
            events.EVENT_JOB_SUBMITTED: 'Job submitted',
            events.EVENT_JOB_MAX_INSTANCES: 'Job reached maximum instances',
            events.EVENT_JOB_EXECUTED: 'Job executed',
            events.EVENT_JOB_ERROR: 'Job exception',
            events.EVENT_JOB_MISSED: 'Job missed'
        }

        # tag initialization
        self.initialized = True

    def event_listener(self, event):
        """
        事件监听器主方法,使用字典映射处理所有APScheduler事件

        Args:
            event: APScheduler事件对象
        """
        handler = self._event_handlers.get(type(event))
        if not handler:
            self._logger.warning(f"No event handler found: {event.code}")

        handler(event)

    def _intercept_job_logging(self):
        """在实例化时进行作业日志拦截替换"""
        import apscheduler.executors.base
        import apscheduler.executors.pool

        # 保存原始函数
        if not hasattr(apscheduler.executors.base, '_run_job_original'):
            apscheduler.executors.base._run_job_original = apscheduler.executors.base.run_job

        # 替换base模块中的函数
        apscheduler.executors.base.run_job = self._run_job_enhanced

        # 关键:同时替换pool模块中的引用
        apscheduler.executors.pool.run_job = self._run_job_enhanced

        self._logger.info("Intercepted run_job function in base and pool modules")

    def _run_job_enhanced(self, job, jobstore_alias, run_times, logger_name):
        """增强版的run_job函数,用于拦截日志"""
        import apscheduler.executors.base

        # 创建独立的日志流
        stream = StringIO()
        handler = logging.StreamHandler(stream)
        formatter = logging.Formatter(
            fmt='[%(asctime)s] %(levelname)s - %(name)s:%(funcName)s in %(lineno)d : %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        handler.setFormatter(formatter)

        # 指定日志记录器 (根日志)
        logger = logging.getLogger()
        # 添加日志处理器
        logger.addHandler(handler)

        try:
            # 调用原始函数,避免递归
            original_func = getattr(apscheduler.executors.base, '_run_job_original')
            retval = original_func(job, jobstore_alias, run_times, logger_name)
        except Exception as e:
            # 记录异常信息到日志流
            logger.error(f"Error in job execution: {e}")
            raise
        finally:
            # 移除日志处理器
            logger.removeHandler(handler)

            # 记录日志到实例的_logs属性中,以作业ID为key
            content = stream.getvalue().strip()
            if content:
                self._logs[job.id] = content
                self._logger.debug(f"Stored job log for {job.id}: {len(content)} characters")
            else:
                self._logger.debug(f"No log content captured for job {job.id}")

        return retval

    def _on_scheduler_event(self, event: events.SchedulerEvent) -> None:
        """
        调度器事件处理方法

        Args:
            event: SchedulerEvent 事件对象
        """
        self._logger.debug(f"Scheduler event: {self._event_mappings.get(event.code)} (Code: {event.code})")

    def _on_job_event(self, event: events.JobEvent) -> None:
        """
        作业事件处理方法

        Args:
            event: JobEvent 事件对象
        """
        self._logger.debug(f"Job event: {self._event_mappings.get(event.code)} (Code: {event.code}, Job: {event.job_id})")

    def _on_job_submission_event(self, event: events.JobSubmissionEvent) -> None:
        """
        作业已提交给调度程序事件处理方法

        Args:
            event: JobSubmissionEvent 事件对象
        """
        self._logger.debug(f"Job submitted event: {self._event_mappings.get(event.code)} (Code: {event.code}, Job: {event.job_id})")
        if event.code != events.EVENT_JOB_SUBMITTED:
            self._logger.warning(f"Maximum number of task instances has been reached, check the job configuration (Job: {event.job_id})")
            return

        try:
            job = self._scheduler.get_job(event.job_id)
            if job is None:
                self._logger.warning(f"Job {event.job_id} not found in scheduler during submission event")
                return

            # 获取作业信息并缓存
            job_info = self._get_job_info(job.id)
            self._job_cache[event.job_id] = job_info

            # 创建执行记录并保存
            record = self._create_executed_job_record(job_info, datetime.now(timezone.utc), 'submitted')
            success = self._save_record_to_db(record, 'add')

            if success:
                self._logger.debug(f"Successfully recorded job submission for {job.id}")
            else:
                self._logger.error(f"Failed to save job submission record for {job.id}")

        except Exception as e:
            self._logger.error(f"Failed to submit job: {e}")

    def _on_job_executed_event(self, event: events.JobExecutionEvent) -> None:
        """
        作业已执行事件处理方法

        Args:
            event: JobExecutionEvent 事件对象
        """
        self._logger.debug(f"Job executed event: {self._event_mappings.get(event.code)} (Code: {event.code}, Job: {event.job_id})")
        try:
            # 获取作业信息
            job_info = self._get_job_info(event.job_id, event)

            # 查找已存在的记录
            existing_record = self._find_existing_record(job_info['id'], event.scheduled_run_time)

            if existing_record and existing_record.status == 'submitted':
                # 更新现有记录
                record = existing_record
                self._logger.debug(f"Updating existing submitted record for job {job_info['id']}")
                self._update_executed_job_record(record, job_info, event)
                success = self._save_record_to_db(record, 'merge')
            else:
                # 创建新记录
                self._logger.debug(f"Creating new execution record for job {job_info['id']}")
                record = self._create_executed_job_record(job_info, event.scheduled_run_time, 'unknown')
                self._update_executed_job_record(record, job_info, event)
                success = self._save_record_to_db(record, 'add')

            if success:
                self._logger.debug(f"Successfully recorded execution event for job {job_info['id']} with status {record.status}")

                # 清理缓存中的任务信息(对于立即执行的任务)
                trigger_info = job_info.get('trigger', {})
                if event.job_id in self._job_cache and trigger_info and trigger_info.get('trigger') == 'date':
                    del self._job_cache[event.job_id]
                    self._logger.debug(f"Cleaned up cached info for immediate job {event.job_id}")
            else:
                self._logger.error(f"Failed to save execution record for job {job_info['id']}")

        except Exception as e:
            self._logger.error(f"Failed to record job execution event: {e}")

    def _parsing_triggers(self, trigger: BaseTrigger) -> Dict[str, Any]:
        """ 解析触发器 """
        data = {}
        if isinstance(trigger, DateTrigger):
            data['trigger'] = 'date'
            if trigger.run_date: data['run_date'] = trigger.run_date.isoformat()

        elif isinstance(trigger, CronTrigger):
            data['trigger'] = 'cron'

            for field in trigger.fields:
                if not field.is_default: data[field.name] = str(field)

        elif isinstance(trigger, IntervalTrigger):
            data['trigger'] = 'interval'

            w, d, hh, mm, ss = self._extract_timedelta(trigger.interval)

            if w > 0: data["weeks"] = w
            if d > 0: data["days"] = d
            if hh > 0: data["hours"] = hh
            if mm > 0: data["minutes"] = mm
            if ss > 0: data["seconds"] = ss

        else:
            data['trigger'] = trigger.__class__.__name__

        # 时间处理
        for k in ('run_date', 'start_date', 'end_date'):
            if hasattr(trigger, k) and getattr(trigger, k):
                data[k] = getattr(trigger, k).strftime("%Y-%m-%d %H:%M:%S")

        # 公共属性
        if hasattr(trigger, 'jitter') and getattr(trigger, 'jitter'): data['jitter'] = getattr(trigger, 'jitter')

        return data

    def _extract_timedelta(self, delta: timedelta) -> Tuple[int, ...]:
        """ 提取时间间隔 """
        if delta.total_seconds() == 0:
            self._logger.warning(f"\nJob max instances: ...\n")
            return 0, 0, 0, 0, 0

        w, d = divmod(delta.days, 7)
        mm, ss = divmod(delta.seconds, 60)
        hh, mm = divmod(mm, 60)
        return w, d, hh, mm, ss

    def _save_record_to_db(self, record: ExecutedJob, operation: str = 'add') -> bool:
        """
        保存记录到数据库

        Args:
            record: 要保存的记录
            operation: 操作类型 ('add' 或 'merge')

        Returns:
            bool: 操作是否成功
        """
        with self._app.app_context():
            session = self._db.session
            try:
                if operation == 'merge':
                    session.merge(record)
                else:
                    session.add(record)
                session.commit()
                self._logger.debug(f"Successfully {operation}ed record for job {record.task_id}")
                return True
            except Exception as e:
                session.rollback()
                self._logger.error(f"Database operation failed for job {record.task_id}: {e}")
                return False
            finally:
                session.close()

    def _find_existing_record(self, job_id: str, run_time: datetime) -> Optional[ExecutedJob]:
        """
        查找已存在的执行记录

        Args:
            job_id: 任务ID
            run_time: 运行时间

        Returns:
            ExecutedJob 或 None
        """
        with self._app.app_context():
            session = self._db.session
            try:
                existing_record = session.query(ExecutedJob).filter_by(
                    task_id=job_id,
                    run_time=run_time
                ).order_by(ExecutedJob.id.desc()).first()
                return existing_record
            finally:
                session.close()

    def _serialize_job_params(self, job_args: list, job_kwargs: dict) -> dict:
        """
        序列化作业参数

        Args:
            job_args: 位置参数
            job_kwargs: 关键字参数

        Returns:
            序列化后的参数字典
        """
        _params = {}
        try:
            _sequencer = lambda x: str(x)
            if job_args: _params['args'] = json.dumps(job_args, default=_sequencer)
            if job_kwargs: _params['kwargs'] = json.dumps(job_kwargs, default=_sequencer)
        except Exception as e:
            self._logger.error(f"Failed to parse job parameters: {e}")
        return _params

    def _get_job_execution_result(self, event: events.JobExecutionEvent) -> tuple:
        """
        获取作业执行结果

        Args:
            event: JobExecutionEvent 事件对象

        Returns:
            tuple: (status, retval, traceback)
        """
        if event.code == events.EVENT_JOB_EXECUTED:
            status = 'success'
            retval = event.retval
            traceback = None

        elif event.code == events.EVENT_JOB_ERROR:
            status = 'failed'
            retval = None
            traceback = event.traceback

        elif event.code == events.EVENT_JOB_MISSED:
            status = 'missed'
            retval = None
            traceback = 'execution time has been missed'

        else:
            status = 'unknown'
            retval = None
            traceback = f'Unknown event code: {event.code}'

        return status, retval, traceback

    def _get_job_info(self, job_id: str, event: Optional[events.JobExecutionEvent] = None) -> dict:
        """
        获取作业信息(从调度器或缓存)

        Args:
            job_id: 任务ID
            event: JobExecutionEvent 事件对象(可选)

        Returns:
            作业信息字典
        """
        job = self._scheduler.get_job(job_id)

        if job is None:
            # 作业可能已被删除,使用缓存的信息
            self._logger.debug(f"Job {job_id} not found in scheduler, using cached data")

            # 首先尝试从缓存获取任务信息
            cached_job = self._job_cache.get(job_id)
            if cached_job:
                return cached_job
            else:
                # 缓存中没有,使用事件数据和默认值
                return {
                    'id': job_id,
                    'name': f"immediate_{job_id}",
                    'func': 'unknown',
                    'args': [],
                    'kwargs': {},
                    'trigger': {'trigger': 'date', 'run_date': event.scheduled_run_time.isoformat() if event and event.scheduled_run_time else None}
                }
        else:
            # 从调度器获取作业信息
            return {
                'id': job.id,
                'name': job.name if job.name else 'unnamed',
                'func': job.func_ref if job.func_ref else 'unknown',
                'args': job.args if job.args else [],
                'kwargs': job.kwargs if job.kwargs else {},
                'trigger': self._parsing_triggers(job.trigger) if job.trigger else {'trigger': 'unknown'}
            }

    def _create_executed_job_record(self, job_info: dict, run_time: datetime, status: str = 'submitted') -> ExecutedJob:
        """
        创建执行记录

        Args:
            job_info: 作业信息
            run_time: 运行时间
            status: 状态

        Returns:
            ExecutedJob 记录
        """
        record = ExecutedJob()
        record.task_id = job_info['id']
        record.task_name = job_info['name']
        record.task_func = job_info['func']
        record.task_args = job_info['args']
        record.task_kwargs = job_info['kwargs']
        record.task_trigger = job_info['trigger']
        record.status = status
        record.run_time = run_time
        return record

    def _update_executed_job_record(self, record: ExecutedJob, job_info: dict, event: events.JobExecutionEvent) -> None:
        """
        更新执行记录

        Args:
            record: 要更新的记录
            job_info: 作业信息
            event: JobExecutionEvent 事件对象
        """
        # 设置执行结果参数
        record.task_params = self._serialize_job_params(job_info['args'], job_info['kwargs'])

        # 计算执行时间
        _end_time = datetime.now(timezone.utc)
        record.end_time = _end_time
        record.duration = (_end_time - event.scheduled_run_time).total_seconds()

        # 设置执行状态和结果
        status, retval, traceback = self._get_job_execution_result(event)
        record.status = status
        if retval is not None:
            record.retval = retval

        if traceback is not None:
            record.traceback = traceback

        # 从实例的_logs属性中获取作业执行日志
        record.message = self._logs.get(event.job_id, None)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值