1use crate::{
2 AuditEntry, AuditEvent, Graph, GraphError, GraphResult, NodeChangeExplanation, NodeHandle,
3 NodeId, OutputFrameExplanation, OutputFrameKindTrace, OutputKey, ResourceCommandCause,
4 ResourceCommandExplanation, ResourceCommandKind, ResourceKey, ScopeId, ScopeResourceInventory,
5 TransactionResult,
6};
7
8impl<C, O> Graph<C, O> {
9 pub fn audit_log(&self) -> &[AuditEntry] {
11 &self.audit.log
12 }
13
14 pub fn why_changed<H: NodeHandle>(&self, node: H) -> Option<&NodeChangeExplanation> {
16 self.why_changed_by_id(node.id())
17 }
18
19 pub fn why_changed_by_id(&self, node: NodeId) -> Option<&NodeChangeExplanation> {
21 self.audit.node_changes.get(&node)
22 }
23
24 pub fn why_resource_command(&self, key: &ResourceKey) -> Option<&ResourceCommandExplanation> {
26 self.audit.resource_commands.get(key)
27 }
28
29 pub fn why_output_frame(&self, key: OutputKey) -> Option<&OutputFrameExplanation> {
31 self.audit.output_frames.get(&key)
32 }
33
34 pub fn dependency_path(&self, from: NodeId, to: NodeId) -> Option<Vec<NodeId>> {
36 if !self.nodes.contains_key(&from) || !self.nodes.contains_key(&to) {
37 return None;
38 }
39 let mut path = vec![from];
40 let mut visited = std::collections::BTreeSet::new();
41 self.dependency_path_inner(from, to, &mut visited, &mut path)
42 .then_some(path)
43 }
44
45 pub fn scope_resource_inventory(&self, scope: ScopeId) -> GraphResult<ScopeResourceInventory> {
47 self.scope_meta(scope)
48 .ok_or(GraphError::UnknownScope(scope))?;
49 let resources = self
50 .resource_owners
51 .iter()
52 .filter(|(_, owners)| owners.contains(&scope))
53 .map(|(key, _)| key.clone())
54 .collect();
55 Ok(ScopeResourceInventory { scope, resources })
56 }
57
58 pub(crate) fn record_transaction_audit(&mut self, result: &TransactionResult<C, O>) {
59 self.audit.log.extend(result.audit_log.iter().cloned());
60 let changed_inputs = result.changed_inputs.clone();
61 let changed_nodes = changed_nodes(result);
62 for entry in &result.audit_log {
63 if let Some(node) = event_node(entry.event) {
64 let dependency_paths = self.paths_from_inputs_to_targets(&changed_inputs, &[node]);
65 let input_causes = input_causes_from_paths(&dependency_paths);
66 self.audit.node_changes.insert(
67 node,
68 NodeChangeExplanation {
69 node,
70 transaction_id: entry.transaction_id,
71 revision: entry.revision,
72 event: entry.event,
73 input_causes,
74 dependency_paths,
75 },
76 );
77 }
78 }
79
80 let causes = std::mem::take(&mut self.audit.pending_resource_causes);
81 debug_assert_eq!(causes.len(), result.resource_plan.commands().len());
82 for (index, command) in result.resource_plan.commands().iter().enumerate() {
83 let cause = causes
84 .get(index)
85 .copied()
86 .expect("resource command cause recorded during reconciliation");
87 let collection_diffs = cause.collection().into_iter().collect::<Vec<_>>();
88 let dependency_paths =
89 self.paths_from_inputs_to_targets(&changed_inputs, &collection_diffs);
90 let input_causes = input_causes_from_paths(&dependency_paths);
91 self.audit.resource_commands.insert(
92 command.key().clone(),
93 ResourceCommandExplanation {
94 key: command.key().clone(),
95 scope: command.scope(),
96 transaction_id: result.transaction_id,
97 revision: result.revision,
98 kind: ResourceCommandKind::from_command(command),
99 cause,
100 collection_diffs,
101 changed_nodes: changed_nodes.clone(),
102 input_causes,
103 dependency_paths,
104 },
105 );
106 }
107
108 for frame in &result.output_frames {
109 let dependencies = self
110 .output_meta(frame.output_key)
111 .map(|meta| meta.dependencies().as_slice().to_vec())
112 .unwrap_or_default();
113 let changed_dependencies = dependencies
114 .iter()
115 .copied()
116 .filter(|node| changed_nodes.contains(node))
117 .collect::<Vec<_>>();
118 let dependency_paths =
119 self.paths_from_inputs_to_targets(&changed_inputs, &changed_dependencies);
120 let input_causes = input_causes_from_paths(&dependency_paths);
121 self.audit.output_frames.insert(
122 frame.output_key,
123 OutputFrameExplanation {
124 output_key: frame.output_key,
125 scope: frame.scope,
126 transaction_id: frame.transaction_id,
127 revision: frame.revision,
128 kind: OutputFrameKindTrace::from_kind(&frame.kind),
129 dependencies,
130 changed_dependencies,
131 input_causes,
132 dependency_paths,
133 },
134 );
135 }
136 }
137
138 fn paths_from_inputs_to_targets(
139 &self,
140 inputs: &[NodeId],
141 targets: &[NodeId],
142 ) -> Vec<Vec<NodeId>> {
143 inputs
144 .iter()
145 .flat_map(|input| {
146 targets
147 .iter()
148 .filter_map(|target| self.dependency_path(*input, *target))
149 })
150 .collect()
151 }
152
153 fn dependency_path_inner(
154 &self,
155 current: NodeId,
156 target: NodeId,
157 visited: &mut std::collections::BTreeSet<NodeId>,
158 path: &mut Vec<NodeId>,
159 ) -> bool {
160 if current == target {
161 return true;
162 }
163 if !visited.insert(current) {
164 return false;
165 }
166 for next in self.downstream_nodes(current) {
167 path.push(next);
168 if self.dependency_path_inner(next, target, visited, path) {
169 return true;
170 }
171 path.pop();
172 }
173 false
174 }
175
176 fn downstream_nodes(&self, node: NodeId) -> Vec<NodeId> {
177 self.nodes
178 .values()
179 .filter_map(|meta| {
180 meta.dependencies()
181 .as_slice()
182 .contains(&node)
183 .then_some(meta.id())
184 })
185 .collect()
186 }
187}
188
189impl ResourceCommandCause {
190 fn collection(self) -> Option<NodeId> {
191 match self {
192 Self::Planner { collection } => Some(collection),
193 Self::ScopeClosed { .. } => None,
194 }
195 }
196}
197
198fn input_causes_from_paths(paths: &[Vec<NodeId>]) -> Vec<NodeId> {
199 let mut causes = Vec::new();
200 for path in paths {
201 if let Some(input) = path.first()
202 && !causes.contains(input)
203 {
204 causes.push(*input);
205 }
206 }
207 causes
208}
209
210fn changed_nodes<C, O>(result: &TransactionResult<C, O>) -> Vec<NodeId> {
211 let mut nodes = result.changed_inputs.clone();
212 nodes.extend(result.changed_derived_nodes.iter().copied());
213 nodes.extend(result.changed_collection_nodes.iter().copied());
214 nodes
215}
216
217fn event_node(event: AuditEvent) -> Option<NodeId> {
218 match event {
219 AuditEvent::InputChanged(node)
220 | AuditEvent::DerivedChanged(node)
221 | AuditEvent::CollectionChanged(node)
222 | AuditEvent::NodeCreated(node) => Some(node),
223 AuditEvent::NodeAttached { node, .. } => Some(node),
224 AuditEvent::InputUnchanged(_)
225 | AuditEvent::ScopeCreated(_)
226 | AuditEvent::ScopeClosed(_) => None,
227 }
228}