- 运行句柄按 run_id 归属,修复取消后重跑时闸门被旧运行删除的竞态 - 调度循环改用取消令牌退出,启动时对账遗留运行,校验 rows_affected - 三类任务统一参与调度,新增 /api/v1/stats 与 /api/v1/schedule - SQLite 启用 WAL 与 busy_timeout,日志清理跳过活跃文件,单行上限 256 KiB - 前端缓存管理令牌并在 403 时重试一次,仪表盘改用统计接口
42 lines
3.1 KiB
Markdown
42 lines
3.1 KiB
Markdown
# ADR-0007:运行期可靠性优化
|
||
|
||
- 状态:已接受
|
||
- 日期:2026-10-25
|
||
|
||
## 背景
|
||
|
||
代码评审与端到端自检暴露出五类问题,都会让"看起来正常"的功能在边界条件下失真:
|
||
|
||
1. 取消任务时执行器直接删除 `running` 句柄,而旧运行的收尾逻辑又无条件删除同任务句柄。取消后立刻重跑时,旧运行会把新运行的闸门删掉,跳过策略失效,同一任务可能并发执行。
|
||
2. 调度器用 `JoinHandle::abort()` 取消循环。若恰好卡在 `executor.trigger()` 的数据库 await 中,`running` 表项和 `queued` 运行会永久残留,该任务此后永远被判定为"运行中"并跳过。
|
||
3. 进程被强杀后,数据库里的 `queued`/`running` 运行没有对账,前端会一直显示"运行中"并持续轮询。
|
||
4. `mark_running`/`complete` 不检查 `rows_affected`,运行记录被级联删除后收尾静默失效。
|
||
5. 调度器只注册脚本任务,HTTP 与进程任务"启用"后返回 200 却永远不会被定时触发。
|
||
|
||
## 决策
|
||
|
||
1. 句柄带运行 ID,收尾使用 `remove_running_if(task_id, run_id)`:只有仍属于本次运行的句柄才会被移除;取消时依旧立即释放闸门,允许马上重跑。
|
||
2. 调度循环只用 `CancellationToken` 退出,不再 `abort`;`ScheduledTask` 的 `JoinHandle` 仅用于保活。
|
||
3. 启动时执行 `fail_interrupted_runs`:把遗留的 `queued`/`running` 统一改为 `cancelled`,并写入"进程重启,运行被中断"。
|
||
4. `mark_running`/`complete` 返回是否更新成功,执行器对 0 行更新记警告并同步清理内存句柄。
|
||
5. 三类 Action 统一参与调度:`list_enabled_tasks` 不再按 `action_type` 过滤,`Scheduler::refresh` 只判断 `enabled`。
|
||
6. SQLite 启用 WAL + `busy_timeout(5s)` + `synchronous=NORMAL`,避免执行器写日志时前端查询撞上 `database is locked`。
|
||
7. 日志清理跳过正在写入的文件;单行输出上限 256 KiB,超出后截断并继续读行尾。
|
||
8. 请求体上限 2 MiB;新增 `/api/v1/stats`(一次查询拿到全部计数)与 `/api/v1/schedule`(下次触发时间)。
|
||
9. 前端缓存会话令牌,写请求收到 403 时丢弃缓存并重试一次(覆盖后端重启轮换令牌)。
|
||
10. Python 任务默认注入 `PYTHONIOENCODING=utf-8`(用户显式设置优先),`PYTHONUNBUFFERED=1` 保持强制。
|
||
|
||
## 原因
|
||
|
||
- 句柄竞态与 `abort` 泄漏都会造成"任务不再执行"且无任何报错,是排障成本最高的一类缺陷。
|
||
- 对账让崩溃恢复变成确定行为,前端轮询也能正常收敛。
|
||
- 统一调度消除了"启用但永不触发"的假能力。
|
||
- 统计与调度接口把仪表盘从 4 次请求降到 2 次,并让"下次执行时间"成为可见信息。
|
||
|
||
## 边界
|
||
|
||
- 取消后立即可重跑,意味着旧进程的终止与新进程的启动可能短暂重叠(取决于 Job Object 终止速度)。
|
||
- 没有全局并发上限:多个任务在同一时刻触发会各自启动一个进程。
|
||
- 对账只针对 `queued`/`running`,不会尝试恢复重试或补偿错过的调度。
|
||
- 单行截断后该行内容不完整,日志里会多一条 `[system]` 说明。
|