fix: 批量任务明细写完再排队(CREATING 状态)

`create_job` 先把任务行以 QUEUED 入库(引擎立刻可见),再逐条登记明细(扫描
媒体库时 500+ 条要数秒);引擎轮询到的快照可能还没包含剩余明细,收尾时"无未结束
明细"检查也看不到它们,于是把任务误标 COMPLETED,剩余视频永远不再被处理。

- 任务行改以 `CREATING` 入库,明细全部登记完才置 QUEUED(引擎只取 QUEUED,
  看不到半成品);登记中途异常置 FAILED 并把异常交给路由层。
- 启动时把上一进程遗留的 CREATING 统一置 FAILED(`fail_creating_batch_jobs`),
  避免明细写一半被热重载/强杀后留下看不见的残留任务。
- 回归测试:明细每写一条就问一次引擎队列(写入过程中取不到任务);中途失败记
  FAILED;启动清理遗留 CREATING。
This commit is contained in:
2026-09-18 22:23:39 +08:00
parent 3f4478523e
commit 7a2dee9b64
5 changed files with 159 additions and 24 deletions
+42 -23
View File
@@ -221,7 +221,10 @@ def create_job(
"folder_path": str(folder),
"workflow_id": workflow_id,
"recursive": int(recursive),
"status": "QUEUED",
# CREATING:明细未写完前引擎看不见本任务。逐条登记 500+ 条明细要数秒,
# 若此刻已是 QUEUED,引擎会读到半个快照并在收尾时把任务误标 COMPLETED,
# 剩下的视频就再也不会被处理(自愈分支只碰运气)。写完明细立刻置 QUEUED。
"status": "CREATING",
"progress": 0,
"total": 0,
"done": 0,
@@ -233,27 +236,35 @@ def create_job(
})
pending = 0
skipped = 0
for video in videos:
# 视频所在目录已存在对应字幕文件 → 已处理过,直接跳过不触发流水线。
if list_sidecar_subtitles(video):
status = "SKIPPED"
skipped += 1
else:
status = "PENDING"
pending += 1
video_id = f"bv_{uuid.uuid4().hex[:12]}"
db.create_batch_video({
"id": video_id,
"job_id": job_id,
"video_path": str(video),
# 私有工作空间:storage/batch/<job_id>/<bv_id>/,与媒体库隔离。
"work_dir": str(BATCH_WORK_ROOT / job_id / video_id),
"run_id": None,
"status": status,
"error": None,
"created_at": now,
"updated_at": now,
})
try:
for video in videos:
# 视频所在目录已存在对应字幕文件 → 已处理过,直接跳过不触发流水线。
if list_sidecar_subtitles(video):
status = "SKIPPED"
skipped += 1
else:
status = "PENDING"
pending += 1
video_id = f"bv_{uuid.uuid4().hex[:12]}"
db.create_batch_video({
"id": video_id,
"job_id": job_id,
"video_path": str(video),
# 私有工作空间:storage/batch/<job_id>/<bv_id>/,与媒体库隔离。
"work_dir": str(BATCH_WORK_ROOT / job_id / video_id),
"run_id": None,
"status": status,
"error": None,
"created_at": now,
"updated_at": now,
})
except Exception as exc: # noqa: BLE001
# 明细写到一半失败:记 FAILED 留可见记录(CREATING 状态没人会拾起,
# 沉默的残留任务会让用户以为什么都没发生),然后把异常交给路由层。
db.update_batch_job(
job_id, status="FAILED", error=str(exc), updated_at=_now_iso(),
)
raise
# total = 本批真正需要处理(无字幕)的视频数;已有字幕被 SKIPPED 的
# 不计入总数也不计入完成数——进度条只反映"实际待处理"的这批。
if pending == 0:
@@ -263,7 +274,10 @@ def create_job(
updated_at=_now_iso(),
)
else:
db.update_batch_job(job_id, total=pending, updated_at=_now_iso())
# 明细全部就位后才排队,引擎从此拿到的快照一定是完整的。
db.update_batch_job(
job_id, status="QUEUED", total=pending, updated_at=_now_iso(),
)
logger.info(
"创建批量任务 %s: 文件夹 %s, 工作流 %s, 共 %d 个视频(%d 待处理, %d 已有字幕跳过)",
job_id, folder, workflow_id, len(videos), pending, skipped,
@@ -291,6 +305,11 @@ class BatchWorker:
return
# 独立运行时确保节点已注册;重复注册幂等。
registry.register_all()
# 上一进程中断留下的 CREATING 任务(明细登记中途被杀/热重载)没人会推进,
# 启动时统一记为 FAILED,避免用户以为任务还在创建中。
stale = self.db.fail_creating_batch_jobs("创建明细中断(进程中断),请重新创建任务")
if stale:
logger.warning("启动清理 %d 个未登记完的批量任务(CREATING → FAILED", stale)
self._stopping = False
self._thread = threading.Thread(
target=self._loop,