fix: 拒绝环形 DAG 并避免无效工作流堵塞任务队列

This commit is contained in:
2026-09-11 17:19:40 +08:00
parent 6eb65e4356
commit f99f8de171
5 changed files with 242 additions and 9 deletions
+15 -1
View File
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException
from wov_app.db import Database
from wov_app.schemas import WorkflowCreate
from wov_app.scheduler import topological_sort
from wov_sdk.models import WorkflowDefinition
router = APIRouter(prefix="/api/admin/workflows", tags=["workflows"])
@@ -32,10 +33,17 @@ def _slugify(value: str) -> str:
def _validate_definition(raw: dict) -> WorkflowDefinition:
"""解析并校验 DAG 定义,非法时转换为 422 HTTP 异常。"""
"""解析并校验 DAG 定义,非法时转换为 422 HTTP 异常。
除 `WorkflowDefinition.validate()` 的结构校验(名称/版本/节点 ID 唯一/
边引用存在)之外,还要求 DAG **可拓扑排序**:环形依赖虽然结构上合法,
但执行时无法确定节点顺序,必须拒绝保存与发布(R04)。
"""
try:
definition = WorkflowDefinition.from_dict(raw)
definition.validate()
# 环形 DAG(含自环)在这里以 "workflow contains a cycle" 被拒绝。
topological_sort(definition)
return definition
except (KeyError, TypeError, ValueError) as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@@ -119,6 +127,12 @@ def publish_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
raise HTTPException(status_code=404, detail="workflow not found")
if workflow["latest_version"] == 0:
raise HTTPException(status_code=422, detail="workflow has no version")
# 发布前重新校验待发布版本:历史遗留的无效定义(例如修复前保存的环形 DAG)
# 不能进入用户应用中心,否则创建出来的任务会在执行期失败(R04)。
latest = db.get_latest_workflow_version(workflow_id)
if latest is None:
raise HTTPException(status_code=422, detail="workflow has no version")
_validate_definition(latest["definition"])
# 发布只是状态切换,不修改已保存的版本数据。
db.upsert_workflow(
{
+18 -3
View File
@@ -150,9 +150,24 @@ class WorkflowScheduler:
return
# 解析并校验 DAG,随后计算拓扑执行顺序。
definition = WorkflowDefinition.from_dict(version["definition"])
definition.validate()
ordered = topological_sort(definition)
#
# 任何预检异常(缺字段/边引用不存在/环形依赖)都必须在这里把任务标
# FAILED:修复前这段在 try 之外,异常直接冒到 _loop 被吞掉,任务停在
# QUEUEDnext_queued_run 每轮拾起同一条队首记录,后续任务全部堵塞
# (R04:环形 DAG 卡死队列)。历史无效版本无法删除,只能就地判失败。
try:
definition = WorkflowDefinition.from_dict(version["definition"])
definition.validate()
ordered = topological_sort(definition)
except Exception as exc: # noqa: BLE001
logger.exception("任务 %s 工作流定义无效,标记失败: %s", run_id, exc)
self.db.update_run(
run_id,
status="FAILED",
error=str(exc),
updated_at=_now_iso(),
)
return
# 从已登记产物重建已完成节点的输出,支持暂停后断点续跑。
outputs_by_node = self.db.restore_run_outputs(run_id)
run_started = time.monotonic()