use tokio_util::sync::CancellationToken;
use uuid::Uuid;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct NodeContext {
pub thread_id: String,
pub node_name: String,
pub attempt: u8,
pub execution_id: String,
pub attempt_id: String,
pub last_checkpoint_id: Option<String>,
pub budget_remaining_eur: Option<f64>,
pub cancellation: Option<CancellationToken>,
pub resumed_input: Option<serde_json::Value>,
}
impl NodeContext {
pub(crate) fn new(
thread_id: String,
node_name: String,
attempt: u8,
last_checkpoint_id: Option<String>,
budget_remaining_eur: Option<f64>,
cancellation: Option<CancellationToken>,
resumed_input: Option<serde_json::Value>,
) -> Self {
let seed = format!(
"{}:{}:{}",
thread_id,
node_name,
last_checkpoint_id.as_deref().unwrap_or("initial")
);
let execution_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, seed.as_bytes()).to_string();
Self {
thread_id,
node_name,
attempt,
execution_id,
attempt_id: Uuid::new_v4().to_string(),
last_checkpoint_id,
budget_remaining_eur,
cancellation,
resumed_input,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execution_id_stable_across_attempts() {
let ctx1 = NodeContext::new(
"thread1".into(),
"nodeA".into(),
0,
Some("cp1".into()),
None,
None,
None,
);
let ctx2 = NodeContext::new(
"thread1".into(),
"nodeA".into(),
1,
Some("cp1".into()),
None,
None,
None,
);
assert_eq!(ctx1.execution_id, ctx2.execution_id);
assert_ne!(ctx1.attempt_id, ctx2.attempt_id);
}
#[test]
fn execution_id_changes_with_checkpoint() {
let ctx1 = NodeContext::new(
"thread1".into(),
"nodeA".into(),
0,
Some("cp1".into()),
None,
None,
None,
);
let ctx2 = NodeContext::new(
"thread1".into(),
"nodeA".into(),
0,
Some("cp2".into()),
None,
None,
None,
);
assert_ne!(ctx1.execution_id, ctx2.execution_id);
}
}