Skip to main content

trellis_core/
resource_reconcile.rs

1use crate::{
2    Graph, GraphError, GraphResult, ResourceCommand, ResourceCommandCause, ResourceKey,
3    ResourcePlan, ScopeId,
4};
5use std::collections::BTreeSet;
6
7impl<C, O> Graph<C, O> {
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)?;
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)?;
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(&self, key: &ResourceKey, scope: ScopeId) -> GraphResult<()> {
147        let Some(owners) = self.resource_owners.get(key) else {
148            return Err(GraphError::ResourceNotOwned);
149        };
150        if !owners.contains(&scope) {
151            return Err(GraphError::ResourceNotOwned);
152        }
153        Ok(())
154    }
155
156    pub(crate) fn require_scope_open(&self, scope: ScopeId) -> GraphResult<()> {
157        let scope_meta = self
158            .scope_meta(scope)
159            .ok_or(GraphError::UnknownScope(scope))?;
160        if scope_meta.is_closed() {
161            return Err(GraphError::ScopeAlreadyClosed(scope));
162        }
163        Ok(())
164    }
165
166    /// Returns resource owners in deterministic resource-key order.
167    pub fn resource_owners(&self, key: &ResourceKey) -> Option<&BTreeSet<ScopeId>> {
168        self.resource_owners.get(key)
169    }
170}