fix(workflow): 修复子流程批量执行缓存键冲突
递归识别子工作流中的视频模型以支持 per_second 计费,并为子流程批量子执行添加 SubFlowScope 隔离 async/segment 缓存键。
This commit is contained in:
@@ -34,6 +34,7 @@ type FlowExecutionInput struct {
|
||||
SessionId string `json:"sessionId" dc:"会话ID"`
|
||||
ExecutedNodes []ExecutedNode `json:"executedNodes"` // 已执行节点列表,包含执行状态
|
||||
ForceNewRun bool `json:"forceNewRun" dc:"是否全新执行(false=断点续跑,视频节点段级复用已成功段)"`
|
||||
SubFlowScope string `json:"subFlowScope" dc:"子流程批量子执行缓存作用域(拼进 async/segment 缓存键隔离各份;顶层为空)"`
|
||||
}
|
||||
|
||||
// ExecutedNode 已执行节点记录,包含节点ID和执行状态
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
nodeDao "ai-agent/workflow/dao/node"
|
||||
sessionDao "ai-agent/workflow/dao/session"
|
||||
flowDto "ai-agent/workflow/model/dto/flow"
|
||||
nodeDto "ai-agent/workflow/model/dto/node"
|
||||
"ai-agent/workflow/model/entity"
|
||||
)
|
||||
@@ -134,22 +135,51 @@ func openBillingOrder(ctx context.Context, execId int64, flowContent *entity.Flo
|
||||
}
|
||||
|
||||
// flowHasVideoModel 工作流是否包含视频模型节点(per_second 计费前置条件)。
|
||||
// 遍历节点,按 modelId 去重后经 isVideoModel 查模型类型,任一为视频模型即 true;
|
||||
// flowContent 缺失/查模型失败按无视频模型处理(per_second 被拦截,fail-closed)。
|
||||
// 遍历节点,按 modelId 去重后经 isVideoModel 查模型类型;sub_flow 节点按 subConfig.workflowId
|
||||
// 递归展开其子工作流(与 SubFlowLambda 运行期解析一致),任一可达层级的视频模型即 true——
|
||||
// 视频模型放在子工作流内同样满足 per_second 前置条件。
|
||||
// flowContent/子工作流失联、取子工作流失败按该支无视频模型处理(fail-closed:per_second 被拦截)。
|
||||
// 递归以被展开的子流程 workflowId 去重防环、跨层共享 modelId 去重,避免重复取子流程/重复查模型类型。
|
||||
func flowHasVideoModel(ctx context.Context, flowContent *entity.FlowInfo) bool {
|
||||
if flowContent == nil {
|
||||
return false
|
||||
}
|
||||
seen := make(map[int64]struct{})
|
||||
return hasVideoModelRecursive(ctx, flowContent,
|
||||
make(map[int64]struct{}), make(map[int64]struct{}))
|
||||
}
|
||||
|
||||
// hasVideoModelRecursive 递归扫描单层 flow 是否含视频模型节点。
|
||||
// 先查节点自身模型 id,再展开 sub_flow 子工作流递归(sub_flow 节点自身无模型,ModelId 恒 0)。
|
||||
// expandedSubFlows 记录已展开的子流程 workflowId(展开前先标记,防 A→B→A 无限递归并去重重复引用);
|
||||
// seenModels 记录已查过的模型 id,跨层共享避免同一模型重复调 isVideoModel。
|
||||
func hasVideoModelRecursive(ctx context.Context, flowContent *entity.FlowInfo,
|
||||
expandedSubFlows, seenModels map[int64]struct{}) bool {
|
||||
for i := range flowContent.Nodes {
|
||||
modelId := flowContent.Nodes[i].ModelConfig.ModelId
|
||||
n := &flowContent.Nodes[i]
|
||||
// sub_flow 节点:取子工作流继续递归;取不到/无法解析按该支无视频模型,继续扫其余节点
|
||||
if sc := n.SubConfig; sc != nil && sc.WorkflowId > 0 {
|
||||
if _, ok := expandedSubFlows[sc.WorkflowId]; ok {
|
||||
continue
|
||||
}
|
||||
expandedSubFlows[sc.WorkflowId] = struct{}{}
|
||||
subRes, err := FlowUserService.Get(ctx, &flowDto.GetFlowUserReq{Id: sc.WorkflowId})
|
||||
if err != nil || subRes == nil || subRes.FlowContent == nil {
|
||||
continue
|
||||
}
|
||||
if hasVideoModelRecursive(ctx, subRes.FlowContent, expandedSubFlows, seenModels) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 普通模型节点:按 modelId 去重后查类型
|
||||
modelId := n.ModelConfig.ModelId
|
||||
if modelId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[modelId]; ok {
|
||||
if _, ok := seenModels[modelId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[modelId] = struct{}{}
|
||||
seenModels[modelId] = struct{}{}
|
||||
if isVideoModel(ctx, modelId) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -61,12 +61,18 @@ func SubFlowLambda(ctx context.Context, input any) (any, error) {
|
||||
configMap := buildConfigMap(getRes.FlowContent, nodeList)
|
||||
batchInputs := make([]*flowDto.FlowExecutionInput, 0, batchCount)
|
||||
for j := 0; j < batchCount; j++ {
|
||||
// 缓存作用域:拼本(外层)子流程节点 Id + 份号 j,供 async/segment 缓存键隔离各份
|
||||
// (同一 exec 下 N 份内层节点 id 相同,无作用域会互相命中/覆盖 done 结果,见《工作流子流程批量缓存隔离设计.md》)。
|
||||
// 嵌套子流程继承父 scope(Global.SubFlowScope)递归叠加,保证跨 exec/嵌套路径唯一;
|
||||
// 跨 launch 续跑按同序重放 batch → 各份 scope 稳定,仍能命中自己那份结果。
|
||||
scope := nodeExecInput.Global.SubFlowScope + fmt.Sprintf("[%s#%d]", nodeExecInput.Config.Id, j)
|
||||
batchInputs = append(batchInputs, &flowDto.FlowExecutionInput{
|
||||
NodeGroupId: nodeExecInput.Global.NodeGroupId,
|
||||
ExecutionId: nodeExecInput.Global.ExecutionId,
|
||||
FlowId: nodeExecInput.Global.FlowId,
|
||||
ConfigMap: cloneConfigMap(configMap),
|
||||
SessionId: nodeExecInput.Global.SessionId,
|
||||
NodeGroupId: nodeExecInput.Global.NodeGroupId,
|
||||
ExecutionId: nodeExecInput.Global.ExecutionId,
|
||||
FlowId: nodeExecInput.Global.FlowId,
|
||||
ConfigMap: cloneConfigMap(configMap),
|
||||
SessionId: nodeExecInput.Global.SessionId,
|
||||
SubFlowScope: scope,
|
||||
})
|
||||
}
|
||||
// 7. 执行批量子流程
|
||||
|
||||
Reference in New Issue
Block a user