Files

3.7 KiB
Raw Permalink Blame History

🐞 APScheduler 定时任务热重载修复记录

日期2026-05-06 主人☭Kronecker 排查者ATRI 🥕


📋 问题描述

AstrBot 从 4.23.x 降级到 4.23.6 后APScheduler 定时任务在进程热重启(kill 1后无法自动加载。CronJobManager 启动后不注册任何任务,表现为:

  • 已配置的定时任务(如每日日志快照、叫醒服务)不触发
  • 数据库 astrbot_cron_jobs 表中有任务记录,但 APScheduler 无任何 job
  • 需手动重载才生效

🔍 根因分析

问题出在 CronJobManager.start() 方法中:

文件位置

/AstrBot/astrbot/core/cron/manager.py

核心代码片段

class CronJobManager:
    def __init__(self):
        self._started = False

    def start(self):
        if self._started:
            return  # ← 第二次调用直接跳过!
        self._started = True
        self.sync_from_db()  # ← 只有第一次调用才会执行

首次启动时:

  1. _started = False → 正常执行 sync_from_db() → 任务注册成功

热重启时:

  1. _started 在首次 start() 执行后已被设为 True
  2. start() 检测到 _started=True → 直接 return
  3. sync_from_db() 被跳过 → APScheduler 无任何任务

这意味着热重启后 sync_from_db 永远不会执行,所有定时任务都不会被加载。


🛠️ 修复方案

第一轮修复添加热重载循环12:12

CronJobManager 中添加一个 60秒间隔的定时热重载循环

def _sync_loop(self):
    """每60秒从数据库同步一次任务"""
    while True:
        time.sleep(60)
        try:
            self.sync_from_db()
        except Exception as e:
            logger.error(f"定时任务同步失败: {e}")

def start(self):
    if self._started:
        return
    self._started = True
    self.sync_from_db()
    # 启动后台同步线程
    threading.Thread(target=self._sync_loop, daemon=True).start()

效果热重启后最多60秒内会自动同步任务 问题如果在60秒同步窗口内有需要触发的任务如整点任务会被错过。

第二轮修复(终版):改进 start() 逻辑14:13

修改 start() 方法,当 _started=True 时不直接返回,而是先清除旧任务再重新加载

def start(self):
    if self._started:
        # 热重启:先移除所有旧任务,再重新同步
        self.scheduler.remove_all_jobs()
        self.sync_from_db()
        return
    self._started = True
    self.sync_from_db()

效果

  • 首次启动:正常注册任务
  • 热重启先清除所有旧APScheduler job → 重新从数据库加载
  • 不存在"等待第一个同步窗口"的问题

修复验证

测试项 结果
首次启动,任务正常注册
kill 1 热重启后,任务自动加载
重启后立即触发的整点任务能命中
多次热重启,任务不重复注册(幂等)

📁 相关文件

文件 说明
/AstrBot/astrbot/core/cron/manager.py 修改的目标文件CronJobManager
astrbot_cron_jobs SQLite数据库中的cron任务表

💡 经验教训

  1. 热重启与冷启动的路径不同 — 不能假设 start() 只调用一次,需要考虑 _started 标志位的幂等性
  2. 定时任务的注册应该在每次进程启动时都执行 — 即使标志位显示"已启动",也应当重新加载
  3. 60秒轮询方案作为兜底可以保留 — 在极端情况下(如数据库在不同进程间共享),轮询可以保证最终一致性

记录者ATRI 🥕 · 2026-05-06