Skip to main content

lightshuttle_runtime/lifecycle/
plan.rs

1//! Resolved execution plan: topologically sorted nodes plus the
2//! dependency graph.
3
4use std::collections::{HashMap, HashSet};
5
6use lightshuttle_manifest::Manifest;
7
8use crate::lifecycle::error::LifecycleError;
9use crate::spec::{ContainerSpec, ResolvedResource, ResourceOutputs, from_resource};
10
11/// A single resource to manage, with its resolved [`ContainerSpec`],
12/// its exposed outputs and its explicit dependencies.
13#[derive(Debug, Clone)]
14pub struct PlanNode {
15    /// Resource name as declared in the manifest.
16    pub name: String,
17    /// Container specification derived from the manifest.
18    pub spec: ContainerSpec,
19    /// Outputs the resource exposes to its dependents (host, port,
20    /// password, url, ...).
21    pub outputs: ResourceOutputs,
22    /// Names of resources this one depends on.
23    pub depends_on: Vec<String>,
24}
25
26/// Topologically sorted execution plan.
27#[derive(Debug, Clone)]
28pub struct LifecyclePlan {
29    nodes: Vec<PlanNode>,
30    edges: HashMap<String, Vec<String>>,
31}
32
33impl LifecyclePlan {
34    /// Build a plan from a parsed manifest.
35    ///
36    /// Resolves the dependency graph, performs a topological sort and
37    /// converts every resource to a [`ContainerSpec`].
38    pub fn from_manifest(manifest: &Manifest) -> Result<Self, LifecycleError> {
39        let project = manifest.project.name.as_str();
40
41        // Build edges and collect spec + outputs for every resource.
42        let mut resolved: HashMap<String, ResolvedResource> = HashMap::new();
43        let mut deps: HashMap<String, Vec<String>> = HashMap::new();
44        for (name, kind) in &manifest.resources {
45            let r =
46                from_resource(project, name, kind).map_err(|source| LifecycleError::SpecBuild {
47                    resource: name.clone(),
48                    source,
49                })?;
50            resolved.insert(name.clone(), r);
51            deps.insert(name.clone(), kind.depends_on().to_vec());
52        }
53
54        // Verify every dependency points to an existing resource.
55        for (name, dependencies) in &deps {
56            for dependency in dependencies {
57                if !resolved.contains_key(dependency) {
58                    return Err(LifecycleError::UnknownResource(format!(
59                        "`{dependency}` (depended on by `{name}`)"
60                    )));
61                }
62            }
63        }
64
65        // Kahn's algorithm for topological sort.
66        let mut in_degree: HashMap<String, usize> = resolved
67            .keys()
68            .map(|name| (name.clone(), 0_usize))
69            .collect();
70        for dependencies in deps.values() {
71            for dependency in dependencies {
72                *in_degree.entry(dependency.clone()).or_insert(0) += 1;
73            }
74        }
75
76        // Build reverse adjacency: dependency → resources that depend on it.
77        let mut reverse: HashMap<String, Vec<String>> = HashMap::new();
78        for (name, dependencies) in &deps {
79            for dependency in dependencies {
80                reverse
81                    .entry(dependency.clone())
82                    .or_default()
83                    .push(name.clone());
84            }
85        }
86
87        // Start with nodes that no one depends on (in_degree == 0 in the
88        // reverse graph means "no dependent waits on this one"). For a
89        // dependency edge dep → res, res is added to nodes that depend
90        // on dep; topo order should yield deps first.
91        //
92        // We invert: deps come before their dependents.
93        // in_degree counts how many incoming dependency edges (= how
94        // many of my own dependencies) each node has.
95        let mut in_count: HashMap<String, usize> = resolved
96            .keys()
97            .map(|name| (name.clone(), deps.get(name).map_or(0, Vec::len)))
98            .collect();
99
100        let mut ready: Vec<String> = in_count
101            .iter()
102            .filter(|(_, count)| **count == 0)
103            .map(|(name, _)| name.clone())
104            .collect();
105        // Deterministic order: sort by name.
106        ready.sort();
107
108        let mut sorted: Vec<String> = Vec::with_capacity(resolved.len());
109        while let Some(node) = ready.pop() {
110            sorted.push(node.clone());
111            if let Some(dependents) = reverse.get(&node) {
112                let mut newly_ready: Vec<String> = Vec::new();
113                for dependent in dependents {
114                    let count = in_count.get_mut(dependent).expect("dependent indexed");
115                    *count -= 1;
116                    if *count == 0 {
117                        newly_ready.push(dependent.clone());
118                    }
119                }
120                newly_ready.sort();
121                ready.extend(newly_ready);
122            }
123        }
124
125        if sorted.len() != resolved.len() {
126            let unresolved: Vec<&String> = resolved
127                .keys()
128                .filter(|name| !sorted.contains(name))
129                .collect();
130            return Err(LifecycleError::Cycle(format!(
131                "{unresolved:?} involved in a cycle"
132            )));
133        }
134
135        let _ = in_degree; // silence unused warning for the alternate counter
136        let _ = HashSet::<&str>::new();
137
138        // Snapshot the edges before draining deps into the nodes.
139        let edges = deps.clone();
140
141        let nodes: Vec<PlanNode> = sorted
142            .into_iter()
143            .map(|name| {
144                let ResolvedResource { spec, outputs } =
145                    resolved.remove(&name).expect("spec indexed by name");
146                let dependencies = deps.remove(&name).unwrap_or_default();
147                PlanNode {
148                    name,
149                    spec,
150                    outputs,
151                    depends_on: dependencies,
152                }
153            })
154            .collect();
155
156        Ok(Self { nodes, edges })
157    }
158
159    /// The list of nodes in topological order.
160    #[must_use]
161    pub fn nodes(&self) -> &[PlanNode] {
162        &self.nodes
163    }
164
165    /// Names of resources that depend on `name`.
166    #[must_use]
167    pub fn dependents_of(&self, name: &str) -> Vec<&str> {
168        let mut out: Vec<&str> = Vec::new();
169        for (resource, deps) in &self.edges {
170            if deps.iter().any(|d| d == name) {
171                out.push(resource.as_str());
172            }
173        }
174        out.sort_unstable();
175        out
176    }
177}