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