1use crate::Result;
2use crate::daemon::Daemon;
3use crate::daemon_id::DaemonId;
4use crate::env;
5use crate::error::{DependencyError, find_similar_daemon};
6use crate::pitchfork_toml::{PitchforkToml, PitchforkTomlDaemon};
7use crate::state_file::StateFile;
8use indexmap::IndexMap;
9use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
10use std::path::{Path, PathBuf};
11
12#[derive(Debug)]
14pub struct DependencyOrder {
15 pub levels: Vec<Vec<DaemonId>>,
18}
19
20pub fn resolve_dependencies(
29 requested: &[DaemonId],
30 all_daemons: &IndexMap<DaemonId, PitchforkTomlDaemon>,
31) -> Result<DependencyOrder> {
32 let mut to_start: HashSet<DaemonId> = HashSet::new();
34 let mut queue: VecDeque<DaemonId> = requested.iter().cloned().collect();
35
36 while let Some(id) = queue.pop_front() {
37 if to_start.contains(&id) {
38 continue;
39 }
40
41 let daemon = all_daemons.get(&id).ok_or_else(|| {
42 let suggestion = find_similar_daemon(
43 &id.qualified(),
44 all_daemons
45 .keys()
46 .map(|k| k.qualified())
47 .collect::<Vec<_>>()
48 .iter()
49 .map(|s| s.as_str()),
50 );
51 DependencyError::DaemonNotFound {
52 name: id.qualified(),
53 suggestion,
54 }
55 })?;
56
57 to_start.insert(id.clone());
58
59 for dep in &daemon.depends {
61 if !all_daemons.contains_key(dep) {
62 return Err(DependencyError::MissingDependency {
63 daemon: id.qualified(),
64 dependency: dep.qualified(),
65 }
66 .into());
67 }
68 if !to_start.contains(dep) {
69 queue.push_back(dep.clone());
70 }
71 }
72 }
73
74 let mut in_degree: HashMap<DaemonId, usize> = HashMap::new();
76 let mut dependents: HashMap<DaemonId, Vec<DaemonId>> = HashMap::new();
77
78 for id in &to_start {
79 in_degree.entry(id.clone()).or_insert(0);
80 dependents.entry(id.clone()).or_default();
81 }
82
83 for id in &to_start {
84 let daemon = all_daemons.get(id).ok_or_else(|| {
85 miette::miette!("Internal error: daemon '{}' missing from configuration", id)
86 })?;
87 for dep in &daemon.depends {
88 if to_start.contains(dep) {
89 *in_degree.get_mut(id).ok_or_else(|| {
90 miette::miette!("Internal error: in_degree missing for daemon '{}'", id)
91 })? += 1;
92 dependents
93 .get_mut(dep)
94 .ok_or_else(|| {
95 miette::miette!("Internal error: dependents missing for daemon '{}'", dep)
96 })?
97 .push(id.clone());
98 }
99 }
100 }
101
102 let mut processed: HashSet<DaemonId> = HashSet::new();
104 let mut levels: Vec<Vec<DaemonId>> = Vec::new();
105 let mut current_level: Vec<DaemonId> = in_degree
106 .iter()
107 .filter(|(_, deg)| **deg == 0)
108 .map(|(id, _)| id.clone())
109 .collect();
110
111 current_level.sort();
113
114 while !current_level.is_empty() {
115 let mut next_level = Vec::new();
116
117 for id in ¤t_level {
118 processed.insert(id.clone());
119
120 let deps = dependents.get(id).ok_or_else(|| {
121 miette::miette!("Internal error: dependents missing for daemon '{}'", id)
122 })?;
123 for dependent in deps {
124 let deg = in_degree.get_mut(dependent).ok_or_else(|| {
125 miette::miette!(
126 "Internal error: in_degree missing for daemon '{}'",
127 dependent
128 )
129 })?;
130 *deg -= 1;
131 if *deg == 0 {
132 next_level.push(dependent.clone());
133 }
134 }
135 }
136
137 levels.push(current_level);
138 next_level.sort(); current_level = next_level;
140 }
141
142 if processed.len() != to_start.len() {
144 let mut involved: Vec<_> = to_start
145 .difference(&processed)
146 .map(|id| id.qualified())
147 .collect();
148 involved.sort(); return Err(DependencyError::CircularDependency { involved }.into());
150 }
151
152 Ok(DependencyOrder { levels })
153}
154
155pub fn compute_reverse_stop_order(active_ids: &[DaemonId]) -> Vec<Vec<DaemonId>> {
170 if active_ids.is_empty() {
171 return Vec::new();
172 }
173 match StateFile::read(&*env::PITCHFORK_STATE_FILE) {
174 Ok(state) => reverse_stop_order(active_ids, &state.daemons),
175 Err(e) => {
176 warn!(
177 "failed to read state for dependency-ordered shutdown, stopping in arbitrary order: {e}"
178 );
179 vec![active_ids.to_vec()]
180 }
181 }
182}
183
184pub fn reverse_stop_order(
199 active_ids: &[DaemonId],
200 daemons: &BTreeMap<DaemonId, Daemon>,
201) -> Vec<Vec<DaemonId>> {
202 let mut configs: HashMap<PathBuf, Option<PitchforkToml>> = HashMap::new();
203 reverse_stop_order_with(active_ids, daemons, &mut |project, id| {
204 configs
205 .entry(project.to_path_buf())
206 .or_insert_with(|| PitchforkToml::all_merged_all_namespaces_from_blocking(project).ok())
207 .as_ref()
208 .and_then(|pt| pt.daemons.get(id))
209 .map(|d| d.depends.clone())
210 .unwrap_or_default()
211 })
212}
213
214fn reverse_stop_order_with(
217 active_ids: &[DaemonId],
218 daemons: &BTreeMap<DaemonId, Daemon>,
219 unrecorded: &mut dyn FnMut(&Path, &DaemonId) -> Vec<DaemonId>,
220) -> Vec<Vec<DaemonId>> {
221 let mut ids: Vec<&DaemonId> = Vec::new();
222 let mut index: HashMap<&DaemonId, usize> = HashMap::new();
223 for id in active_ids {
224 if !index.contains_key(id) {
225 index.insert(id, ids.len());
226 ids.push(id);
227 }
228 }
229
230 let needs: Vec<Vec<usize>> = ids
235 .iter()
236 .map(|id| stopped_dependencies(id, daemons, &index, unrecorded))
237 .collect();
238 let mut needed_by = vec![0usize; ids.len()];
239 for deps in &needs {
240 for &dep in deps {
241 needed_by[dep] += 1;
242 }
243 }
244
245 let mut levels: Vec<Vec<DaemonId>> = Vec::new();
246 let mut placed = vec![false; ids.len()];
247 let mut level: Vec<usize> = (0..ids.len()).filter(|&i| needed_by[i] == 0).collect();
248 while !level.is_empty() {
249 let mut next = Vec::new();
250 for &i in &level {
251 placed[i] = true;
252 for &dep in &needs[i] {
253 needed_by[dep] -= 1;
254 if needed_by[dep] == 0 {
255 next.push(dep);
256 }
257 }
258 }
259 levels.push(level.iter().map(|&i| ids[i].clone()).collect());
260 next.sort_unstable();
261 level = next;
262 }
263 let cycle: Vec<DaemonId> = (0..ids.len())
264 .filter(|&i| !placed[i])
265 .map(|i| ids[i].clone())
266 .collect();
267 if !cycle.is_empty() {
268 levels.push(cycle);
269 }
270
271 debug!("shutdown order: {levels:?}");
272 levels
273}
274
275fn stopped_dependencies(
281 id: &DaemonId,
282 daemons: &BTreeMap<DaemonId, Daemon>,
283 index: &HashMap<&DaemonId, usize>,
284 unrecorded: &mut dyn FnMut(&Path, &DaemonId) -> Vec<DaemonId>,
285) -> Vec<usize> {
286 let mut found = Vec::new();
287 let mut seen: HashSet<DaemonId> = HashSet::from([id.clone()]);
288 let mut queue: VecDeque<(DaemonId, Option<PathBuf>)> = VecDeque::from([(id.clone(), None)]);
289 while let Some((current, reached_from)) = queue.pop_front() {
290 let (depends, project) = match daemons.get(¤t) {
291 Some(daemon) => (
292 daemon.depends.clone(),
293 daemon.watch_base_dir.clone().or(reached_from),
294 ),
295 None => match reached_from {
296 Some(project) => (unrecorded(&project, ¤t), Some(project)),
297 None => continue,
298 },
299 };
300 for dep in depends {
301 if seen.contains(&dep) {
302 continue;
303 }
304 seen.insert(dep.clone());
305 match index.get(&dep) {
306 Some(&i) => found.push(i),
307 None => queue.push_back((dep, project.clone())),
308 }
309 }
310 }
311 found
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::daemon_id::DaemonId;
318 use crate::pitchfork_toml::PitchforkTomlDaemon;
319 use indexmap::IndexMap;
320
321 fn make_daemon(depends: Vec<&str>) -> PitchforkTomlDaemon {
325 PitchforkTomlDaemon {
326 run: "echo test".into(),
327 depends: depends
328 .into_iter()
329 .map(|s| DaemonId::new("global", s))
330 .collect(),
331 ..Default::default()
332 }
333 }
334
335 fn id(name: &str) -> DaemonId {
336 DaemonId::new("global", name)
337 }
338
339 fn records(entries: &[(DaemonId, &[DaemonId])]) -> BTreeMap<DaemonId, Daemon> {
340 entries
341 .iter()
342 .map(|(id, depends)| {
343 (
344 id.clone(),
345 Daemon {
346 id: id.clone(),
347 depends: depends.to_vec(),
348 ..Default::default()
349 },
350 )
351 })
352 .collect()
353 }
354
355 #[test]
356 fn test_stop_order_follows_recorded_dependencies_of_any_project() {
357 let db = DaemonId::new("projb", "db");
358 let app = DaemonId::new("projb", "app");
359 let daemons = records(&[(db.clone(), &[]), (app.clone(), std::slice::from_ref(&db))]);
360 assert_eq!(
361 reverse_stop_order(&[db.clone(), app.clone()], &daemons),
362 vec![vec![app], vec![db]]
363 );
364 }
365
366 #[test]
367 fn test_stop_order_goes_through_daemons_not_being_stopped() {
368 let daemons = records(&[
370 (id("cache"), &[]),
371 (id("api"), &[id("cache")]),
372 (id("web"), &[id("api")]),
373 ]);
374 assert_eq!(
375 reverse_stop_order(&[id("cache"), id("web")], &daemons),
376 vec![vec![id("web")], vec![id("cache")]]
377 );
378 }
379
380 #[test]
381 fn test_stop_order_puts_unrelated_and_unrecorded_daemons_first() {
382 let daemons = records(&[(id("db"), &[]), (id("app"), &[id("db")])]);
383 assert_eq!(
384 reverse_stop_order(&[id("db"), id("adhoc"), id("app")], &daemons),
385 vec![vec![id("adhoc"), id("app")], vec![id("db")]]
386 );
387 }
388
389 #[test]
390 fn test_stop_order_goes_through_a_daemon_without_a_record_by_its_config() {
391 let project = PathBuf::from("/project");
394 let mut daemons = records(&[(id("cache"), &[]), (id("web"), &[id("api")])]);
395 daemons.get_mut(&id("web")).unwrap().watch_base_dir = Some(project.clone());
396 let mut looked_up = Vec::new();
397 let levels =
398 reverse_stop_order_with(&[id("cache"), id("web")], &daemons, &mut |dir, daemon| {
399 looked_up.push((dir.to_path_buf(), daemon.clone()));
400 if *daemon == id("api") {
401 vec![id("cache")]
402 } else {
403 vec![]
404 }
405 });
406 assert_eq!(levels, vec![vec![id("web")], vec![id("cache")]]);
407 assert_eq!(looked_up, vec![(project, id("api"))]);
408 }
409
410 #[test]
411 fn test_stop_order_stops_a_cycle_together_last() {
412 let daemons = records(&[
413 (id("a"), &[id("b")]),
414 (id("b"), &[id("a")]),
415 (id("c"), &[id("a")]),
416 ]);
417 assert_eq!(
418 reverse_stop_order(&[id("a"), id("b"), id("c")], &daemons),
419 vec![vec![id("c")], vec![id("a"), id("b")]]
420 );
421 }
422
423 fn reference_stop_order(
427 active_ids: &[DaemonId],
428 daemons: &BTreeMap<DaemonId, Daemon>,
429 ) -> Vec<Vec<DaemonId>> {
430 let all_needs = |id: &DaemonId| {
431 let mut found = HashSet::new();
432 let mut queue = VecDeque::from([id.clone()]);
433 while let Some(current) = queue.pop_front() {
434 for dep in daemons
435 .get(¤t)
436 .map(|d| &d.depends)
437 .into_iter()
438 .flatten()
439 {
440 if found.insert(dep.clone()) {
441 queue.push_back(dep.clone());
442 }
443 }
444 }
445 found
446 };
447 let mut remaining: Vec<DaemonId> = Vec::new();
448 for id in active_ids {
449 if !remaining.contains(id) {
450 remaining.push(id.clone());
451 }
452 }
453 let needs: HashMap<DaemonId, HashSet<DaemonId>> = remaining
454 .iter()
455 .map(|id| (id.clone(), all_needs(id)))
456 .collect();
457 let mut levels = Vec::new();
458 while !remaining.is_empty() {
459 let level: Vec<DaemonId> = remaining
460 .iter()
461 .filter(|id| {
462 !remaining
463 .iter()
464 .any(|other| other != *id && needs[other].contains(*id))
465 })
466 .cloned()
467 .collect();
468 if level.is_empty() {
469 levels.push(std::mem::take(&mut remaining));
470 break;
471 }
472 remaining.retain(|id| !level.contains(id));
473 levels.push(level);
474 }
475 levels
476 }
477
478 #[test]
479 fn test_stop_order_matches_the_reference_on_random_graphs() {
480 let mut seed: u64 = 0x2545_f491_4f6c_dd1d;
482 let mut next = |n: u64| {
483 seed = seed
484 .wrapping_mul(6_364_136_223_846_793_005)
485 .wrapping_add(1_442_695_040_888_963_407);
486 (seed >> 33) % n
487 };
488 for _ in 0..500 {
489 let count = 1 + next(12);
490 let names: Vec<DaemonId> = (0..count).map(|i| id(&format!("d{i}"))).collect();
491 let mut deps: Vec<(DaemonId, Vec<DaemonId>)> = Vec::new();
492 for name in &names {
493 let links = (0..next(3))
495 .map(|_| names[next(count) as usize].clone())
496 .collect();
497 deps.push((name.clone(), links));
498 }
499 let daemons: BTreeMap<DaemonId, Daemon> = deps
500 .iter()
501 .filter(|_| next(5) != 0) .map(|(id, depends)| {
503 (
504 id.clone(),
505 Daemon {
506 id: id.clone(),
507 depends: depends.clone(),
508 ..Default::default()
509 },
510 )
511 })
512 .collect();
513 let active: Vec<DaemonId> = names
514 .iter()
515 .filter(|_| next(3) != 0) .cloned()
517 .collect();
518 assert_eq!(
519 reverse_stop_order(&active, &daemons),
520 reference_stop_order(&active, &daemons),
521 "active {active:?}, records {deps:?}"
522 );
523 }
524 }
525
526 #[test]
527 fn test_stop_order_of_a_long_chain() {
528 let names: Vec<DaemonId> = (0..5000).map(|i| id(&format!("d{i}"))).collect();
529 let daemons: BTreeMap<DaemonId, Daemon> = names
530 .iter()
531 .enumerate()
532 .map(|(i, name)| {
533 (
534 name.clone(),
535 Daemon {
536 id: name.clone(),
537 depends: names.get(i + 1).cloned().into_iter().collect(),
538 ..Default::default()
539 },
540 )
541 })
542 .collect();
543 let active: Vec<DaemonId> = names.iter().step_by(2).cloned().collect();
545 let levels = reverse_stop_order(&active, &daemons);
546 assert_eq!(
547 levels,
548 active.iter().map(|id| vec![id.clone()]).collect::<Vec<_>>()
549 );
550 }
551
552 #[test]
553 fn test_no_dependencies() {
554 let mut daemons = IndexMap::new();
555 daemons.insert(id("api"), make_daemon(vec![]));
556
557 let result = resolve_dependencies(&[id("api")], &daemons).unwrap();
558
559 assert_eq!(result.levels.len(), 1);
560 assert_eq!(result.levels[0], vec![id("api")]);
561 }
562
563 #[test]
564 fn test_simple_dependency() {
565 let mut daemons = IndexMap::new();
566 daemons.insert(id("postgres"), make_daemon(vec![]));
567 daemons.insert(id("api"), make_daemon(vec!["postgres"]));
568
569 let result = resolve_dependencies(&[id("api")], &daemons).unwrap();
570
571 assert_eq!(result.levels.len(), 2);
572 assert_eq!(result.levels[0], vec![id("postgres")]);
573 assert_eq!(result.levels[1], vec![id("api")]);
574 }
575
576 #[test]
577 fn test_multiple_dependencies() {
578 let mut daemons = IndexMap::new();
579 daemons.insert(id("postgres"), make_daemon(vec![]));
580 daemons.insert(id("redis"), make_daemon(vec![]));
581 daemons.insert(id("api"), make_daemon(vec!["postgres", "redis"]));
582
583 let result = resolve_dependencies(&[id("api")], &daemons).unwrap();
584
585 assert_eq!(result.levels.len(), 2);
586 assert!(result.levels[0].contains(&id("postgres")));
588 assert!(result.levels[0].contains(&id("redis")));
589 assert_eq!(result.levels[1], vec![id("api")]);
590 }
591
592 #[test]
593 fn test_transitive_dependencies() {
594 let mut daemons = IndexMap::new();
595 daemons.insert(id("database"), make_daemon(vec![]));
596 daemons.insert(id("backend"), make_daemon(vec!["database"]));
597 daemons.insert(id("api"), make_daemon(vec!["backend"]));
598
599 let result = resolve_dependencies(&[id("api")], &daemons).unwrap();
600
601 assert_eq!(result.levels.len(), 3);
602 assert_eq!(result.levels[0], vec![id("database")]);
603 assert_eq!(result.levels[1], vec![id("backend")]);
604 assert_eq!(result.levels[2], vec![id("api")]);
605 }
606
607 #[test]
608 fn test_diamond_dependency() {
609 let mut daemons = IndexMap::new();
610 daemons.insert(id("db"), make_daemon(vec![]));
611 daemons.insert(id("auth"), make_daemon(vec!["db"]));
612 daemons.insert(id("data"), make_daemon(vec!["db"]));
613 daemons.insert(id("api"), make_daemon(vec!["auth", "data"]));
614
615 let result = resolve_dependencies(&[id("api")], &daemons).unwrap();
616
617 assert_eq!(result.levels.len(), 3);
618 assert_eq!(result.levels[0], vec![id("db")]);
619 assert!(result.levels[1].contains(&id("auth")));
621 assert!(result.levels[1].contains(&id("data")));
622 assert_eq!(result.levels[2], vec![id("api")]);
623 }
624
625 #[test]
626 fn test_circular_dependency_detected() {
627 let mut daemons = IndexMap::new();
628 daemons.insert(id("a"), make_daemon(vec!["c"]));
629 daemons.insert(id("b"), make_daemon(vec!["a"]));
630 daemons.insert(id("c"), make_daemon(vec!["b"]));
631
632 let result = resolve_dependencies(&[id("a")], &daemons);
633
634 assert!(result.is_err());
635 let err = result.unwrap_err().to_string();
636 assert!(err.contains("circular dependency"));
637 }
638
639 #[test]
640 fn test_missing_dependency_error() {
641 let mut daemons = IndexMap::new();
642 let mut daemon = make_daemon(vec![]);
643 daemon.depends = vec![DaemonId::new("global", "nonexistent")];
644 daemons.insert(id("api"), daemon);
645
646 let result = resolve_dependencies(&[id("api")], &daemons);
647
648 assert!(result.is_err());
649 let err = result.unwrap_err().to_string();
650 assert!(err.contains("nonexistent"));
651 assert!(err.contains("not defined"));
652 }
653
654 #[test]
655 fn test_missing_requested_daemon_error() {
656 let daemons = IndexMap::new();
657
658 let result = resolve_dependencies(&[id("nonexistent")], &daemons);
659
660 assert!(result.is_err());
661 let err = result.unwrap_err().to_string();
662 assert!(err.contains("nonexistent"));
663 assert!(err.contains("not found"));
664 }
665
666 #[test]
667 fn test_multiple_requested_daemons() {
668 let mut daemons = IndexMap::new();
669 daemons.insert(id("db"), make_daemon(vec![]));
670 daemons.insert(id("api"), make_daemon(vec!["db"]));
671 daemons.insert(id("worker"), make_daemon(vec!["db"]));
672
673 let result = resolve_dependencies(&[id("api"), id("worker")], &daemons).unwrap();
674
675 assert_eq!(result.levels.len(), 2);
676 assert_eq!(result.levels[0], vec![id("db")]);
677 assert!(result.levels[1].contains(&id("api")));
679 assert!(result.levels[1].contains(&id("worker")));
680 }
681
682 #[test]
683 fn test_start_all_with_dependencies() {
684 let mut daemons = IndexMap::new();
685 daemons.insert(id("db"), make_daemon(vec![]));
686 daemons.insert(id("cache"), make_daemon(vec![]));
687 daemons.insert(id("api"), make_daemon(vec!["db", "cache"]));
688 daemons.insert(id("worker"), make_daemon(vec!["db"]));
689
690 let all_ids: Vec<DaemonId> = daemons.keys().cloned().collect();
691 let result = resolve_dependencies(&all_ids, &daemons).unwrap();
692
693 assert_eq!(result.levels.len(), 2);
694 assert!(result.levels[0].contains(&id("db")));
696 assert!(result.levels[0].contains(&id("cache")));
697 assert!(result.levels[1].contains(&id("api")));
699 assert!(result.levels[1].contains(&id("worker")));
700 }
701}