Skip to main content

trellis_core/
resource_reconcile.rs

1use crate::{
2    Graph, GraphError, GraphResult, ResourceCommand, ResourceCommandCause, ResourceCommandKind,
3    ResourceKey, ResourcePlan, ScopeId,
4};
5use std::collections::BTreeSet;
6
7impl<C> Graph<C> {
8    pub(crate) fn produce_resource_plan(
9        &mut self,
10        closed_scopes: &[ScopeId],
11    ) -> GraphResult<ResourcePlan<C>> {
12        self.audit.pending_resource_causes.clear();
13        let planners = self.resource_planners.clone();
14        let mut plan = ResourcePlan::new();
15        for planner in planners {
16            if self.collection_diffs.contains_key(&planner.collection) {
17                let planned = planner.run(self)?;
18                let cause = ResourceCommandCause::Planner {
19                    collection: planner.collection,
20                };
21                let reconciled = self.reconcile_resource_plan(planner.scope, planned, cause)?;
22                plan.append(reconciled);
23            }
24        }
25
26        for scope in closed_scopes {
27            let close_plan = self.close_scope_resources(*scope);
28            plan.append(close_plan);
29        }
30
31        Ok(plan)
32    }
33
34    fn reconcile_resource_plan(
35        &mut self,
36        planner_scope: ScopeId,
37        plan: ResourcePlan<C>,
38        cause: ResourceCommandCause,
39    ) -> GraphResult<ResourcePlan<C>> {
40        let mut reconciled = ResourcePlan::new();
41        for command in plan.into_commands() {
42            if command.scope() != planner_scope {
43                return Err(GraphError::ResourceScopeMismatch(command.scope()));
44            }
45            self.reconcile_resource_command(command, &mut reconciled, cause)?;
46        }
47        Ok(reconciled)
48    }
49
50    fn reconcile_resource_command(
51        &mut self,
52        command: ResourceCommand<C>,
53        plan: &mut ResourcePlan<C>,
54        cause: ResourceCommandCause,
55    ) -> GraphResult<()> {
56        match command {
57            ResourceCommand::Open {
58                key,
59                scope,
60                command,
61            } => self.reconcile_open(key, scope, command, plan, cause),
62            ResourceCommand::Close { key, scope } => {
63                self.remove_resource_owner(&key, scope, plan, cause);
64                Ok(())
65            }
66            ResourceCommand::Replace {
67                key,
68                scope,
69                command,
70            } => {
71                self.require_scope_open(scope)?;
72                self.require_resource_owner(&key, scope, ResourceCommandKind::Replace)?;
73                self.resource_owners
74                    .entry(key.clone())
75                    .or_default()
76                    .insert(scope);
77                plan.replace(key, scope, command);
78                self.audit.pending_resource_causes.push(cause);
79                Ok(())
80            }
81            ResourceCommand::Refresh {
82                key,
83                scope,
84                command,
85            } => {
86                self.require_scope_open(scope)?;
87                self.require_resource_owner(&key, scope, ResourceCommandKind::Refresh)?;
88                self.resource_owners
89                    .entry(key.clone())
90                    .or_default()
91                    .insert(scope);
92                plan.refresh(key, scope, command);
93                self.audit.pending_resource_causes.push(cause);
94                Ok(())
95            }
96        }
97    }
98
99    fn reconcile_open(
100        &mut self,
101        key: ResourceKey,
102        scope: ScopeId,
103        command: C,
104        plan: &mut ResourcePlan<C>,
105        cause: ResourceCommandCause,
106    ) -> GraphResult<()> {
107        self.require_scope_open(scope)?;
108        let owners = self.resource_owners.entry(key.clone()).or_default();
109        let was_empty = owners.is_empty();
110        owners.insert(scope);
111        if was_empty {
112            plan.open(key, scope, command);
113            self.audit.pending_resource_causes.push(cause);
114        }
115        Ok(())
116    }
117
118    fn close_scope_resources(&mut self, scope: ScopeId) -> ResourcePlan<C> {
119        let keys: Vec<ResourceKey> = self.resource_owners.keys().cloned().collect();
120        let mut plan = ResourcePlan::new();
121        let cause = ResourceCommandCause::ScopeClosed { scope };
122        for key in keys {
123            self.remove_resource_owner(&key, scope, &mut plan, cause);
124        }
125        plan
126    }
127
128    fn remove_resource_owner(
129        &mut self,
130        key: &ResourceKey,
131        scope: ScopeId,
132        plan: &mut ResourcePlan<C>,
133        cause: ResourceCommandCause,
134    ) {
135        let Some(owners) = self.resource_owners.get_mut(key) else {
136            return;
137        };
138        owners.remove(&scope);
139        if owners.is_empty() {
140            self.resource_owners.remove(key);
141            plan.close(key.clone(), scope);
142            self.audit.pending_resource_causes.push(cause);
143        }
144    }
145
146    fn require_resource_owner(
147        &self,
148        key: &ResourceKey,
149        scope: ScopeId,
150        command_kind: ResourceCommandKind,
151    ) -> GraphResult<()> {
152        let Some(owners) = self.resource_owners.get(key) else {
153            return Err(GraphError::ResourceNotOwned {
154                key: key.clone(),
155                scope,
156                command_kind,
157            });
158        };
159        if !owners.contains(&scope) {
160            return Err(GraphError::ResourceNotOwned {
161                key: key.clone(),
162                scope,
163                command_kind,
164            });
165        }
166        Ok(())
167    }
168
169    pub(crate) fn require_scope_open(&self, scope: ScopeId) -> GraphResult<()> {
170        let scope_meta = self
171            .scope_meta(scope)
172            .ok_or(GraphError::UnknownScope(scope))?;
173        if scope_meta.is_closed() {
174            return Err(GraphError::ScopeAlreadyClosed(scope));
175        }
176        Ok(())
177    }
178
179    /// Returns resource owners in deterministic resource-key order.
180    pub fn resource_owners(&self, key: &ResourceKey) -> Option<&BTreeSet<ScopeId>> {
181        self.resource_owners.get(key)
182    }
183}