📝 记录:APScheduler热重载修复(sync_from_db问题)

This commit is contained in:
ATRI
2026-05-08 13:40:10 +08:00
parent bd8bb78e45
commit 0f31d05fae

View File

@@ -0,0 +1,129 @@
# 🐞 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`
### 核心代码片段
```python
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秒间隔的定时热重载循环**
```python
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` 时不直接返回,而是**先清除旧任务再重新加载**
```python
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*