use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use petgraph::algo::tarjan_scc;
use petgraph::graphmap::DiGraphMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::error::WyrdError;
use super::ids::TaskId;
use super::task::EvalTask;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ExecutionPlan {
pub stages: Vec<Stage>,
pub task_count: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Stage {
pub index: u32,
pub tasks: Vec<TaskId>,
}
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum DagError {
#[error("dag has a cycle: {cycle:?}")]
Cycle {
cycle: Vec<TaskId>,
},
#[error("task {task:?} depends on missing task {dep:?}")]
MissingDependency {
task: TaskId,
dep: TaskId,
},
#[error("task {task:?} depends on itself")]
SelfLoop {
task: TaskId,
},
}
impl From<DagError> for WyrdError {
fn from(error: DagError) -> Self {
let details = match &error {
DagError::Cycle { cycle } => serde_json::json!({
"kind": "cycle",
"cycle": cycle.iter().map(TaskId::as_str).collect::<Vec<_>>(),
}),
DagError::MissingDependency { task, dep } => serde_json::json!({
"kind": "missing_dependency",
"task": task.as_str(),
"dep": dep.as_str(),
}),
DagError::SelfLoop { task } => serde_json::json!({
"kind": "self_loop",
"task": task.as_str(),
}),
};
WyrdError::ValaTaskDagInvalid {
message: error.to_string(),
details,
}
}
}
pub fn validate_dag(tasks: &BTreeMap<TaskId, EvalTask>) -> Result<ExecutionPlan, DagError> {
let mut in_degree: HashMap<&TaskId, u32> = HashMap::with_capacity(tasks.len());
let mut adj: HashMap<&TaskId, Vec<&TaskId>> = HashMap::with_capacity(tasks.len());
for id in tasks.keys() {
in_degree.insert(id, 0);
adj.insert(id, Vec::new());
}
for (id, task) in tasks {
for dep in task.depends_on() {
if dep == id {
return Err(DagError::SelfLoop { task: id.clone() });
}
let Some(downstream_tasks) = adj.get_mut(dep) else {
return Err(DagError::MissingDependency {
task: id.clone(),
dep: dep.clone(),
});
};
downstream_tasks.push(id);
if let Some(degree) = in_degree.get_mut(id) {
*degree += 1;
}
}
}
let mut plan = Vec::new();
let mut emitted: usize = 0;
let mut ready: Vec<&TaskId> = in_degree
.iter()
.filter(|(_, degree)| **degree == 0)
.map(|(id, _)| *id)
.collect();
ready.sort();
let mut stage_idx: u32 = 0;
while !ready.is_empty() {
let stage_tasks: Vec<TaskId> = ready.iter().map(|task| (*task).clone()).collect();
emitted = emitted.saturating_add(stage_tasks.len());
plan.push(Stage {
index: stage_idx,
tasks: stage_tasks,
});
stage_idx = stage_idx.saturating_add(1);
let mut next_ready = Vec::new();
for id in ready {
if let Some(downstreams) = adj.get(id) {
for &downstream in downstreams {
if let Some(degree) = in_degree.get_mut(downstream) {
*degree -= 1;
if *degree == 0 {
next_ready.push(downstream);
}
}
}
}
}
next_ready.sort();
ready = next_ready;
}
if emitted == tasks.len() {
return Ok(ExecutionPlan {
stages: plan,
task_count: emitted,
});
}
let remaining: HashSet<&TaskId> = in_degree
.iter()
.filter(|(_, degree)| **degree > 0)
.map(|(id, _)| *id)
.collect();
let cycle = smallest_cycle(&adj, &remaining);
Err(DagError::Cycle { cycle })
}
fn smallest_cycle<'a>(
adj: &HashMap<&'a TaskId, Vec<&'a TaskId>>,
remaining: &HashSet<&'a TaskId>,
) -> Vec<TaskId> {
let mut graph: DiGraphMap<&TaskId, ()> = DiGraphMap::new();
for vertex in remaining {
graph.add_node(*vertex);
}
for vertex in remaining {
for &edge in &adj[vertex] {
if remaining.contains(edge) {
graph.add_edge(*vertex, edge, ());
}
}
}
let mut sccs: Vec<Vec<&TaskId>> = tarjan_scc(&graph)
.into_iter()
.filter(|scc| scc.len() >= 2 || (scc.len() == 1 && self_loops_to(adj, scc[0])))
.collect();
sccs.sort_by(|left, right| {
left.len()
.cmp(&right.len())
.then_with(|| left.iter().min().cmp(&right.iter().min()))
});
let Some(scc) = sccs.first() else {
return Vec::new();
};
let scc_set: HashSet<&TaskId> = scc.iter().copied().collect();
let Some(&start) = scc.iter().min() else {
return Vec::new();
};
let mut parent: HashMap<&TaskId, &TaskId> = HashMap::new();
let mut queue: VecDeque<&TaskId> = VecDeque::new();
queue.push_back(start);
parent.insert(start, start);
let mut closing: Option<&TaskId> = None;
'bfs: while let Some(vertex) = queue.pop_front() {
for &edge in &adj[vertex] {
if !scc_set.contains(edge) {
continue;
}
if edge == start {
closing = Some(vertex);
break 'bfs;
}
if !parent.contains_key(edge) {
parent.insert(edge, vertex);
queue.push_back(edge);
}
}
}
let mut walk_rev = Vec::new();
if let Some(last) = closing {
let mut current = last;
while current != start {
walk_rev.push(current);
current = parent[current];
}
}
let mut walk: Vec<&TaskId> = vec![start];
walk.extend(walk_rev.into_iter().rev());
walk.push(start);
walk.into_iter().cloned().collect()
}
fn self_loops_to(adj: &HashMap<&TaskId, Vec<&TaskId>>, vertex: &TaskId) -> bool {
adj.get(vertex).is_some_and(|edges| edges.contains(&vertex))
}