1. 什么是 DAG?为什么工作流引擎要用 DAG 来表示?
考察点:数据结构基础、DAG 特性
参考答案:
DAG 是有向无环图。"有向"是说边有方向,从 A 指向 B 表示 A 执行完才能执行 B;"无环"是说不能有循环依赖,A→B→C→A 这种是不允许的。
工作流用 DAG 有很多好处:节点之间谁先谁后一目了然、没有依赖关系的节点可以同时跑、能算出一个合法的执行顺序、无环的特性保证工作流一定能执行完。
在 PaiFlow 里,节点就是 Node 对象,边就是 Edge 对象,存的是 source 和 target 两个节点 ID。
2. 你的 WorkflowEngine 是怎么解析 DSL(JSON)并构建执行链路的?
考察点:DSL 解析、链路构建逻辑
参考答案:
整个过程分三步:
第一步:反序列化,前端传过来的是 JSON,我用 FastJSON 直接反序列化成 WorkflowDSL 对象,里面包含 nodes 列表和 edges 列表。
第二步:构建节点索引,遍历 nodes 列表,建一个 Map
// WorkflowEngine.java L157-164
Map();
for (Node node : workflowDSL.getNodes()) {
if (node.getNodeType() == NodeTypeEnum.START) {
startNode = node; // 标记入口点
}
node.init(); // 初始化节点状态
nodeMap.put(node.getId(), node);
}
第三步:根据边构建节点关系,遍历 edges 列表,对于每条边:
-
把 target 节点加到 source 节点的 nextNodes 列表
-
把 source 节点加到 target 节点的 preNodes 列表
-
如果是异常分支的边,加到 failNodes 列表
// WorkflowEngine.java L171-203
for (Edge edge : workflowDSL.getEdges()) {
// 1. 获取源节点和目标节点对象
Node sourceNode = nodeMap.get(edge.getSource());
Node targetNode = nodeMap.get(edge.getTarget());
// 2. 建立反向依赖:记录目标节点的前置节点 (PreNodes)
// 这用于后续执行时的依赖检查:只有所有 PreNodes 都执行完,TargetNode 才能执行
targetNode.getPreNodes().add(sourceNode);
// 3. 建立正向依赖:根据 Handle 类型决定是 "正常路径" 还是 "异常路径"
String handle = edge.getSourceHandle();
if (StringUtils.isNotBlank(handle)) {
if (handle.startsWith("condition_switch_normal_one_of")) {
// 正常分支:Source 成功后 -> 执行 Target
sourceNode.getNextNodes().add(targetNode);
} else if (handle.startsWith("condition_switch_intent_chain")) {
// 异常/失败分支:Source 失败后 -> 执行 Target
sourceNode.getFailNodes().add(targetNode);
}
} else {
// 默认情况:无条件流转
sourceNode.getNextNodes().add(targetNode);
}
}
最后返回 Start 节点,从它开始就能顺着 nextNodes 遍历整个图了。执行时从 startNode 开始,递归执行 nextNodes 里的节点就行了。
3. 工作流中如何处理节点之间的依赖关系?如果节点 C 依赖节点 A 和 B,怎么保证 A、B 都执行完才执行 C?
考察点:依赖管理、执行顺序控制
参考答案:
工作流引擎采用了深度优先搜索 (DFS) 结合递归回溯检查的策略。当引擎尝试执行节点 C 时,它不会立即执行,而是先检查 C 的所有前置节点(A 和 B)的状态。
// WorkflowEngine.java L216
private void executeNode(Node node, VariablePool variablePool, WorkflowMsgCallback callback) throws Exception {
// ... (省略部分代码)
// 1. 前置校验: node 执行的前提条件是所有的前置Node都已经执行完毕
if (!CollectionUtils.isEmpty(node.getPreNodes())) {
for (Node preNode : node.getPreNodes()) {
// 关键点:如果发现有前置节点还没执行完 (!executed)
if (!preNode.getStatus().executed()) {
// 递归调用:先去执行那个前置节点
executeNode(preNode, variablePool, callback);
}
}
}
// ... (只有当上面循环结束,意味着所有 preNodes 都已执行,才继续往下执行当前节点逻辑)
// 3. 执行当前节点
// ...
}
当执行到 C 时,发现 A 没执行完,就先去执行 A;A 执行完回来,发现 B 没执行完,再去执行 B;都执行完了,才真正执行 C。
追问 1:在 PaiFlow 中,一共有多少种节点状态?
在 PaiFlow 的工作流引擎中,节点共有 6 种状态 ,INIT(初始化)、RUNNING(运行中)、SUCCESS(成功)、ERROR(失败)、SKIP(跳过)、MARK(标记)。
节点初始化时是 INIT,开始执行时变为 RUNNING,执行完成后根据结果变为 SUCCESS 或 ERROR。如果在条件...
回复