Files

130 lines
3.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🐞 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*