fix: 批量任务残留PENDING仍误标COMPLETED——置完成前校验无残留+崩溃恢复批量任务

事故(batch_969fabe74b83 等 3 个任务实测):批量引擎串行处理到 9.9GB
大视频时中断,_run_job 无条件收尾把任务置 COMPLETED,留下"N 个 PENDING
待处理却已完成"的僵尸状态——3 个任务遗留的 PENDING 完全相同(kiwvr-887/
mdvr-413/savr-1077 共 10 个大视频),且均卡在大文件前从未被真正处理。

- batch.py: _run_job 置 COMPLETED 前校验全部非 SKIPPED 视频已结束
  (无 PENDING/PAUSED 残留),否则保持 RUNNING 交引擎下一轮续跑
- db.py/main.py: 新增 recover_interrupted_batch_jobs,重启时把 RUNNING
  批量任务恢复 QUEUED(否则停在 RUNNING 永远不会被再次拾起)
- tests: 新增 2 条 TDD 回归(残留 PENDING 不置 COMPLETED、批量崩溃恢复)
- scripts/fix_zombie_batch_jobs.py: 历史僵尸数据修复脚本(置回 QUEUED 续跑)
- AGENTS.md: 补充完成任务判定与崩溃恢复约定
This commit is contained in:
2026-09-07 11:25:33 +08:00
parent 4d2a1912da
commit c063aed1f1
7 changed files with 219 additions and 0 deletions
+23
View File
@@ -408,8 +408,31 @@ class BatchWorker:
# 全部视频处理完成:先用明细实时对齐汇总(done 只计实际完成的,
# 不含 SKIPPED),再置 COMPLETED。
#
# 置 COMPLETED 前必须校验**没有未处理完的视频残留**:若本轮循环因
# 视频处理中断/异常(_process_video 返回但视频仍 PENDING,等同进程
# 在处理中被杀)而没有真正处理完所有 PENDING,就**不能**标完成——
# 否则会出现"明细还有 N 个待处理、任务却已完成"的僵尸状态
# batch_969fabe74b83 等 3 个任务真实发生:引擎串行处理到 9.9GB
# 大视频时中断,10 个视频留 PENDING 却被无条件置 COMPLETED)。
# 此时保持 RUNNING,让引擎下一轮(重启后重新拾起 RUNNING 任务)
# 继续处理剩余 PENDING,全部结束才真正置 COMPLETED。
self.db.sync_batch_job_progress(job_id)
job = self.db.get_batch_job(job_id)
if job is None:
return
leftovers = [
v for v in self.db.list_batch_videos(job_id)
if v["status"] not in ("SKIPPED", "COMPLETED", "FAILED")
]
if leftovers:
# 有未处理完的视频(PENDING/PAUSED/QUEUED 等):保持 RUNNING
# 由引擎下一轮续跑;记录日志便于排查中断位置。
logger.warning(
"批量任务 %s 仍有 %d 个视频未处理完(%s…),保持 RUNNING 待续跑,不置 COMPLETED",
job_id, len(leftovers), Path(leftovers[0]["video_path"]).name,
)
return
done = int(job["done"]) if job else 0
failed = int(job["failed"]) if job else 0
self.db.update_batch_job(
+18
View File
@@ -385,6 +385,24 @@ class Database:
)
return cur.rowcount
def recover_interrupted_batch_jobs(self, updated_at: str) -> int:
"""重启恢复:把遗留 RUNNING 的批量任务恢复为 QUEUED,返回恢复数量。
批量引擎处理视频(尤其大文件)时进程被杀/重启,批量任务会停在
RUNNING:其关联 run 由 recover_interrupted_runs 恢复为 QUEUED,但
批量任务本身若保持 RUNNINGnext_queued_batch_job 只拾取 QUEUED
永远不会重新驱动它 → 未处理完的 PENDING 视频永久残留
batch_969fabe74b83 事故链路之一)。恢复为 QUEUED 后引擎重新拾起,
从断点(剩余 PENDING 视频 + 已恢复的 run)继续处理。用户主动暂停的
PAUSED 批量任务保持不变,等待显式 resume。
"""
with self._connect() as conn:
cur = conn.execute(
"UPDATE batch_jobs SET status = 'QUEUED', updated_at = ? WHERE status = 'RUNNING'",
(updated_at,),
)
return cur.rowcount
def pause_run(self, run_id: str, updated_at: str) -> None:
"""暂停任务:置为 PAUSED;调度器会在节点边界检查并停止推进。"""
with self._connect() as conn:
+4
View File
@@ -42,7 +42,11 @@ async def lifespan(app: FastAPI):
seed_default_workflows(db)
# 重启恢复:上次进程被杀时遗留的 RUNNING 任务恢复为 QUEUED
# 调度器会从产物表断点续跑(不重复已完成节点);PAUSED 保持等待显式 resume。
# 批量任务同样恢复:RUNNING 的批量 job 置回 QUEUED,其关联 run 由
# recover_interrupted_runs 恢复,批量引擎重新拾起后从断点续跑剩余 PENDING
# (不恢复则批量任务停在 RUNNING 永远不会被再次驱动,未处理视频永久残留)。
db.recover_interrupted_runs(datetime.now(timezone.utc).isoformat())
db.recover_interrupted_batch_jobs(datetime.now(timezone.utc).isoformat())
scheduler = WorkflowScheduler(db, STORAGE_DIR)
# 调度器默认开启,处理排队中的任务;测试可关闭后手动执行。
if os.getenv("WOV_SCHEDULER_ENABLED", "1") == "1":