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
+85
View File
@@ -748,3 +748,88 @@ def test_worker_clears_stale_keep_model_flag_before_stage(tmp_path: Path, monkey
# 验证结果:prep(非 LLM)看不到残留标志;translate(LLM 阶段)才写入;
# post(非 LLM)不再看到它。
assert flag_trace == [("prep", False), ("translate", True), ("post", False)]
def test_job_hidden_from_engine_until_details_written(tmp_path: Path, monkeypatch) -> None:
"""建任务期间任务对引擎不可见:明细写完前不被拾起,避免半成品被标完成。
曾因任务行先入库、524 条明细后写,引擎在明细写一半时拾起任务、收尾时快照
里没有剩余明细,把任务误标 COMPLETED(视频永远不再被处理)。
"""
# 数据:3 个待处理视频的媒体库。
folder = tmp_path / "videos"
for name in ("a.mp4", "b.mp4", "c.mp4"):
_make_video(folder / name)
db = _published_db(tmp_path)
probes: list[str | None] = []
original = db.create_batch_video
def _probe_after_insert(item: dict) -> None:
"""每写完一条明细,立刻问一次引擎队列(模拟轮询线程的拾取时机)。"""
original(item)
picked = db.next_queued_batch_job()
probes.append(picked["id"] if picked else None)
monkeypatch.setattr(db, "create_batch_video", _probe_after_insert)
# 测试过程
job_id = create_job(db, str(folder), "wf", recursive=False)
# 验证结果:写入过程中引擎始终取不到任务;写完后才是 QUEUED 且明细完整。
assert probes == [None, None, None]
job = db.get_batch_job(job_id)
assert job["status"] == "QUEUED"
assert job["total"] == 3
assert len(db.list_batch_videos(job_id)) == 3
assert db.next_queued_batch_job()["id"] == job_id
def test_create_job_marks_failed_when_detail_insert_breaks(tmp_path: Path, monkeypatch) -> None:
"""明细写入中途失败时任务记 FAILED 并留下错误,不产生看不见的残留任务。"""
# 数据:2 个视频,第二条明细写入时抛异常。
folder = tmp_path / "videos"
for name in ("a.mp4", "b.mp4"):
_make_video(folder / name)
db = _published_db(tmp_path)
original = db.create_batch_video
calls = {"n": 0}
def _fail_second(item: dict) -> None:
calls["n"] += 1
if calls["n"] == 2:
raise RuntimeError("磁盘写满")
original(item)
monkeypatch.setattr(db, "create_batch_video", _fail_second)
# 测试过程 + 验证结果:异常继续抛出,任务可被观察到且为 FAILED。
with pytest.raises(RuntimeError):
create_job(db, str(folder), "wf", recursive=False)
job = db.list_batch_jobs(limit=10)[0]
assert job["status"] == "FAILED"
assert "磁盘写满" in (job["error"] or "")
assert db.next_queued_batch_job() is None
def test_worker_start_fails_leftover_creating_job(tmp_path: Path) -> None:
"""进程中断留下的 CREATING 任务在引擎启动时记为 FAILED,不静默残留。"""
# 数据:一条只登记到一半的任务(模拟明细写入中途进程被杀/热重载)。
db = _published_db(tmp_path)
db.create_batch_job({
"id": "batch_halfway", "folder_path": "/videos", "workflow_id": "wf",
"recursive": 0, "status": "CREATING", "progress": 0, "total": 0, "done": 0,
"failed": 0, "current_video": None, "error": None,
"created_at": "2026-09-18T00:00:00+00:00", "updated_at": "2026-09-18T00:00:00+00:00",
})
worker = BatchWorker(db, interval_seconds=999)
# 测试过程
worker.start()
try:
# 验证结果:任务变为可见的 FAILED,且不会被引擎当排队任务拾起。
job = db.get_batch_job("batch_halfway")
assert job["status"] == "FAILED"
assert "中断" in (job["error"] or "")
assert db.next_queued_batch_job() is None
finally:
worker.stop()