harness_loop/orchestrator/
dag.rs1use crate::orchestrator::job::{Job, JobId, JobState};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct Dag {
13 jobs: HashMap<JobId, Job>,
14}
15
16impl Dag {
17 pub fn new() -> Self {
18 Self {
19 jobs: HashMap::new(),
20 }
21 }
22
23 pub fn from_jobs(jobs: impl IntoIterator<Item = Job>) -> Self {
25 let mut d = Dag::new();
26 for j in jobs {
27 d.jobs.insert(j.id.clone(), j);
28 }
29 d
30 }
31
32 pub fn add(&mut self, job: Job) {
33 self.jobs.insert(job.id.clone(), job);
34 }
35
36 pub fn get(&self, id: &str) -> Option<&Job> {
37 self.jobs.get(id)
38 }
39 pub fn get_mut(&mut self, id: &str) -> Option<&mut Job> {
40 self.jobs.get_mut(id)
41 }
42 pub fn len(&self) -> usize {
43 self.jobs.len()
44 }
45 pub fn is_empty(&self) -> bool {
46 self.jobs.is_empty()
47 }
48 pub fn ids(&self) -> Vec<JobId> {
49 self.jobs.keys().cloned().collect()
50 }
51 pub fn jobs_mut(&mut self) -> impl Iterator<Item = &mut Job> {
52 self.jobs.values_mut()
53 }
54
55 pub fn jobs(&self) -> impl Iterator<Item = &Job> {
56 self.jobs.values()
57 }
58
59 pub fn deps_satisfied(&self, job: &Job) -> bool {
61 job.deps.iter().all(|d| {
62 self.jobs
63 .get(d)
64 .map(|j| j.state == JobState::Succeeded)
65 .unwrap_or(false)
66 })
67 }
68
69 pub fn deps_blocked(&self, job: &Job) -> bool {
72 job.deps.iter().any(|d| {
73 self.jobs
74 .get(d)
75 .map(|j| matches!(j.state, JobState::DeadLettered | JobState::Cancelled))
76 .unwrap_or(true) })
78 }
79
80 pub fn succeeded_results(&self) -> Vec<(JobId, crate::orchestrator::job::JobResult)> {
83 self.jobs
84 .values()
85 .filter(|j| j.state == JobState::Succeeded)
86 .filter_map(|j| j.result.clone().map(|r| (j.id.clone(), r)))
87 .collect()
88 }
89
90 pub fn unknown_deps(&self) -> Vec<(JobId, JobId)> {
99 self.jobs()
100 .flat_map(|j| {
101 j.deps
102 .iter()
103 .filter(|d| !self.jobs.contains_key(*d))
104 .map(move |d| (j.id.clone(), d.clone()))
105 })
106 .collect()
107 }
108
109 pub fn downstream_of(&self, id: &str) -> Vec<JobId> {
116 let mut out: Vec<JobId> = vec![id.into()];
117 let mut i = 0;
118 while i < out.len() {
119 let current = out[i].clone();
120 for j in self.jobs() {
121 if j.deps.contains(¤t) && !out.contains(&j.id) {
122 out.push(j.id.clone());
123 }
124 }
125 i += 1;
126 }
127 out
128 }
129
130 pub fn find_cycle(&self) -> Option<Vec<JobId>> {
131 #[derive(Clone, Copy, PartialEq)]
132 enum Mark {
133 White,
134 Gray,
135 Black,
136 }
137 let mut mark: HashMap<&str, Mark> = self
138 .jobs
139 .keys()
140 .map(|k| (k.as_str(), Mark::White))
141 .collect();
142 let mut stack: Vec<&str> = Vec::new();
143
144 fn dfs<'a>(
145 node: &'a str,
146 jobs: &'a HashMap<JobId, Job>,
147 mark: &mut HashMap<&'a str, Mark>,
148 stack: &mut Vec<&'a str>,
149 ) -> Option<Vec<JobId>> {
150 mark.insert(node, Mark::Gray);
151 stack.push(node);
152 if let Some(j) = jobs.get(node) {
153 for dep in &j.deps {
154 match mark.get(dep.as_str()).copied() {
155 Some(Mark::Gray) => {
156 let from = stack.iter().position(|n| *n == dep.as_str()).unwrap_or(0);
158 return Some(stack[from..].iter().map(|s| s.to_string()).collect());
159 }
160 Some(Mark::White) => {
161 if let Some(c) = dfs(dep.as_str(), jobs, mark, stack) {
162 return Some(c);
163 }
164 }
165 _ => {}
166 }
167 }
168 }
169 stack.pop();
170 mark.insert(node, Mark::Black);
171 None
172 }
173
174 let ids: Vec<&str> = self.jobs.keys().map(|s| s.as_str()).collect();
175 for id in ids {
176 if mark.get(id).copied() == Some(Mark::White)
177 && let Some(c) = dfs(id, &self.jobs, &mut mark, &mut stack)
178 {
179 return Some(c);
180 }
181 }
182 None
183 }
184}
185
186#[derive(Debug, Clone)]
189pub enum PlanDelta {
190 Add(Vec<Job>),
192 Done,
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn j(id: &str, deps: &[&str]) -> Job {
201 Job::new(id, format!("do {id}")).with_deps(deps.iter().copied())
202 }
203
204 #[test]
205 fn deps_satisfied_only_when_all_succeeded() {
206 let mut d = Dag::from_jobs([j("a", &[]), j("b", &[]), j("c", &["a", "b"])]);
207 let c = d.get("c").unwrap().clone();
208 assert!(!d.deps_satisfied(&c));
209 d.get_mut("a").unwrap().state = JobState::Succeeded;
210 assert!(!d.deps_satisfied(&c));
211 d.get_mut("b").unwrap().state = JobState::Succeeded;
212 assert!(d.deps_satisfied(&c));
213 }
214
215 #[test]
216 fn dead_dep_blocks_downstream() {
217 let mut d = Dag::from_jobs([j("a", &[]), j("c", &["a"])]);
218 d.get_mut("a").unwrap().state = JobState::DeadLettered;
219 let c = d.get("c").unwrap().clone();
220 assert!(d.deps_blocked(&c));
221 }
222
223 #[test]
224 fn detects_cycle() {
225 let d = Dag::from_jobs([j("a", &["c"]), j("b", &["a"]), j("c", &["b"])]);
226 assert!(d.find_cycle().is_some());
227 let ok = Dag::from_jobs([j("a", &[]), j("b", &["a"]), j("c", &["b"])]);
228 assert!(ok.find_cycle().is_none());
229 }
230}