事故(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: 补充完成任务判定与崩溃恢复约定
99 lines
4.1 KiB
Python
99 lines
4.1 KiB
Python
"""FastAPI 应用入口。
|
||
|
||
负责组装数据库、进程内节点注册表、调度器与静态前端,并在应用生命周期内
|
||
管理后台线程的启动与清理。单体版所有节点在同一进程内直接调用。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
# 先加载 .env(含 LLM API Key 等本地配置),再导入读取环境变量的 config。
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv()
|
||
|
||
import os # noqa: E402
|
||
from contextlib import asynccontextmanager # noqa: E402
|
||
from datetime import datetime, timezone # noqa: E402
|
||
from pathlib import Path # noqa: E402
|
||
|
||
from fastapi import FastAPI # noqa: E402
|
||
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
|
||
from fastapi.staticfiles import StaticFiles # noqa: E402
|
||
|
||
from wov_app import registry
|
||
from wov_app.batch import BatchWorker
|
||
from wov_app.config import BATCH_ENABLED, DB_PATH, STORAGE_DIR, WORKSPACE_ROOT
|
||
from wov_app.db import Database
|
||
from wov_app.maintenance import OrphanCleaner
|
||
from wov_app.routers import apps, batch, workflows
|
||
from wov_app.scheduler import WorkflowScheduler
|
||
from wov_app.seed import seed_default_workflows
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
"""应用生命周期:启动时初始化数据、注册节点和后台服务,退出时回收资源。"""
|
||
# 确保数据与存储目录存在,避免首次启动写文件失败。
|
||
db = Database(DB_PATH)
|
||
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||
# 静态注册全部内置节点到进程内注册表(内存态,不落库)。
|
||
registry.register_all()
|
||
# 默认创建演示工作流,可关闭便于测试。
|
||
if os.getenv("WOV_AUTO_SEED", "1") == "1":
|
||
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":
|
||
scheduler.start()
|
||
cleaner = OrphanCleaner(db, STORAGE_DIR)
|
||
# 孤儿数据清理默认开启,定时清除死数据;测试可关闭。
|
||
if os.getenv("WOV_CLEANUP_ENABLED", "1") == "1":
|
||
cleaner.start()
|
||
# 批量处理引擎:处理用户所选文件夹的批量任务(source=batch 的运行),
|
||
# 与主调度器互不抢占;默认开启,测试可关闭。
|
||
batch_worker = BatchWorker(db)
|
||
if os.getenv("WOV_BATCH_ENABLED", "1") == "1":
|
||
batch_worker.start()
|
||
# 共享对象挂到 app.state,路由通过 Depends 延迟获取。
|
||
app.state.db = db
|
||
app.state.scheduler = scheduler
|
||
app.state.cleaner = cleaner
|
||
app.state.batch = batch_worker
|
||
yield
|
||
# 退出时先停批量引擎、调度器与清理器,避免残留后台线程。
|
||
batch_worker.stop()
|
||
cleaner.stop()
|
||
scheduler.stop()
|
||
|
||
|
||
app = FastAPI(title="VRSub API(单体版)", version="0.1.0", lifespan=lifespan)
|
||
|
||
# MVP 阶段不做鉴权,允许跨域便于本地调试与静态页面访问。
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
app.include_router(workflows.router)
|
||
app.include_router(apps.router)
|
||
app.include_router(batch.router)
|
||
|
||
|
||
@app.get("/health")
|
||
def health() -> dict:
|
||
"""进程存活探针,供部署环境与前端检测后端可用性。"""
|
||
return {"status": "ok", "service": "wov-api", "mode": "monolith"}
|
||
|
||
|
||
# 静态前端目录位于仓库根下的 web,由 FastAPI 直接挂载。
|
||
FRONTEND_DIR = WORKSPACE_ROOT / "web"
|
||
app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")
|