📝 记录:APScheduler热重载修复(sync_from_db问题)
This commit is contained in:
129
ATRI My Dear Moments/questions/2026-05-06.md
Normal file
129
ATRI My Dear Moments/questions/2026-05-06.md
Normal 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*
|
||||||
Reference in New Issue
Block a user