Files
DevFlow/crates/df-workflow/src/state.rs
T

258 lines
9.6 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())),
}
}
/// 获取节点状态
///
/// 锁中毒时降级返回 `NodeStatus::Pending`(保守默认:视为未启动,
/// 执行器不会误判为已完成/失败),并 `tracing::error!` 记录,不 panic。
pub fn get(&self, node_id: &NodeId) -> NodeStatus {
match self.states.lock() {
Ok(states) => states.get(node_id).cloned().unwrap_or(NodeStatus::Pending),
Err(poisoned) => {
tracing::error!(
"状态机锁中毒,get({}) 降级返回 Pending{}",
node_id,
poisoned
);
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)
)
}
/// 状态转换 — 校验合法性后更新,非法转换返回错误
///
/// 锁中毒时降级返回 `Err`(携带「状态机锁中毒」上下文)并 `tracing::error!` 记录,
/// 由调用方决定如何处理(通常是 `set_running`/`set_completed`/`set_failed`
/// 把 Err 上抛 → 节点执行流捕获后置 Failed),不 panic 拖垮 runtime。
pub fn transition(&self, node_id: NodeId, target: NodeStatus) -> anyhow::Result<()> {
let mut states = match self.states.lock() {
Ok(guard) => guard,
Err(poisoned) => {
tracing::error!(
"状态机锁中毒,transition({}, {}) 降级返 Err{}",
node_id,
target.as_str(),
poisoned
);
anyhow::bail!("状态机锁中毒,节点 {} 状态转换失败", node_id);
}
};
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) {
match self.states.lock() {
Ok(mut states) => {
states.insert(node_id, NodeStatus::Cancelled);
}
Err(poisoned) => {
tracing::error!(
"状态机锁中毒,set_cancelled({}) 降级丢弃取消信号:{}",
node_id,
poisoned
);
}
}
}
/// 获取所有状态快照(clone 返回,调用方持独立副本)
///
/// 锁中毒时降级返回空 HashMap(调用方遍历视为无已完成节点,保守安全),
/// 并 `tracing::error!` 记录,不 panic。
pub fn snapshot(&self) -> HashMap<NodeId, NodeStatus> {
match self.states.lock() {
Ok(states) => states.clone(),
Err(poisoned) => {
tracing::error!(
"状态机锁中毒,snapshot() 降级返回空 HashMap{}",
poisoned
);
HashMap::new()
}
}
}
/// 检查节点是否被取消
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()));
}
}