后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
259 lines
13 KiB
Rust
259 lines
13 KiB
Rust
//! DAG 执行器 — 按拓扑层级调度执行
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use df_types::events::WorkflowEvent;
|
||
use df_types::types::NodeId;
|
||
|
||
use crate::conditions::ConditionEngine;
|
||
use crate::dag::Dag;
|
||
use crate::eventbus::EventBus;
|
||
use crate::node::{NodeContext, NodeOutput};
|
||
use crate::state::StateMachine;
|
||
|
||
/// DAG 执行器
|
||
pub struct DagExecutor {
|
||
/// 事件总线
|
||
event_bus: EventBus,
|
||
/// 节点状态机
|
||
state_machine: StateMachine,
|
||
/// 工作流执行 ID(由调用方传入,下沉到每个 NodeContext)
|
||
execution_id: String,
|
||
}
|
||
|
||
impl DagExecutor {
|
||
/// 创建执行器
|
||
///
|
||
/// `execution_id` 为本次工作流执行的唯一标识,会下沉到每个节点的 NodeContext,
|
||
/// 用于节点内的事件关联、审计追踪等。
|
||
pub fn new(event_bus: EventBus, execution_id: String) -> Self {
|
||
Self {
|
||
event_bus,
|
||
state_machine: StateMachine::new(),
|
||
execution_id,
|
||
}
|
||
}
|
||
|
||
/// 返回状态机的共享引用(Arc clone,与 NodeContext.node_status 共享底层 HashMap)
|
||
///
|
||
/// run_workflow 创建执行器后将其注册到 AppState 全局表,
|
||
/// cancel_workflow_node IPC 经 execution_id 取出此引用调 set_cancelled,
|
||
/// 写入直达运行中阻塞节点(HumanNode)的 is_cancelled 轮询。
|
||
pub fn state_machine(&self) -> StateMachine {
|
||
self.state_machine.clone()
|
||
}
|
||
|
||
/// 执行 DAG 工作流
|
||
///
|
||
/// 同一拓扑层内的节点并发执行,层与层之间串行;
|
||
/// 本层全部节点完成后统一更新状态,任一失败则中止后续层。
|
||
pub async fn run(
|
||
&mut self,
|
||
dag: &Dag,
|
||
initial_config: serde_json::Value,
|
||
) -> anyhow::Result<HashMap<NodeId, NodeOutput>> {
|
||
let layers = dag.topological_layers()?;
|
||
let mut outputs: HashMap<NodeId, NodeOutput> = HashMap::new();
|
||
let start = std::time::Instant::now();
|
||
|
||
tracing::info!("DAG 执行开始,共 {} 层", layers.len());
|
||
|
||
// 预建入边索引:target → 直接前驱列表 + 边条件表达式(O(E) 一次构建)
|
||
// 避免在内层按节点循环中调用 dag.predecessors()(每次 O(E) 全表扫描,整体 O(V·E))。
|
||
// 含 condition 是为 conditions-eval feature 接入:flag 开时按入边 condition 过滤前驱 input。
|
||
// flag 关时 condition 字段被忽略,行为与增强前完全一致(零破坏)。
|
||
let mut adjacency_in: HashMap<NodeId, Vec<(NodeId, Option<String>)>> = HashMap::new();
|
||
for edge in &dag.edges {
|
||
adjacency_in
|
||
.entry(edge.target.clone())
|
||
.or_default()
|
||
.push((edge.source.clone(), edge.condition.clone()));
|
||
}
|
||
|
||
for (layer_idx, layer) in layers.iter().enumerate() {
|
||
tracing::info!("执行第 {} 层,共 {} 个节点", layer_idx, layer.len());
|
||
|
||
// 阶段一:逐节点发 NodeStarted、置为运行中,并构建执行 future
|
||
let mut node_futures = Vec::with_capacity(layer.len());
|
||
for node_id in layer {
|
||
let node = dag.nodes.get(node_id).ok_or_else(|| {
|
||
anyhow::anyhow!("节点 {} 不存在于 DAG 中", node_id)
|
||
})?;
|
||
|
||
// 构建节点上下文:通过入边索引 O(入度) 取前驱,而非 O(E) 全表扫描。
|
||
//
|
||
// conditions-eval feature(默认关):
|
||
// 关:所有入边 inputs 全收集,condition 字段完全忽略 = 旧行为(零破坏)。
|
||
// 开:对带 condition 的入边以 source output.data 为 context 求值;
|
||
// false 则不收集该前驱 input。若节点【存在带 condition 的入边,且无任何
|
||
// 入边(条件或无条件)放行】,则跳过该节点执行(不入 outputs、保持 Pending、
|
||
// 不 emit),实现条件路由。无条件边 source 有 output 即视为放行。
|
||
// 求值失败保守 false(对齐引擎语义)。
|
||
//
|
||
// 注:跳过判定在 set_running 之前 —— 跳过的节点不应被标记 Running/触发事件,
|
||
// 保持 Pending 终态(与"条件未命中"语义一致)。
|
||
let eval_conditions = cfg!(feature = "conditions-eval");
|
||
let mut inputs: HashMap<NodeId, NodeOutput> = HashMap::new();
|
||
let mut has_any_cond_edge = false;
|
||
let mut any_edge_passed = false; // 任一入边放行(含无条件边)
|
||
if let Some(preds) = adjacency_in.get(node_id) {
|
||
for (pred_id, cond) in preds {
|
||
if let Some(out) = outputs.get(pred_id) {
|
||
if eval_conditions {
|
||
if let Some(cond) = cond {
|
||
has_any_cond_edge = true;
|
||
// 以 source output.data 为 context 求值;失败保守 false
|
||
let passed = ConditionEngine::evaluate(cond, &out.data)
|
||
.unwrap_or(false);
|
||
if passed {
|
||
inputs.insert(pred_id.clone(), out.clone());
|
||
any_edge_passed = true;
|
||
}
|
||
} else {
|
||
// 无 condition 的入边:无条件放行(必收集)
|
||
inputs.insert(pred_id.clone(), out.clone());
|
||
any_edge_passed = true;
|
||
}
|
||
} else {
|
||
// flag 关:condition 完全忽略,旧行为全收集
|
||
inputs.insert(pred_id.clone(), out.clone());
|
||
any_edge_passed = true;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 根节点(无入边):不参与条件跳过判定,正常执行
|
||
any_edge_passed = true;
|
||
}
|
||
|
||
// 条件路由:flag 开 + 存在条件边 + 没有任何入边放行 → 跳过执行
|
||
if eval_conditions && has_any_cond_edge && !any_edge_passed {
|
||
tracing::info!(
|
||
"节点 {} 所有入边条件均不满足,跳过执行(条件路由)",
|
||
node_id
|
||
);
|
||
continue;
|
||
}
|
||
|
||
// 发送 NodeStarted 事件
|
||
self.event_bus
|
||
.send(WorkflowEvent::NodeStarted {
|
||
node_id: node_id.clone(),
|
||
})
|
||
.await;
|
||
|
||
self.state_machine.set_running(node_id.clone())?;
|
||
|
||
let ctx = NodeContext {
|
||
node_id: node_id.clone(),
|
||
inputs,
|
||
// 节点级覆盖全局级:deep_merge(initial_config, node_configs[id])。
|
||
// 节点 NodeDef.config 未定义(node_configs 缺该 key)→ 直接用全局 initial_config,
|
||
// 与旧行为一致(零行为破坏现有 HumanNode/AiNode 等读全局 config 的调用方)。
|
||
config: match dag.node_configs.get(node_id) {
|
||
Some(node_cfg) => crate::dag::deep_merge(&initial_config, node_cfg),
|
||
None => initial_config.clone(),
|
||
},
|
||
execution_id: self.execution_id.clone(),
|
||
event_bus: self.event_bus.clone(),
|
||
// 共享执行器状态机,使节点(如 HumanNode)能读取真实状态而非空状态机
|
||
node_status: self.state_machine.clone(),
|
||
};
|
||
|
||
// 仅捕获节点共享引用与所有权上下文,避免与 self 借用冲突
|
||
let id = node_id.clone();
|
||
node_futures.push(async move {
|
||
let node_start = std::time::Instant::now();
|
||
let result = node.execute(ctx).await;
|
||
(id, result, node_start.elapsed().as_millis() as u64)
|
||
});
|
||
}
|
||
|
||
// 阶段二:同层节点并发执行,等待全部完成
|
||
let results = futures::future::join_all(node_futures).await;
|
||
|
||
// 阶段三:统一更新状态并发送事件,任一失败则整体返回 Err
|
||
let mut first_err: Option<anyhow::Error> = None;
|
||
for (node_id, result, duration) in results {
|
||
match result {
|
||
Ok(output) => {
|
||
// 已取消的节点跳过 set_completed:
|
||
// Ok 后 join_all 让出执行权,窗口内 cancel_workflow_node IPC → set_cancelled 改 Cancelled,
|
||
// 随后此处 set_completed 走 transition 命中(Cancelled→Completed)非法 → bail 致已批准审批报失败(R-P1-3 TOCTOU)
|
||
// 与 Err 分支 is_cancelled 短路对称:已取消节点保持 Cancelled 终态
|
||
if !self.state_machine.is_cancelled(&node_id) {
|
||
self.state_machine.set_completed(node_id.clone())?;
|
||
// 未取消才 emit NodeCompleted,取消时 emit NodeCancelled(对齐 Err 分支语义):
|
||
// 避免 状态机=Cancelled 但事件=NodeCompleted 的矛盾信号
|
||
self.event_bus
|
||
.send(WorkflowEvent::NodeCompleted {
|
||
node_id: node_id.clone(),
|
||
duration_ms: duration,
|
||
})
|
||
.await;
|
||
} else {
|
||
// 对齐 Err 分支:取消节点 emit NodeCancelled(非 NodeCompleted),
|
||
// 前端按 type 归「取消」,与状态机 Cancelled 一致
|
||
self.event_bus
|
||
.send(WorkflowEvent::NodeCancelled {
|
||
node_id: node_id.clone(),
|
||
})
|
||
.await;
|
||
}
|
||
// outputs.insert 保留现状不动:取消时仍写入 output 供下游消费,
|
||
// 当前测试锁定"取消不中止后续层",改 insert 会变行为致回归
|
||
outputs.insert(node_id, output);
|
||
}
|
||
Err(e) => {
|
||
let error_msg = e.to_string();
|
||
// 已取消的节点(如 HumanNode 审批取消)跳过 set_failed:
|
||
// Cancelled 已是终态,transition(Cancelled→Failed) 非法会 bail 致工作流崩溃(B-03b-R1)
|
||
if self.state_machine.is_cancelled(&node_id) {
|
||
// emit NodeCancelled(非 NodeFailed):取消语义有别于失败,前端按 type 归「取消」非「失败」
|
||
self.event_bus
|
||
.send(WorkflowEvent::NodeCancelled {
|
||
node_id: node_id.clone(),
|
||
})
|
||
.await;
|
||
} else {
|
||
self.state_machine.set_failed(node_id.clone())?;
|
||
self.event_bus
|
||
.send(WorkflowEvent::NodeFailed {
|
||
node_id: node_id.clone(),
|
||
error: error_msg,
|
||
})
|
||
.await;
|
||
}
|
||
// 取消与失败一致:节点返回 Err 即中止后续层(run 返回 Err);
|
||
// 仅事件类型区分,工作流结果语义保持不变(零行为破坏)。
|
||
if first_err.is_none() {
|
||
first_err = Some(e.context(format!("节点 {} 执行失败", node_id)));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Some(e) = first_err {
|
||
return Err(e);
|
||
}
|
||
}
|
||
|
||
let total = start.elapsed().as_millis() as u64;
|
||
// B-03b-R10 ③: WorkflowCompleted 携带 execution_id,供 forward 循环按 exec_id 匹配
|
||
// (全局 event_bus 单例下并发工作流的终态事件不再误触发他人 forward 的完成判定)。
|
||
self.event_bus
|
||
.send(WorkflowEvent::WorkflowCompleted {
|
||
execution_id: self.execution_id.clone(),
|
||
total_duration_ms: total,
|
||
})
|
||
.await;
|
||
|
||
tracing::info!("DAG 执行完成,耗时 {}ms", total);
|
||
Ok(outputs)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "executor_helpers.rs"]
|
||
mod tests;
|