1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6 BudgetKind, FingerprintValue, IncrementalEngine, IncrementalError, Observation,
7 ObservationKind, QueryResult, Revision, SnapshotBudgets, SnapshotError, ValueFingerprint,
8 state::Node,
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct GraphSnapshot<K, V> {
14 pub nodes: Vec<SnapshotNode<K, V>>,
16}
17
18impl<K, V> GraphSnapshot<K, V> {
19 #[must_use]
21 pub fn new(nodes: Vec<SnapshotNode<K, V>>) -> Self {
22 Self { nodes }
23 }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct SnapshotNode<K, V> {
29 pub key: K,
31 pub revision: Revision,
33 pub dirty: bool,
35 pub value: Option<V>,
37 pub fingerprint: Option<ValueFingerprint>,
39 pub dependencies: Vec<Observation<K>>,
41}
42
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
45pub struct RestoreReport {
46 pub nodes: usize,
48 pub recovered_dirty: usize,
51}
52
53impl<K, V> IncrementalEngine<K, V>
54where
55 K: Ord + Clone,
56 V: Clone + FingerprintValue,
57{
58 pub fn snapshot<I>(
60 &mut self,
61 roots: I,
62 budgets: SnapshotBudgets,
63 ) -> QueryResult<K, GraphSnapshot<K, V>>
64 where
65 I: IntoIterator<Item = K>,
66 {
67 let mut pending = roots.into_iter().collect::<BTreeSet<_>>();
68 let Some(root) = pending.iter().next().cloned() else {
69 return Ok(GraphSnapshot::new(Vec::new()));
70 };
71 let mut seen = BTreeSet::new();
72 let mut nodes = Vec::new();
73 let mut edges = 0_usize;
74 while let Some(key) = pending.iter().next().cloned() {
75 pending.remove(&key);
76 if !seen.insert(key.clone()) {
77 continue;
78 }
79 if nodes.len().saturating_add(1) > budgets.max_nodes {
80 let token = self.alloc_continuation(root);
81 return Err(IncrementalError::BudgetExceeded {
82 kind: BudgetKind::Output,
83 limit: budgets.max_nodes,
84 consumed: nodes.len().saturating_add(1),
85 continuation: Some(token),
86 });
87 }
88 let node = self
89 .nodes
90 .get(&key)
91 .ok_or_else(|| IncrementalError::UnknownQuery { key: key.clone() })?;
92 edges = edges.saturating_add(node.dependencies.len());
93 if edges > budgets.max_edges {
94 let token = self.alloc_continuation(root);
95 return Err(IncrementalError::BudgetExceeded {
96 kind: BudgetKind::Output,
97 limit: budgets.max_edges,
98 consumed: edges,
99 continuation: Some(token),
100 });
101 }
102 for observation in &node.dependencies {
103 if matches!(observation.kind(), ObservationKind::Read) {
104 pending.insert(observation.key().clone());
105 }
106 }
107 nodes.push(SnapshotNode {
108 key,
109 revision: node.revision,
110 dirty: node.dirty,
111 value: node.value.clone(),
112 fingerprint: node.fingerprint,
113 dependencies: node.dependencies.clone(),
114 });
115 }
116 Ok(GraphSnapshot::new(nodes))
117 }
118
119 pub fn restore_snapshot(
121 &mut self,
122 snapshot: GraphSnapshot<K, V>,
123 ) -> Result<RestoreReport, SnapshotError<K>> {
124 let mut keys = BTreeSet::new();
125 for node in &snapshot.nodes {
126 if !keys.insert(node.key.clone()) {
127 return Err(SnapshotError::DuplicateNode {
128 key: node.key.clone(),
129 });
130 }
131 }
132
133 self.nodes.clear();
134 self.reverse.clear();
135 let mut recovered_dirty = 0_usize;
136 let mut max_revision = self.next_revision.saturating_sub(1);
137 for snapshot_node in snapshot.nodes {
138 max_revision = max_revision.max(snapshot_node.revision.get());
139 let mut dirty = snapshot_node.dirty;
140 let mut recovered = false;
141 if snapshot_node.value.is_none() {
142 recovered = !dirty;
143 dirty = true;
144 }
145 let mut fingerprint = snapshot_node.fingerprint;
146 if let Some(value) = &snapshot_node.value {
147 let computed = value.incremental_fingerprint();
148 if fingerprint != Some(computed) {
149 fingerprint = Some(computed);
150 recovered = recovered || !dirty;
151 dirty = true;
152 }
153 }
154 if has_missing_read_deps(&snapshot_node, &keys) {
155 recovered = recovered || !dirty;
156 dirty = true;
157 }
158 if recovered {
159 recovered_dirty += 1;
160 }
161 restore_external_revisions(&mut self.source_revisions, &snapshot_node);
162 self.nodes.insert(
163 snapshot_node.key,
164 Node {
165 revision: snapshot_node.revision,
166 dirty,
167 value: snapshot_node.value,
168 fingerprint,
169 dependencies: snapshot_node.dependencies,
170 },
171 );
172 }
173 self.next_revision = self.next_revision.max(max_revision.saturating_add(1));
174 self.rebuild_reverse();
175 Ok(RestoreReport {
176 nodes: self.nodes.len(),
177 recovered_dirty,
178 })
179 }
180
181 fn rebuild_reverse(&mut self) {
182 self.reverse.clear();
183 for (key, node) in &self.nodes {
184 for observation in &node.dependencies {
185 self.reverse
186 .entry(observation.key().clone())
187 .or_default()
188 .insert(key.clone());
189 }
190 }
191 }
192}
193
194fn has_missing_read_deps<K, V>(snapshot_node: &SnapshotNode<K, V>, keys: &BTreeSet<K>) -> bool
195where
196 K: Ord,
197{
198 snapshot_node.dependencies.iter().any(|observation| {
199 matches!(observation.kind(), ObservationKind::Read) && !keys.contains(observation.key())
200 })
201}
202
203fn restore_external_revisions<K, V>(
204 source_revisions: &mut BTreeMap<K, Revision>,
205 snapshot_node: &SnapshotNode<K, V>,
206) where
207 K: Ord + Clone,
208{
209 for observation in &snapshot_node.dependencies {
210 if matches!(observation.kind(), ObservationKind::Read) {
211 continue;
212 }
213 source_revisions
214 .entry(observation.key().clone())
215 .and_modify(|revision| {
216 if observation.revision() > *revision {
217 *revision = observation.revision();
218 }
219 })
220 .or_insert(observation.revision());
221 }
222}