snix_castore/directoryservice/
directory_graph.rs1#![deny(missing_docs)]
2use futures::{StreamExt, TryStreamExt};
13use petgraph::{
14 graph::{DiGraph, NodeIndex},
15 visit::{Bfs, DfsPostOrder, Walker},
16};
17use std::collections::HashMap;
18use tracing::{instrument, warn};
19
20use crate::directoryservice::{
21 DirectoryService,
22 order_validator::{self, LeavesToRoot, OrderValidator, RootToLeaves},
23};
24use crate::{B3Digest, Directory, Node};
25
26#[derive(Default)]
33pub struct DirectoryGraph {
34 graph: DiGraph<Directory, ()>,
37
38 root_idx: NodeIndex,
40}
41
42#[derive(PartialEq, Eq, Debug)]
43enum DrainOrder {
44 RootToLeaves,
48 LeavesToRoot,
50}
51
52impl DirectoryGraph {
53 fn drain(self, order: DrainOrder) -> impl Iterator<Item = Directory> {
55 let order = match order {
56 DrainOrder::RootToLeaves => {
57 Bfs::new(&self.graph, self.root_idx)
59 .iter(&self.graph)
60 .collect::<Vec<_>>()
61 }
62 DrainOrder::LeavesToRoot => {
63 DfsPostOrder::new(&self.graph, self.root_idx)
65 .iter(&self.graph)
66 .collect::<Vec<_>>()
67 }
68 };
69
70 let (mut nodes, _edges) = self.graph.into_nodes_edges();
71 order
72 .into_iter()
73 .map(move |i| std::mem::take(&mut nodes[i.index()].weight))
74 }
75
76 #[instrument(level = "trace", skip_all)]
78 pub fn drain_leaves_to_root(self) -> impl Iterator<Item = Directory> {
79 self.drain(DrainOrder::LeavesToRoot)
80 }
81
82 #[instrument(level = "trace", skip_all)]
84 pub fn drain_root_to_leaves(self) -> impl Iterator<Item = Directory> {
85 self.drain(DrainOrder::RootToLeaves)
86 }
87
88 pub fn root(&self) -> &Directory {
90 self.graph
91 .node_weight(self.root_idx)
92 .expect("Snix bug: root not found")
93 }
94}
95
96pub struct DirectoryGraphBuilder<OV> {
106 order_validator: OV,
108
109 graph: DiGraph<Option<Directory>, ()>,
114
115 digest_to_node_idx: HashMap<B3Digest, NodeIndex>,
118}
119
120impl DirectoryGraphBuilder<LeavesToRoot> {
121 pub fn new() -> Self {
124 Self::default()
125 }
126}
127
128impl Default for DirectoryGraphBuilder<LeavesToRoot> {
129 fn default() -> Self {
130 Self {
131 order_validator: Default::default(),
132 graph: Default::default(),
133 digest_to_node_idx: Default::default(),
134 }
135 }
136}
137
138impl DirectoryGraphBuilder<RootToLeaves> {
139 pub fn new(root_digest: B3Digest) -> Self {
145 Self {
146 order_validator: RootToLeaves::new_with_root_digest(root_digest),
147 graph: Default::default(),
148 digest_to_node_idx: Default::default(),
149 }
150 }
151}
152
153impl<OV> DirectoryGraphBuilder<OV>
154where
155 OV: OrderValidator,
156{
157 #[instrument(level = "trace", skip_all, fields(directory.digest = %directory.digest(), directory.size = directory.size()), err)]
159 pub fn try_insert(
160 &mut self,
161 directory: Directory,
162 ) -> Result<(), order_validator::OrderingError> {
163 self.order_validator.try_accept(&directory)?;
165
166 let self_ix = *self
168 .digest_to_node_idx
169 .entry(directory.digest())
170 .or_insert_with(|| self.graph.add_node(None));
171
172 if self.graph[self_ix].is_some() {
176 return Ok(());
177 }
178
179 for (_, node) in directory.nodes() {
183 let Node::Directory {
184 digest: refereced_digest,
185 ..
186 } = node
187 else {
188 continue;
189 };
190
191 let referenced_ix = *self
192 .digest_to_node_idx
193 .entry(*refereced_digest)
194 .or_insert_with(|| {
195 self.graph.add_node(None)
198 });
199
200 self.graph.add_edge(self_ix, referenced_ix, ());
201 }
202
203 self.graph[self_ix] = Some(directory);
205
206 Ok(())
207 }
208
209 pub fn build(self) -> Result<DirectoryGraph, order_validator::OrderingError> {
211 self.order_validator.finalize()?;
212
213 let graph: DiGraph<Directory, ()> = self.graph.map_owned(
215 |_ix, mut n| n.take().expect("Snix bug: no pending directories"),
216 |_ix, e| e,
217 );
218
219 let mut incomings = graph.externals(petgraph::Incoming);
221 let root_idx = incomings
222 .next()
223 .expect("Snix bug: There must be 1 incoming external");
224 debug_assert!(
225 incomings.next().is_none(),
226 "Snix bug: There must be 1 incoming external"
227 );
228 Ok(DirectoryGraph { graph, root_idx })
229 }
230}
231
232#[cfg(feature = "compat-accept-bigger-sizes")]
233impl DirectoryGraph {
234 pub fn with_recalculated_sizes(self) -> Self {
239 fn fix_sizes_recursive(
244 directory: &Directory,
245 source: &HashMap<B3Digest, crate::Directory>,
246 new_closure: &mut DirectoryGraphBuilder<LeavesToRoot>,
247 ) -> crate::Node {
248 let new_dir = Directory::try_from_iter(directory.nodes().map(|(path, node)| {
249 (
250 path.to_owned(),
251 if let Node::Directory {
252 digest,
253 size: _size,
254 } = node
255 {
256 fix_sizes_recursive(
257 source
258 .get(digest)
259 .expect("Snix bug: digest not found in source"),
260 source,
261 new_closure,
262 )
263 } else {
264 node.to_owned()
265 },
266 )
267 }))
268 .expect("Snix bug: constructed invalid directory");
269
270 let new_digest = new_dir.digest();
271 let new_size = new_dir.size();
272
273 new_closure
274 .try_insert(new_dir)
275 .expect("Snix bug: rewriting produced invalid closure");
276
277 crate::Node::Directory {
278 digest: new_digest,
279 size: new_size,
280 }
281 }
282
283 let root_digest = self.root().digest();
284
285 let directories = HashMap::from_iter(
286 self.drain_leaves_to_root()
287 .map(|directory| (directory.digest(), directory)),
288 );
289
290 let mut new_graph = DirectoryGraphBuilder::<LeavesToRoot>::new();
291
292 fix_sizes_recursive(
293 directories
294 .get(&root_digest)
295 .expect("Snix bug: root digest not found"),
296 &directories,
297 &mut new_graph,
298 );
299
300 new_graph
301 .build()
302 .expect("Snix bug: rewriting produced invalid closure")
303 }
304}
305
306#[tonic::async_trait]
308pub trait DirectoryServiceGraphExt {
309 async fn get_directory_graph(
311 &self,
312 digest: &B3Digest,
313 ) -> Result<Option<DirectoryGraph>, super::Error>;
314
315 async fn put_directory_graph(
317 &self,
318 directory_graph: DirectoryGraph,
319 ) -> Result<B3Digest, super::Error>;
320
321 }
323
324#[tonic::async_trait]
325impl<T> DirectoryServiceGraphExt for T
326where
327 T: DirectoryService,
328{
329 async fn get_directory_graph(
331 &self,
332 digest: &B3Digest,
333 ) -> Result<Option<DirectoryGraph>, super::Error> {
334 let mut builder = DirectoryGraphBuilder::<RootToLeaves>::new(digest.to_owned());
335 let mut directories = std::pin::pin!(self.get_recursive(digest).peekable());
336
337 if directories.as_mut().peek().await.is_none() {
338 return Ok(None);
339 }
340
341 while let Some(directory) = directories.try_next().await? {
342 builder.try_insert(directory)?;
343 }
344
345 Ok(Some(builder.build()?))
346 }
347
348 async fn put_directory_graph(
350 &self,
351 directory_graph: DirectoryGraph,
352 ) -> Result<B3Digest, super::Error> {
353 let mut putter = self.put_multiple_start();
354
355 for directory in directory_graph.drain_leaves_to_root() {
356 putter.put(directory).await?;
357 }
358
359 Ok(putter.close().await?)
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use crate::directoryservice::directory_graph::DirectoryGraphBuilder;
366 use crate::directoryservice::order_validator::{LeavesToRoot, OrderValidator, RootToLeaves};
367 use crate::fixtures::{DIRECTORY_A, DIRECTORY_B, DIRECTORY_C};
368 use crate::{Directory, Node};
369 use rstest::rstest;
370 use std::sync::LazyLock;
371
372 pub static BROKEN_PARENT_DIRECTORY: LazyLock<Directory> = LazyLock::new(|| {
373 Directory::try_from_iter([(
374 "foo".try_into().unwrap(),
375 Node::Directory {
376 digest: DIRECTORY_A.digest(),
377 size: DIRECTORY_A.size() + 42, },
379 )])
380 .unwrap()
381 });
382
383 #[rstest]
384 #[case::ltr_empty_graph(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[], false, None)]
386 #[case::ltr_empty_directory(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_A], false, Some(vec![&*DIRECTORY_A]))]
388 #[case::ltr_simple_closure(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_A, &*DIRECTORY_B], false, Some(vec![&*DIRECTORY_A, &*DIRECTORY_B]))]
390 #[case::ltr_same_child(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_A, &*DIRECTORY_A, &*DIRECTORY_C], false, Some(vec![&*DIRECTORY_A, &*DIRECTORY_C]))]
393 #[case::ltr_same_child_dedup(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_A, &*DIRECTORY_C], false, Some(vec![&*DIRECTORY_A, &*DIRECTORY_C]))]
395 #[case::ltr_unconnected_node(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_A, &*DIRECTORY_C, &*DIRECTORY_B], false, None)]
398 #[case::ltr_dangling_pointer(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_B], true, None)]
400 #[case::ltr_wrong_size_in_parent(DirectoryGraphBuilder::<LeavesToRoot>::new(), &[&*DIRECTORY_A, &*BROKEN_PARENT_DIRECTORY], true, None)]
402
403 #[case::rtl_empty_directory(DirectoryGraphBuilder::<RootToLeaves>::new(DIRECTORY_A.digest()), &[&*DIRECTORY_A], false, Some(vec![&*DIRECTORY_A]))]
405 #[case::rtl_simple_closure(DirectoryGraphBuilder::<RootToLeaves>::new(DIRECTORY_B.digest()), &[&*DIRECTORY_B, &*DIRECTORY_A], false, Some(vec![&*DIRECTORY_A, &*DIRECTORY_B]))]
407 #[case::rtl_same_child_dedup(DirectoryGraphBuilder::<RootToLeaves>::new(DIRECTORY_C.digest()), &[&*DIRECTORY_C, &*DIRECTORY_A], false, Some(vec![&*DIRECTORY_A, &*DIRECTORY_C]))]
409 #[case::rtl_unconnected_node(DirectoryGraphBuilder::<RootToLeaves>::new(DIRECTORY_C.digest()), &[&*DIRECTORY_C, &*DIRECTORY_B], true, None)]
411 #[case::rtl_wrong_size_in_parent(DirectoryGraphBuilder::<RootToLeaves>::new(BROKEN_PARENT_DIRECTORY.digest()), &[&*BROKEN_PARENT_DIRECTORY, &*DIRECTORY_A], true, None)]
413 fn directory_graph(
414 #[case] mut builder: DirectoryGraphBuilder<impl OrderValidator>,
415 #[case] directories_to_upload: &[&Directory],
416 #[case] exp_fail_upload_last: bool,
417 #[case] exp_build: Option<Vec<&Directory>>, ) {
419 let mut it = directories_to_upload.iter().peekable();
420 while let Some(d) = it.next() {
421 if it.peek().is_none() && exp_fail_upload_last {
422 builder
423 .try_insert((*d).to_owned())
424 .expect_err("last insert to fail");
425 } else {
426 builder
427 .try_insert((*d).to_owned())
428 .expect("insert to succeed");
429 }
430 }
431
432 if exp_fail_upload_last {
433 return;
434 }
435
436 if let Some(exp_drain_ltr) = exp_build {
437 let directory_graph = builder.build().expect("build to succeed");
438
439 let drained_ltr = directory_graph.drain_leaves_to_root().collect::<Vec<_>>();
441
442 assert_eq!(
443 exp_drain_ltr
444 .iter()
445 .map(|d| (*d).to_owned())
446 .collect::<Vec<_>>(),
447 drained_ltr
448 );
449 } else {
450 assert!(builder.build().is_err(), "expected build to fail");
451 }
452 }
453
454 #[test]
455 fn rtl_wrong_digest() {
458 let mut builder = DirectoryGraphBuilder::<RootToLeaves>::new(DIRECTORY_B.digest());
459 builder
460 .try_insert(DIRECTORY_A.clone())
461 .expect_err("expect insert of root with wrong digest to fail");
462 }
463}