use std::collections::{HashMap, HashSet, VecDeque};
use crate::dag::cache::CacheStrategy;
use crate::dag::error::{DagError, Result};
use crate::dag::types::{ConcurrencyLimit, DagTask, TaskId};
pub struct DagGraph {
pub(crate) tasks: HashMap<TaskId, Box<dyn DagTask>>,
pub(crate) dependents: HashMap<TaskId, HashSet<TaskId>>,
pub(crate) dependencies: HashMap<TaskId, HashSet<TaskId>>,
pub(crate) concurrency_groups: HashMap<String, ConcurrencyLimit>,
pub(crate) task_groups: HashMap<TaskId, String>,
pub(crate) task_cache: HashMap<TaskId, Box<dyn CacheStrategy>>,
}
impl DagGraph {
pub fn new() -> Self {
Self {
tasks: HashMap::new(),
dependents: HashMap::new(),
dependencies: HashMap::new(),
concurrency_groups: HashMap::new(),
task_groups: HashMap::new(),
task_cache: HashMap::new(),
}
}
pub fn add_task(&mut self, id: TaskId, task: Box<dyn DagTask>) -> Result<()> {
if self.tasks.contains_key(&id) {
return Err(DagError::DuplicateTask(id));
}
self.tasks.insert(id.clone(), task);
self.dependents.entry(id.clone()).or_default();
self.dependencies.entry(id).or_default();
Ok(())
}
pub fn add_dep(&mut self, dependent: &TaskId, dependency: &TaskId) -> Result<()> {
if !self.tasks.contains_key(dependent) {
return Err(DagError::UnknownTask(dependent.clone()));
}
if !self.tasks.contains_key(dependency) {
return Err(DagError::UnknownTask(dependency.clone()));
}
self.dependents
.entry(dependency.clone())
.or_default()
.insert(dependent.clone());
self.dependencies
.entry(dependent.clone())
.or_default()
.insert(dependency.clone());
Ok(())
}
pub fn set_concurrency_group(&mut self, name: &str, max_concurrent: usize) {
self.concurrency_groups.insert(
name.to_string(),
ConcurrencyLimit {
max_concurrent: Some(max_concurrent),
},
);
}
pub fn assign_group(&mut self, task_id: &TaskId, group: &str) {
self.task_groups.insert(task_id.clone(), group.to_string());
}
pub fn validate(&self) -> Result<()> {
if self.tasks.is_empty() {
return Err(DagError::EmptyDag);
}
let mut in_degree: HashMap<&TaskId, usize> = HashMap::new();
for id in self.tasks.keys() {
let deps = self.dependencies.get(id).map(|d| d.len()).unwrap_or(0);
in_degree.insert(id, deps);
}
let mut queue: VecDeque<&TaskId> = in_degree
.iter()
.filter(|(_, °)| deg == 0)
.map(|(&id, _)| id)
.collect();
let mut visited = 0;
while let Some(id) = queue.pop_front() {
visited += 1;
if let Some(deps) = self.dependents.get(id) {
for dep in deps {
if let Some(degree) = in_degree.get_mut(dep) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dep);
}
}
}
}
}
if visited != self.tasks.len() {
let cycle_task = in_degree
.iter()
.find(|(_, °)| deg > 0)
.map(|(&id, _)| id.clone())
.unwrap_or_else(|| TaskId("unknown".into()));
return Err(DagError::CycleDetected(cycle_task));
}
Ok(())
}
pub fn roots(&self) -> Vec<TaskId> {
self.tasks
.keys()
.filter(|id| {
self.dependencies
.get(*id)
.map(|d| d.is_empty())
.unwrap_or(true)
})
.cloned()
.collect()
}
pub fn topo_sort(&self) -> Result<Vec<TaskId>> {
let mut in_degree: HashMap<&TaskId, usize> = HashMap::new();
for id in self.tasks.keys() {
let deps = self.dependencies.get(id).map(|d| d.len()).unwrap_or(0);
in_degree.insert(id, deps);
}
let mut queue: VecDeque<&TaskId> = in_degree
.iter()
.filter(|(_, °)| deg == 0)
.map(|(&id, _)| id)
.collect();
let mut order = Vec::new();
while let Some(id) = queue.pop_front() {
order.push(id.clone());
if let Some(deps) = self.dependents.get(id) {
for dep in deps {
if let Some(degree) = in_degree.get_mut(dep) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dep);
}
}
}
}
}
if order.len() != self.tasks.len() {
let cycle_task = in_degree
.iter()
.find(|(_, °)| deg > 0)
.map(|(&id, _)| id.clone())
.unwrap_or_else(|| TaskId("unknown".into()));
return Err(DagError::CycleDetected(cycle_task));
}
Ok(order)
}
pub fn take_task(&mut self, id: &TaskId) -> Option<Box<dyn DagTask>> {
self.tasks.remove(id)
}
pub fn len(&self) -> usize {
self.tasks.len()
}
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::context::DagContext;
use crate::dag::types::{FnTask, TaskOutcome};
fn noop_task(name: &str) -> Box<dyn DagTask> {
let n = name.to_string();
Box::new(FnTask::new(n, |_| TaskOutcome::Success))
}
#[test]
fn test_empty_dag() {
let g = DagGraph::new();
assert!(matches!(g.validate(), Err(DagError::EmptyDag)));
}
#[test]
fn test_single_task() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
assert!(g.validate().is_ok());
assert_eq!(g.roots(), vec![TaskId::from("a")]);
}
#[test]
fn test_linear_chain() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
g.add_task(TaskId::from("b"), noop_task("b")).unwrap();
g.add_task(TaskId::from("c"), noop_task("c")).unwrap();
g.add_dep(&TaskId::from("b"), &TaskId::from("a")).unwrap();
g.add_dep(&TaskId::from("c"), &TaskId::from("b")).unwrap();
assert!(g.validate().is_ok());
assert_eq!(g.roots(), vec![TaskId::from("a")]);
let order = g.topo_sort().unwrap();
let a_pos = order.iter().position(|t| t.0 == "a").unwrap();
let b_pos = order.iter().position(|t| t.0 == "b").unwrap();
let c_pos = order.iter().position(|t| t.0 == "c").unwrap();
assert!(a_pos < b_pos);
assert!(b_pos < c_pos);
}
#[test]
fn test_diamond_dag() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
g.add_task(TaskId::from("b"), noop_task("b")).unwrap();
g.add_task(TaskId::from("c"), noop_task("c")).unwrap();
g.add_task(TaskId::from("d"), noop_task("d")).unwrap();
g.add_dep(&TaskId::from("b"), &TaskId::from("a")).unwrap();
g.add_dep(&TaskId::from("c"), &TaskId::from("a")).unwrap();
g.add_dep(&TaskId::from("d"), &TaskId::from("b")).unwrap();
g.add_dep(&TaskId::from("d"), &TaskId::from("c")).unwrap();
assert!(g.validate().is_ok());
let order = g.topo_sort().unwrap();
let a_pos = order.iter().position(|t| t.0 == "a").unwrap();
let d_pos = order.iter().position(|t| t.0 == "d").unwrap();
assert!(a_pos < d_pos);
}
#[test]
fn test_cycle_detection() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
g.add_task(TaskId::from("b"), noop_task("b")).unwrap();
g.add_dep(&TaskId::from("b"), &TaskId::from("a")).unwrap();
g.add_dep(&TaskId::from("a"), &TaskId::from("b")).unwrap();
assert!(matches!(g.validate(), Err(DagError::CycleDetected(_))));
}
#[test]
fn test_duplicate_task() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
assert!(matches!(
g.add_task(TaskId::from("a"), noop_task("a")),
Err(DagError::DuplicateTask(_))
));
}
#[test]
fn test_unknown_dep() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
assert!(matches!(
g.add_dep(&TaskId::from("a"), &TaskId::from("missing")),
Err(DagError::UnknownTask(_))
));
}
#[test]
fn test_self_loop_cycle() {
let mut g = DagGraph::new();
g.add_task(TaskId::from("a"), noop_task("a")).unwrap();
g.add_dep(&TaskId::from("a"), &TaskId::from("a")).unwrap();
assert!(matches!(g.validate(), Err(DagError::CycleDetected(_))));
}
#[test]
fn test_deep_cycle() {
let mut g = DagGraph::new();
for name in &["a", "b", "c", "d", "e"] {
g.add_task(TaskId::from(*name), noop_task(name)).unwrap();
}
g.add_dep(&TaskId::from("b"), &TaskId::from("a")).unwrap();
g.add_dep(&TaskId::from("c"), &TaskId::from("b")).unwrap();
g.add_dep(&TaskId::from("d"), &TaskId::from("c")).unwrap();
g.add_dep(&TaskId::from("e"), &TaskId::from("d")).unwrap();
g.add_dep(&TaskId::from("a"), &TaskId::from("e")).unwrap(); assert!(matches!(g.validate(), Err(DagError::CycleDetected(_))));
}
#[test]
fn test_large_dag_performance() {
let mut g = DagGraph::new();
for i in 0..1000 {
let id = format!("t{}", i);
g.add_task(TaskId::from(id.as_str()), noop_task(&id))
.unwrap();
if i > 0 {
let prev = format!("t{}", i - 1);
g.add_dep(&TaskId::from(id.as_str()), &TaskId::from(prev.as_str()))
.unwrap();
}
}
let start = std::time::Instant::now();
assert!(g.validate().is_ok());
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(1),
"1000-task DAG validation took {:?}",
elapsed
);
let order = g.topo_sort().unwrap();
assert_eq!(order.len(), 1000);
assert_eq!(order[0], TaskId::from("t0"));
assert_eq!(order[999], TaskId::from("t999"));
}
}