Skip to main content

tatara_core/domain/
dag.rs

1//! Job dependency DAG types and topological sort.
2
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet, VecDeque};
5
6/// A dependency on another job.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct JobDependency {
9    /// ID of the job this depends on.
10    pub job_id: String,
11
12    /// Condition that must be met.
13    #[serde(default)]
14    pub condition: DependencyCondition,
15}
16
17/// What condition the dependency must meet.
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
19#[serde(rename_all = "snake_case")]
20pub enum DependencyCondition {
21    /// Dependency must have at least one healthy running allocation.
22    #[default]
23    Healthy,
24    /// Dependency must have completed (for batch jobs).
25    Complete,
26    /// Dependency must have produced an output.
27    OutputReady,
28}
29
30/// An output produced by a job (e.g., a Nix store path).
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct JobOutput {
33    pub key: String,
34    pub value: String,
35}
36
37/// Topological sort of job IDs using Kahn's algorithm.
38/// Returns sorted order or an error listing the cycle participants.
39pub fn topological_sort(
40    job_ids: &[String],
41    dependencies: &HashMap<String, Vec<String>>,
42) -> Result<Vec<String>, Vec<String>> {
43    let mut in_degree: HashMap<&str, usize> = HashMap::new();
44    let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
45
46    for id in job_ids {
47        in_degree.entry(id.as_str()).or_insert(0);
48        adj.entry(id.as_str()).or_default();
49    }
50
51    let job_set: HashSet<&str> = job_ids.iter().map(|s| s.as_str()).collect();
52
53    for (job_id, deps) in dependencies {
54        for dep in deps {
55            // Validate that dependencies reference existing jobs
56            if !job_set.contains(dep.as_str()) {
57                return Err(vec![format!(
58                    "job '{}' depends on unknown job '{}'",
59                    job_id, dep
60                )]);
61            }
62            adj.entry(dep.as_str()).or_default().push(job_id.as_str());
63            *in_degree.entry(job_id.as_str()).or_insert(0) += 1;
64        }
65    }
66
67    let mut queue: VecDeque<&str> = in_degree
68        .iter()
69        .filter(|(_, &deg)| deg == 0)
70        .map(|(&id, _)| id)
71        .collect();
72
73    let mut sorted = Vec::new();
74
75    while let Some(node) = queue.pop_front() {
76        sorted.push(node.to_string());
77        if let Some(neighbors) = adj.get(node) {
78            for &neighbor in neighbors {
79                if let Some(deg) = in_degree.get_mut(neighbor) {
80                    *deg -= 1;
81                    if *deg == 0 {
82                        queue.push_back(neighbor);
83                    }
84                }
85            }
86        }
87    }
88
89    if sorted.len() == job_ids.len() {
90        Ok(sorted)
91    } else {
92        let cycle: Vec<String> = in_degree
93            .iter()
94            .filter(|(_, &deg)| deg > 0)
95            .map(|(&id, _)| id.to_string())
96            .collect();
97        Err(cycle)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_simple_dag() {
107        let ids = vec!["a".into(), "b".into(), "c".into()];
108        let mut deps = HashMap::new();
109        deps.insert("b".into(), vec!["a".into()]);
110        deps.insert("c".into(), vec!["b".into()]);
111
112        let sorted = topological_sort(&ids, &deps).unwrap();
113        assert_eq!(sorted, vec!["a", "b", "c"]);
114    }
115
116    #[test]
117    fn test_parallel_deps() {
118        let ids = vec!["a".into(), "b".into(), "c".into(), "d".into()];
119        let mut deps = HashMap::new();
120        deps.insert("c".into(), vec!["a".into(), "b".into()]);
121        deps.insert("d".into(), vec!["c".into()]);
122
123        let sorted = topological_sort(&ids, &deps).unwrap();
124        // a and b can be in any order, but must come before c, which comes before d
125        let pos_a = sorted.iter().position(|x| x == "a").unwrap();
126        let pos_b = sorted.iter().position(|x| x == "b").unwrap();
127        let pos_c = sorted.iter().position(|x| x == "c").unwrap();
128        let pos_d = sorted.iter().position(|x| x == "d").unwrap();
129        assert!(pos_a < pos_c);
130        assert!(pos_b < pos_c);
131        assert!(pos_c < pos_d);
132    }
133
134    #[test]
135    fn test_cycle_detection() {
136        let ids = vec!["a".into(), "b".into(), "c".into()];
137        let mut deps = HashMap::new();
138        deps.insert("a".into(), vec!["c".into()]);
139        deps.insert("b".into(), vec!["a".into()]);
140        deps.insert("c".into(), vec!["b".into()]);
141
142        let result = topological_sort(&ids, &deps);
143        assert!(result.is_err());
144    }
145
146    #[test]
147    fn test_no_deps() {
148        let ids = vec!["a".into(), "b".into(), "c".into()];
149        let deps = HashMap::new();
150
151        let sorted = topological_sort(&ids, &deps).unwrap();
152        assert_eq!(sorted.len(), 3);
153    }
154
155    #[test]
156    fn test_invalid_dependency() {
157        let ids = vec!["a".into(), "b".into()];
158        let mut deps = HashMap::new();
159        deps.insert("b".into(), vec!["nonexistent".into()]);
160
161        let result = topological_sort(&ids, &deps);
162        assert!(result.is_err());
163        let err = result.unwrap_err();
164        assert!(err[0].contains("unknown job"));
165    }
166
167    #[test]
168    fn test_self_loop() {
169        let ids = vec!["a".into()];
170        let mut deps = HashMap::new();
171        deps.insert("a".into(), vec!["a".into()]);
172
173        let result = topological_sort(&ids, &deps);
174        assert!(result.is_err());
175    }
176
177    #[test]
178    fn test_single_node() {
179        let ids = vec!["only".into()];
180        let deps = HashMap::new();
181        let sorted = topological_sort(&ids, &deps).unwrap();
182        assert_eq!(sorted, vec!["only"]);
183    }
184}