fix(workflow): 修复子流程批量执行缓存键冲突

递归识别子工作流中的视频模型以支持 per_second 计费,并为子流程批量子执行添加 SubFlowScope 隔离 async/segment 缓存键。
This commit is contained in:
2026-09-03 16:50:18 +08:00
parent fc629ee493
commit f7957761f0
4 changed files with 65 additions and 15 deletions
+17 -4
View File
@@ -34,6 +34,17 @@ func FormLambda(ctx context.Context, input any) (any, error) {
return nodeInput, nil
}
// cacheNodeId 缓存键节点 id:scope 非空(子流程批量子执行)时拼上作用域后缀,使各份子执行的
// async/segment 缓存行互相隔离(同一 exec 下多份内层节点 id 相同,不隔离会互相命中/覆盖 done 结果,
// 见《工作流子流程批量缓存隔离设计.md》);顶层 scope 为空 → 原样返回,行为不变。
// 仅用于缓存读写键,节点的 Config.Id / node_execution 记录 / 输出引用一律不受影响。
func cacheNodeId(global *flowDto.FlowExecutionInput, nodeId string) string {
if global == nil || global.SubFlowScope == "" {
return nodeId
}
return nodeId + global.SubFlowScope
}
// ModelLambda 模型调用节点
func ModelLambda(ctx context.Context, input any) (any, error) {
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
@@ -45,6 +56,8 @@ func ModelLambda(ctx context.Context, input any) (any, error) {
if err != nil {
return nil, err
}
// async/segment 缓存键节点 id(子流程批量子执行带 scope,顶层即 Config.Id
cNodeId := cacheNodeId(nodeInput.Global, nodeInput.Config.Id)
// 2. 前置工具:决定模型调用入参(单次/多次)
// 入参统一为扁平模型请求体(BuildModelRequestBody 输出,key 为点分路径)。
@@ -79,7 +92,7 @@ func ModelLambda(ctx context.Context, input any) (any, error) {
// 续跑(!ForceNewRun)时读取该节点已成功段;全新执行不查(BuildExecution 已清旧段),saved 为 nil → 全量重生成
var saved map[int]entity.SegmentRef
if !nodeInput.Global.ForceNewRun && segVideo {
saved, err = flowDao.FlowSegmentResultDao.ListByNode(ctx, nodeInput.Global.ExecutionId, nodeInput.Config.Id)
saved, err = flowDao.FlowSegmentResultDao.ListByNode(ctx, nodeInput.Global.ExecutionId, cNodeId)
if err != nil {
return nil, err
}
@@ -102,7 +115,7 @@ func ModelLambda(ctx context.Context, input any) (any, error) {
defer wg.Done()
// 每段单次调用,不原地重试:段失败即走节点失败收口(HandleFailedNodeExecution → Interrupt),
// 下次 reExecute 由 planSegmentResume 复用已成功段、仅重生成失败段
results[i], tokenRes[i], isInference[i], errs[i] = ModelCallResultLambda(ctx, nodeInput.Config.ModelConfig.ModelId, nodeInput.Global.SessionId, params, nodeInput.Config.Prompt, nodeInput.Global.ExecutionId, nodeInput.Config.Id, idxList[i])
results[i], tokenRes[i], isInference[i], errs[i] = ModelCallResultLambda(ctx, nodeInput.Config.ModelConfig.ModelId, nodeInput.Global.SessionId, params, nodeInput.Config.Prompt, nodeInput.Global.ExecutionId, cNodeId, idxList[i])
// 每段成功立即落库:该段刚成功即持久化,其他段仍在跑时已成功段也不丢;
// 后续段失败或进程崩溃(panic/OOM/kill)时,已完成段已在库中,reExecute 可直接复用
if segVideo && errs[i] == nil {
@@ -112,7 +125,7 @@ func ModelLambda(ctx context.Context, input any) (any, error) {
if key == "" || url == "" {
continue
}
if err := flowDao.FlowSegmentResultDao.Save(ctx, nodeInput.Global.ExecutionId, nodeInput.Config.Id, idxList[i], key, url); err != nil {
if err := flowDao.FlowSegmentResultDao.Save(ctx, nodeInput.Global.ExecutionId, cNodeId, idxList[i], key, url); err != nil {
saveErrs[i] = err
}
}
@@ -160,7 +173,7 @@ func ModelLambda(ctx context.Context, input any) (any, error) {
}
} else {
for _, params := range paramsList {
res, modelRes, _, err := ModelCallResultLambda(ctx, nodeInput.Config.ModelConfig.ModelId, nodeInput.Global.SessionId, params, nodeInput.Config.Prompt, nodeInput.Global.ExecutionId, nodeInput.Config.Id, flowDao.FlowAsyncSegSentinel)
res, modelRes, _, err := ModelCallResultLambda(ctx, nodeInput.Config.ModelConfig.ModelId, nodeInput.Global.SessionId, params, nodeInput.Config.Prompt, nodeInput.Global.ExecutionId, cNodeId, flowDao.FlowAsyncSegSentinel)
if err != nil {
return nil, err
}