136
src/dag.rs
136
src/dag.rs
@@ -14,6 +14,10 @@ pub enum DagError {
|
|||||||
Storage(Box<dyn std::error::Error + Send + Sync>),
|
Storage(Box<dyn std::error::Error + Send + Sync>),
|
||||||
MissingDependency { job: u32, depends_on: u32 },
|
MissingDependency { job: u32, depends_on: u32 },
|
||||||
CycleDetected { remaining: Vec<u32> },
|
CycleDetected { remaining: Vec<u32> },
|
||||||
|
UnknownJob { job: u32 },
|
||||||
|
DependenciesIncomplete { job: u32, missing: Vec<u32> },
|
||||||
|
FlowFailed { failed_job: u32 },
|
||||||
|
JobNotStarted { job: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DagError {
|
impl fmt::Display for DagError {
|
||||||
@@ -28,6 +32,20 @@ impl fmt::Display for DagError {
|
|||||||
DagError::CycleDetected { remaining } => {
|
DagError::CycleDetected { remaining } => {
|
||||||
write!(f, "Cycle detected; unresolved nodes: {:?}", remaining)
|
write!(f, "Cycle detected; unresolved nodes: {:?}", remaining)
|
||||||
}
|
}
|
||||||
|
DagError::UnknownJob { job } => write!(f, "Unknown job id: {}", job),
|
||||||
|
DagError::DependenciesIncomplete { job, missing } => write!(
|
||||||
|
f,
|
||||||
|
"Job {} cannot start; missing completed deps: {:?}",
|
||||||
|
job, missing
|
||||||
|
),
|
||||||
|
DagError::FlowFailed { failed_job } => {
|
||||||
|
write!(f, "Flow failed due to job {}", failed_job)
|
||||||
|
}
|
||||||
|
DagError::JobNotStarted { job } => write!(
|
||||||
|
f,
|
||||||
|
"Job {} cannot be completed because it is not marked as started",
|
||||||
|
job
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +77,10 @@ pub struct FlowDag {
|
|||||||
pub roots: Vec<u32>, // in_degree == 0
|
pub roots: Vec<u32>, // in_degree == 0
|
||||||
pub leaves: Vec<u32>, // out_degree == 0
|
pub leaves: Vec<u32>, // out_degree == 0
|
||||||
pub levels: Vec<Vec<u32>>, // topological layers for parallel execution
|
pub levels: Vec<Vec<u32>>, // topological layers for parallel execution
|
||||||
|
// Runtime execution state
|
||||||
|
pub started: HashSet<u32>,
|
||||||
|
pub completed: HashSet<u32>,
|
||||||
|
pub failed_job: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn build_flow_dag(
|
pub async fn build_flow_dag(
|
||||||
@@ -200,8 +222,122 @@ pub async fn build_flow_dag(
|
|||||||
roots,
|
roots,
|
||||||
leaves,
|
leaves,
|
||||||
levels,
|
levels,
|
||||||
|
started: HashSet::new(),
|
||||||
|
completed: HashSet::new(),
|
||||||
|
failed_job: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(dag)
|
Ok(dag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FlowDag {
|
||||||
|
/// Return all jobs that are ready to be processed.
|
||||||
|
/// A job is ready if:
|
||||||
|
/// - it exists in the DAG
|
||||||
|
/// - it is not already started or completed
|
||||||
|
/// - it has no dependencies, or all dependencies are completed
|
||||||
|
/// If any job has failed, the entire flow is considered failed and an error is returned.
|
||||||
|
pub fn ready_jobs(&self) -> DagResult<Vec<u32>> {
|
||||||
|
if let Some(failed_job) = self.failed_job {
|
||||||
|
return Err(DagError::FlowFailed { failed_job });
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ready: Vec<u32> = Vec::new();
|
||||||
|
for (&jid, summary) in &self.nodes {
|
||||||
|
if self.completed.contains(&jid) || self.started.contains(&jid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut deps_ok = true;
|
||||||
|
for dep in &summary.depends {
|
||||||
|
if !self.completed.contains(dep) {
|
||||||
|
deps_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deps_ok {
|
||||||
|
ready.push(jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready.sort_unstable();
|
||||||
|
Ok(ready)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a job as started.
|
||||||
|
/// Strict validation rules:
|
||||||
|
/// - Unknown jobs are rejected with UnknownJob
|
||||||
|
/// - If the flow has already failed, return FlowFailed
|
||||||
|
/// - If the job is already started or completed, this is a no-op (idempotent)
|
||||||
|
/// - If any dependency is not completed, return DependenciesIncomplete with the missing deps
|
||||||
|
pub fn mark_job_started(&mut self, job: u32) -> DagResult<()> {
|
||||||
|
if !self.nodes.contains_key(&job) {
|
||||||
|
return Err(DagError::UnknownJob { job });
|
||||||
|
}
|
||||||
|
if self.completed.contains(&job) || self.started.contains(&job) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(failed_job) = self.failed_job {
|
||||||
|
return Err(DagError::FlowFailed { failed_job });
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary = self.nodes.get(&job).expect("checked contains_key");
|
||||||
|
let missing: Vec<u32> = summary
|
||||||
|
.depends
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|d| !self.completed.contains(d))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !missing.is_empty() {
|
||||||
|
return Err(DagError::DependenciesIncomplete { job, missing });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.started.insert(job);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a job as completed.
|
||||||
|
/// Strict validation rules:
|
||||||
|
/// - Unknown jobs are rejected with UnknownJob
|
||||||
|
/// - If the job is already completed, this is a no-op (idempotent)
|
||||||
|
/// - If the flow has already failed, return FlowFailed
|
||||||
|
/// - If the job was not previously started, return JobNotStarted
|
||||||
|
pub fn mark_job_completed(&mut self, job: u32) -> DagResult<()> {
|
||||||
|
if !self.nodes.contains_key(&job) {
|
||||||
|
return Err(DagError::UnknownJob { job });
|
||||||
|
}
|
||||||
|
if self.completed.contains(&job) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(failed_job) = self.failed_job {
|
||||||
|
return Err(DagError::FlowFailed { failed_job });
|
||||||
|
}
|
||||||
|
if !self.started.contains(&job) {
|
||||||
|
return Err(DagError::JobNotStarted { job });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.started.remove(&job);
|
||||||
|
self.completed.insert(job);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a job as failed.
|
||||||
|
/// Behavior:
|
||||||
|
/// - Unknown jobs are rejected with UnknownJob
|
||||||
|
/// - If a failure is already recorded:
|
||||||
|
/// - If it is the same job, no-op (idempotent)
|
||||||
|
/// - If it is a different job, return FlowFailed with the already-failed job
|
||||||
|
/// - Otherwise record this job as the failed job
|
||||||
|
pub fn mark_job_failed(&mut self, job: u32) -> DagResult<()> {
|
||||||
|
if !self.nodes.contains_key(&job) {
|
||||||
|
return Err(DagError::UnknownJob { job });
|
||||||
|
}
|
||||||
|
match self.failed_job {
|
||||||
|
Some(existing) if existing == job => Ok(()),
|
||||||
|
Some(existing) => Err(DagError::FlowFailed { failed_job: existing }),
|
||||||
|
None => {
|
||||||
|
self.failed_job = Some(job);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
20
src/rpc.rs
20
src/rpc.rs
@@ -59,6 +59,26 @@ fn dag_err(e: DagError) -> ErrorObjectOwned {
|
|||||||
"DAG Cycle Detected",
|
"DAG Cycle Detected",
|
||||||
Some(Value::String(e.to_string())),
|
Some(Value::String(e.to_string())),
|
||||||
),
|
),
|
||||||
|
DagError::UnknownJob { .. } => ErrorObjectOwned::owned(
|
||||||
|
-32022,
|
||||||
|
"DAG Unknown Job",
|
||||||
|
Some(Value::String(e.to_string())),
|
||||||
|
),
|
||||||
|
DagError::DependenciesIncomplete { .. } => ErrorObjectOwned::owned(
|
||||||
|
-32023,
|
||||||
|
"DAG Dependencies Incomplete",
|
||||||
|
Some(Value::String(e.to_string())),
|
||||||
|
),
|
||||||
|
DagError::FlowFailed { .. } => ErrorObjectOwned::owned(
|
||||||
|
-32024,
|
||||||
|
"DAG Flow Failed",
|
||||||
|
Some(Value::String(e.to_string())),
|
||||||
|
),
|
||||||
|
DagError::JobNotStarted { .. } => ErrorObjectOwned::owned(
|
||||||
|
-32025,
|
||||||
|
"DAG Job Not Started",
|
||||||
|
Some(Value::String(e.to_string())),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user