Files
DevFlow/crates/df-workflow/src/state.rs
绝尘 b157bc9077 修复: relay token 改环境变量 + api_key 脱敏 + 路径规范化 + create_project 自动建目录
- relay token 必设 DF_RELAY_TOKEN 环境变量,移除硬编码默认
- AiProviderRecord Debug 脱敏 api_key/model_configs/config
- ScriptNode 危险命令告警(rm -rf/DROP TABLE 等)
- bind_directory 路径规范化,拒绝含 .. 的原始路径
- create_project 目录不存在时自动创建
- probe_pwsh OnceLock 全局缓存,异步探测不阻塞 tokio
- retry jitter 改 rand,范围扩大到 ±50%
- EventBus send 错误改 tracing::warn
- StateMachine 文档警告不得在 await 期间持锁
2026-06-28 13:39:22 +08:00

215 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 节点状态机 — 管理节点的状态转换
//!
//! 合法转换Pending → RunningRunning → Completed / Failed
//! 非法转换返回错误,避免状态被随意覆盖。
//!
//! 共享语义:内部用 `Arc<Mutex<HashMap>>``clone()` 为浅拷贝Arc 引用计数 +1
//! 所有 clone 共享同一底层 HashMap。DagExecutor 的 `state_machine` 与其下沉到
//! `NodeContext.node_status` 的 clone 共享底层run_workflow 把执行器状态机注册到
//! AppState 全局表后,`cancel_workflow_node` IPC 的 `set_cancelled` 可直达运行中
//! 阻塞节点(如 HumanNode的 `is_cancelled` 轮询。
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use df_types::types::{NodeId, NodeStatus};
/// 节点状态机
///
/// # ⚠️ 并发安全警告
///
/// `state_machine` 方法(`get`、`transition`、`set_running`、`set_completed`、
/// `set_failed`、`set_cancelled`、`snapshot`、`is_cancelled`)内部持锁
/// `self.states.lock()`**不得在 `.await` 期间持锁**。
/// 若在异步上下文中调用,请确保获取结果后立即释放锁(即不要将锁跨越
/// `.await` 点)。当前所有方法均为同步且及时释放,符合此规则。
///
/// 共享语义:内部用 `Arc<Mutex<HashMap>>``clone()` 为浅拷贝Arc 引用计数 +1
/// 所有 clone 共享同一底层 HashMap。
#[derive(Debug, Clone)]
pub struct StateMachine {
/// 节点状态映射Arc 共享clone 浅拷贝同一 HashMap
states: Arc<Mutex<HashMap<NodeId, NodeStatus>>>,
}
impl StateMachine {
/// 创建状态机
pub fn new() -> Self {
Self {
states: Arc::new(Mutex::new(HashMap::new())),
}
}
/// 获取节点状态
pub fn get(&self, node_id: &NodeId) -> NodeStatus {
self.states
.lock()
.expect("状态机锁中毒")
.get(node_id)
.cloned()
.unwrap_or(NodeStatus::Pending)
}
/// 判断状态转换是否合法
fn is_legal(from: &NodeStatus, to: &NodeStatus) -> bool {
matches!(
(from, to),
(NodeStatus::Pending, NodeStatus::Running)
| (NodeStatus::Running, NodeStatus::Completed)
| (NodeStatus::Running, NodeStatus::Failed)
)
}
/// 状态转换 — 校验合法性后更新,非法转换返回错误
pub fn transition(&self, node_id: NodeId, target: NodeStatus) -> anyhow::Result<()> {
let mut states = self.states.lock().expect("状态机锁中毒");
let current = states
.get(&node_id)
.cloned()
.unwrap_or(NodeStatus::Pending);
if !Self::is_legal(&current, &target) {
anyhow::bail!(
"节点 {} 状态转换非法:{} -> {}",
node_id,
current.as_str(),
target.as_str()
);
}
states.insert(node_id, target);
Ok(())
}
/// 设置节点为运行中(仅允许 Pending -> Running
pub fn set_running(&self, node_id: NodeId) -> anyhow::Result<()> {
self.transition(node_id, NodeStatus::Running)
}
/// 设置节点为已完成(仅允许 Running -> Completed
pub fn set_completed(&self, node_id: NodeId) -> anyhow::Result<()> {
self.transition(node_id, NodeStatus::Completed)
}
/// 设置节点为失败(仅允许 Running -> Failed
pub fn set_failed(&self, node_id: NodeId) -> anyhow::Result<()> {
self.transition(node_id, NodeStatus::Failed)
}
/// 设置节点为已取消 — 唯一受控旁路:不经 transition 合法性校验
///
/// 设计理由:人工审批取消场景由前端 cancel_workflow_node IPC 异步触发,
/// 此时节点可能处于 Running 之外的任意态(如 Pending 排队中、阻塞等待审批中),
/// 若走 transition 校验会被 is_legal(Pending→Cancelled) 拒绝。
/// 故 set_cancelled 作为唯一受控旁路直接置位 Cancelled
/// 阻塞节点(如 HumanNode)在 select! 轮询分支通过 is_cancelled 检测后返回 Err。
///
/// 注:原 set_waiting/set_skipped 同为旁路置位但全仓零调用,已删除。
pub fn set_cancelled(&self, node_id: NodeId) {
self.states
.lock()
.expect("状态机锁中毒")
.insert(node_id, NodeStatus::Cancelled);
}
/// 获取所有状态快照clone 返回,调用方持独立副本)
pub fn snapshot(&self) -> HashMap<NodeId, NodeStatus> {
self.states.lock().expect("状态机锁中毒").clone()
}
/// 检查节点是否被取消
pub fn is_cancelled(&self, node_id: &NodeId) -> bool {
matches!(self.get(node_id), NodeStatus::Cancelled)
}
}
impl Default for StateMachine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_legal_transitions() {
let sm = StateMachine::new();
let id = "node-a".to_string();
// Pending -> Running -> Completed 全程合法
assert!(sm.set_running(id.clone()).is_ok());
assert_eq!(sm.get(&id), NodeStatus::Running);
assert!(sm.set_completed(id.clone()).is_ok());
assert_eq!(sm.get(&id), NodeStatus::Completed);
// Running -> Failed 合法
let id2 = "node-b".to_string();
sm.set_running(id2.clone()).unwrap();
assert!(sm.set_failed(id2.clone()).is_ok());
assert_eq!(sm.get(&id2), NodeStatus::Failed);
}
#[test]
fn test_illegal_transitions_rejected() {
let sm = StateMachine::new();
let id = "node-a".to_string();
// Pending -> Completed 非法
let err = sm.set_completed(id.clone()).unwrap_err();
assert!(err.to_string().contains("状态转换非法"));
assert!(err.to_string().contains("node-a"));
// 状态保持不变
assert_eq!(sm.get(&id), NodeStatus::Pending);
// Pending -> Failed 非法
assert!(sm.set_failed(id.clone()).is_err());
// Completed 为终态,不允许再转 Running
sm.set_running(id.clone()).unwrap();
sm.set_completed(id.clone()).unwrap();
assert!(sm.set_running(id.clone()).is_err());
// Running -> Running 重复置位非法
let id2 = "node-b".to_string();
sm.set_running(id2.clone()).unwrap();
assert!(sm.set_running(id2.clone()).is_err());
}
#[test]
fn test_set_cancelled_marks_node_cancelled() {
// set_cancelled 为唯一受控旁路,不经转换校验直接置位 Cancelled
let sm = StateMachine::new();
let id = "human-1".to_string();
// 初始为 Pendingis_cancelled = false
assert_eq!(sm.get(&id), NodeStatus::Pending);
assert!(!sm.is_cancelled(&id));
// 取消后置位 Cancelledis_cancelled = true
sm.set_cancelled(id.clone());
assert_eq!(sm.get(&id), NodeStatus::Cancelled);
assert!(sm.is_cancelled(&id));
}
#[test]
fn test_clone_shares_underlying_state() {
// 核心机制clone 浅拷贝 Arc写入对另一副本可见。
// 这让 cancel IPC 写 AppState 注册表的 StateMachine
// 直达 HumanNode 持有的 ctx.node_statusexecutor.state_machine 的 clone
let sm = StateMachine::new();
let sm_clone = sm.clone();
// 原实例写入clone 可见
sm.set_cancelled("human-1".to_string());
assert!(sm_clone.is_cancelled(&"human-1".to_string()));
// clone 写入,原实例亦可见(双向)
sm_clone.set_running("node-a".to_string()).unwrap();
assert_eq!(sm.get(&"node-a".to_string()), NodeStatus::Running);
// 第三副本同样共享
let sm3 = sm_clone.clone();
assert!(sm3.is_cancelled(&"human-1".to_string()));
}
}