1use std::path::{Path, PathBuf};
4
5use xxhash_rust::xxh3::xxh3_64;
6
7use crate::graph::GraphParams;
8use crate::graph::block::{
9 GraphNodeBlockBuild, GraphNodeBlockInput, GraphNodeError, GraphNodeLayout,
10 NODE_BLOCK_TRAILER_LEN, encode_node_blocks,
11};
12use crate::lifecycle::QueryCancellation;
13use crate::lifecycle::durability::{DurabilityPolicy, SyncRequirement};
14use crate::lifecycle::stats::{AccountedCounter, Accounting, AllocationComponent};
15use crate::lifecycle::{QueryControl, SnapshotLease, Store, StoreError};
16use crate::quant::{Bit4Factors, QuantError, est_dot_bit4, prepare_bit4_query};
17use crate::scan::ScanError;
18use crate::segment::layout::RegionKind;
19use crate::segment::reader::SegmentReader;
20use crate::segment::writer::{
21 SegmentBuild, SegmentCopiedRegion, SegmentDocumentVersions, SegmentFactors,
22 write_segment_with_encoded_graph_and_copied_regions,
23};
24use crate::segment::{SegmentError, SegmentId, SegmentMeta};
25use crate::vfs::Vfs;
26
27const ENTRY_POINT_COUNT: usize = 4;
28const MEDOID_SAMPLE_ROWS: usize = 1_000;
29const CHECKPOINT_MAGIC: [u8; 8] = *b"ZEVAMCP1";
30const CHECKPOINT_VERSION: u16 = 1;
31const CHECKPOINT_HEADER_BYTES: usize = 80;
32const CHECKPOINT_CHECKSUM_BYTES: usize = 8;
33const CANCELLATION_CHECK_ROWS: usize = 64;
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub(crate) enum GraphRewriteRegion {
38 Reencoded,
40 Replaced,
42 CopyForward,
44 Unsupported,
46}
47
48pub(crate) const fn graph_rewrite_source_region(kind: u16) -> GraphRewriteRegion {
50 match RegionKind::from_id(kind) {
51 Some(
52 RegionKind::Columns
53 | RegionKind::Alive
54 | RegionKind::VectorCodes
55 | RegionKind::VectorFactors
56 | RegionKind::VectorRescore
57 | RegionKind::DocumentVersions,
58 ) => GraphRewriteRegion::Reencoded,
59 Some(RegionKind::GraphNodeBlocks | RegionKind::ChecksumTable) => {
60 GraphRewriteRegion::Replaced
61 }
62 Some(RegionKind::Postings | RegionKind::StoredMetadata | RegionKind::StoredText) | None => {
63 GraphRewriteRegion::CopyForward
64 }
65 Some(
66 RegionKind::GraphColocatedCodes
67 | RegionKind::SignPlane
68 | RegionKind::PdxClusteredBlocks
69 | RegionKind::VectorSpaceN,
70 ) => GraphRewriteRegion::Unsupported,
71 }
72}
73
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub enum GraphBuildPasses {
77 #[default]
80 One,
81 Two,
83}
84
85pub struct GraphBuildArtifact {
87 encoded_region: Vec<u8>,
88 entry_points: Vec<u32>,
89 work_rows_completed: u64,
90 _memory: Option<AccountedCounter>,
91}
92
93impl GraphBuildArtifact {
94 #[must_use]
96 pub fn encoded_region(&self) -> &[u8] {
97 &self.encoded_region
98 }
99
100 #[must_use]
102 pub fn entry_points(&self) -> &[u32] {
103 &self.entry_points
104 }
105
106 #[must_use]
108 pub const fn work_rows_completed(&self) -> u64 {
109 self.work_rows_completed
110 }
111
112 pub fn write_segment_with_graph(
114 &self,
115 vfs: &dyn Vfs,
116 directory: &Path,
117 input: &SegmentReader,
118 output_id: SegmentId,
119 policy: DurabilityPolicy,
120 ) -> Result<SegmentMeta, GraphBuildError> {
121 write_segment_with_encoded_graph(
122 &self.encoded_region,
123 vfs,
124 directory,
125 input,
126 output_id,
127 policy,
128 )
129 }
130}
131
132pub(crate) fn write_segment_with_encoded_graph(
133 encoded_region: &[u8],
134 vfs: &dyn Vfs,
135 directory: &Path,
136 input: &SegmentReader,
137 output_id: SegmentId,
138 policy: DurabilityPolicy,
139) -> Result<SegmentMeta, GraphBuildError> {
140 let columns = input.columns()?;
141 let alive = input.alive()?;
142 let factors = input.bit4_factors()?;
143 let mut doc_ids = Vec::with_capacity(input.meta().row_count as usize);
144 let mut revisions = Vec::with_capacity(input.meta().row_count as usize);
145 let mut has_documents = None;
146 for row in 0..input.meta().row_count as usize {
147 let document = input.document_version(row)?;
148 let present = document.is_some();
149 if has_documents.is_some_and(|expected| expected != present) {
150 return Err(GraphBuildError::Segment(SegmentError::Geometry(
151 "document-version region is present for only part of the segment".to_owned(),
152 )));
153 }
154 has_documents = Some(present);
155 if let Some(document) = document {
156 doc_ids.push(document.doc_id());
157 revisions.push(document.revision());
158 }
159 }
160 let build = SegmentBuild {
161 id: output_id,
162 scheme: input.meta().scheme,
163 dims: input.meta().dims,
164 codes: input.bit4_codes()?,
165 factors: SegmentFactors::Bit4(factors),
166 rescore: input.rescore_f32()?,
167 columns: &columns,
168 alive: &alive,
169 };
170 let documents = (has_documents == Some(true)).then_some(SegmentDocumentVersions {
171 doc_ids: &doc_ids,
172 revisions: &revisions,
173 });
174 let mut copied_regions = Vec::new();
175 for entry in input.directory() {
176 match graph_rewrite_source_region(entry.kind) {
177 GraphRewriteRegion::CopyForward => {
178 copied_regions.push(SegmentCopiedRegion {
179 kind: entry.kind,
180 version: entry.version,
181 bytes: input.region_by_id(entry.kind)?,
182 });
183 }
184 GraphRewriteRegion::Unsupported => {
185 return Err(GraphBuildError::Segment(SegmentError::Geometry(format!(
186 "graph rewrite cannot carry source region kind {}",
187 entry.kind
188 ))));
189 }
190 GraphRewriteRegion::Reencoded | GraphRewriteRegion::Replaced => {}
191 }
192 }
193 write_segment_with_encoded_graph_and_copied_regions(
194 vfs,
195 directory,
196 build,
197 encoded_region,
198 documents,
199 &copied_regions,
200 policy,
201 )
202 .map_err(GraphBuildError::Segment)
203}
204
205#[derive(Debug)]
207pub enum GraphBuildError {
208 Profile(crate::graph::search::GraphProfileError),
210 CheckpointIo {
212 path: PathBuf,
214 source: std::io::Error,
216 },
217 CheckpointCorrupt(String),
219 BudgetExhausted {
221 rows_completed: u64,
223 },
224 UnitNorm {
226 row_id: u32,
228 squared_norm: f64,
230 tolerance: f64,
232 },
233 Cancelled {
235 partial: bool,
237 },
238 Timeout {
240 partial: bool,
242 },
243 ReadCancelled {
245 partial: bool,
247 },
248 Segment(SegmentError),
250 Quant(QuantError),
252 NodeBlock(GraphNodeError),
254 Geometry(String),
256 NodeIdOutOfRange {
258 node_id: u32,
260 node_count: u32,
262 },
263 Store(StoreError),
265}
266
267impl std::fmt::Display for GraphBuildError {
268 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 match self {
270 Self::Profile(error) => write!(formatter, "graph build {error}"),
271 Self::CheckpointIo { path, source } => {
272 write!(
273 formatter,
274 "graph build checkpoint I/O {}: {source}",
275 path.display()
276 )
277 }
278 Self::CheckpointCorrupt(detail) => {
279 write!(formatter, "graph build checkpoint is corrupt: {detail}")
280 }
281 Self::BudgetExhausted { rows_completed } => write!(
282 formatter,
283 "graph build work budget exhausted after {rows_completed} checkpointed rows"
284 ),
285 Self::UnitNorm {
286 row_id,
287 squared_norm,
288 tolerance,
289 } => write!(
290 formatter,
291 "angular graph row {row_id} has squared norm {squared_norm}; expected 1 within {tolerance}"
292 ),
293 Self::Cancelled { partial } => {
294 write!(formatter, "graph build was cancelled (partial={partial})")
295 }
296 Self::Timeout { partial } => {
297 write!(
298 formatter,
299 "graph build deadline expired (partial={partial})"
300 )
301 }
302 Self::ReadCancelled { partial } => write!(
303 formatter,
304 "store close cancelled graph build (partial={partial})"
305 ),
306 Self::Segment(error) => write!(formatter, "graph build segment input: {error}"),
307 Self::Quant(error) => write!(formatter, "graph build Bit4 score: {error}"),
308 Self::NodeBlock(error) => write!(formatter, "graph build node blocks: {error}"),
309 Self::Geometry(detail) => write!(formatter, "graph build geometry: {detail}"),
310 Self::NodeIdOutOfRange {
311 node_id,
312 node_count,
313 } => write!(
314 formatter,
315 "graph build node id {node_id} is outside dense row count {node_count}"
316 ),
317 Self::Store(error) => write!(formatter, "graph build memory accounting: {error}"),
318 }
319 }
320}
321
322impl std::error::Error for GraphBuildError {
323 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
324 match self {
325 Self::Profile(error) => Some(error),
326 Self::CheckpointIo { source, .. } => Some(source),
327 Self::Segment(error) => Some(error),
328 Self::Quant(error) => Some(error),
329 Self::NodeBlock(error) => Some(error),
330 Self::Store(error) => Some(error),
331 Self::CheckpointCorrupt(_)
332 | Self::BudgetExhausted { .. }
333 | Self::UnitNorm { .. }
334 | Self::Cancelled { .. }
335 | Self::Timeout { .. }
336 | Self::ReadCancelled { .. }
337 | Self::Geometry(_)
338 | Self::NodeIdOutOfRange { .. } => None,
339 }
340 }
341}
342
343struct GraphBuildSession<'a> {
344 vfs: &'a dyn Vfs,
345 policy: DurabilityPolicy,
346 vectors: SegmentVectors<'a>,
347 params: GraphParams,
348 seed: u64,
349 passes: GraphBuildPasses,
350 checkpoint_path: PathBuf,
351 state: BuildState,
352}
353
354impl<'a> GraphBuildSession<'a> {
355 #[allow(clippy::too_many_arguments)]
356 fn new(
357 vfs: &'a dyn Vfs,
358 reader: &'a SegmentReader,
359 params: GraphParams,
360 seed: u64,
361 passes: GraphBuildPasses,
362 checkpoint_path: &Path,
363 policy: DurabilityPolicy,
364 accounting: Option<&std::sync::Arc<Accounting>>,
365 ) -> Result<Self, GraphBuildError> {
366 let vectors = SegmentVectors::new(reader)?;
367 let state = match vfs.read(checkpoint_path) {
368 Ok(bytes) => {
369 match decode_checkpoint(&bytes, &vectors, params, seed, passes, accounting) {
370 Ok(state) => state,
371 Err(error @ GraphBuildError::CheckpointCorrupt(_)) => {
372 remove_checkpoint(vfs, checkpoint_path)?;
373 return Err(error);
374 }
375 Err(other) => return Err(other),
376 }
377 }
378 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
379 BuildState::new(&vectors, params, seed, accounting)?
380 }
381 Err(source) => {
382 return Err(GraphBuildError::CheckpointIo {
383 path: checkpoint_path.to_path_buf(),
384 source,
385 });
386 }
387 };
388 Ok(Self {
389 vfs,
390 policy,
391 vectors,
392 params,
393 seed,
394 passes,
395 checkpoint_path: checkpoint_path.to_path_buf(),
396 state,
397 })
398 }
399
400 fn advance_batch(
401 &mut self,
402 cancellation: &QueryCancellation<'_>,
403 ) -> Result<bool, GraphBuildError> {
404 check_cancellation(cancellation)?;
405 let complete = self.state.advance_batch(
406 &self.vectors,
407 self.params,
408 self.seed,
409 self.passes,
410 Some(cancellation),
411 )?;
412 check_cancellation(cancellation)?;
413 {
414 let checkpoint_bytes = checkpoint_encoded_bytes(&self.state)?;
415 let base_charge = match self.state.memory.as_mut() {
416 Some(charge) => {
417 let base = charge.bytes();
418 let peak = usize::try_from(base)
419 .ok()
420 .and_then(|base| base.checked_add(checkpoint_bytes))
421 .ok_or_else(|| {
422 GraphBuildError::Geometry("checkpoint accounting overflow".to_owned())
423 })?;
424 charge.set(peak)?;
425 Some(base)
426 }
427 None => None,
428 };
429 let written = write_checkpoint(
430 self.vfs,
431 self.policy,
432 &self.checkpoint_path,
433 &self.vectors,
434 self.params,
435 self.seed,
436 self.passes,
437 &self.state,
438 );
439 if let (Some(charge), Some(base)) = (self.state.memory.as_mut(), base_charge) {
440 let base = usize::try_from(base).map_err(|_| {
441 GraphBuildError::Geometry("base accounting exceeds usize".to_owned())
442 })?;
443 charge.set(base)?;
444 }
445 written?;
446 }
447 Ok(complete)
448 }
449
450 fn completed_work_rows(&self) -> Result<u64, GraphBuildError> {
451 let rows = u64::try_from(self.state.order.len())
452 .map_err(|_| GraphBuildError::Geometry("graph row count exceeds u64".to_owned()))?;
453 let next = u64::try_from(self.state.next_index)
454 .map_err(|_| GraphBuildError::Geometry("graph progress exceeds u64".to_owned()))?;
455 match self.state.phase {
456 BuildPhase::Build => Ok(next),
457 BuildPhase::Refine => rows
458 .checked_add(next)
459 .ok_or_else(|| GraphBuildError::Geometry("graph progress overflow".to_owned())),
460 BuildPhase::Complete => match self.passes {
461 GraphBuildPasses::One => Ok(rows),
462 GraphBuildPasses::Two => rows
463 .checked_mul(2)
464 .ok_or_else(|| GraphBuildError::Geometry("graph progress overflow".to_owned())),
465 },
466 }
467 }
468
469 fn artifact(self) -> Result<GraphBuildArtifact, GraphBuildError> {
470 if !self.state.is_complete(self.passes) {
471 return Err(GraphBuildError::Geometry(
472 "graph artifact requested before every pass completed".to_owned(),
473 ));
474 }
475 let BuildState {
476 entries,
477 adjacency,
478 memory,
479 ..
480 } = self.state;
481 let artifact = encode_artifact(&self.vectors, self.params, &entries, &adjacency, memory)?;
482 remove_checkpoint(self.vfs, &self.checkpoint_path)?;
483 Ok(artifact)
484 }
485}
486
487impl From<SegmentError> for GraphBuildError {
488 fn from(error: SegmentError) -> Self {
489 Self::Segment(error)
490 }
491}
492
493impl From<QuantError> for GraphBuildError {
494 fn from(error: QuantError) -> Self {
495 Self::Quant(error)
496 }
497}
498
499impl From<GraphNodeError> for GraphBuildError {
500 fn from(error: GraphNodeError) -> Self {
501 Self::NodeBlock(error)
502 }
503}
504
505impl From<StoreError> for GraphBuildError {
506 fn from(error: StoreError) -> Self {
507 Self::Store(error)
508 }
509}
510
511#[cfg(test)]
512fn build_graph(
513 reader: &SegmentReader,
514 params: GraphParams,
515 seed: u64,
516 passes: GraphBuildPasses,
517) -> Result<GraphBuildArtifact, GraphBuildError> {
518 let vectors = SegmentVectors::new(reader)?;
519 let mut state = BuildState::new(&vectors, params, seed, None)?;
520 while !state.advance_batch(&vectors, params, seed, passes, None)? {}
521 let BuildState {
522 entries,
523 adjacency,
524 memory,
525 ..
526 } = state;
527 encode_artifact(&vectors, params, &entries, &adjacency, memory)
528}
529
530#[derive(Clone, Copy, Debug)]
532pub struct CheckpointedGraphBuild<'a> {
533 params: GraphParams,
534 seed: u64,
535 passes: GraphBuildPasses,
536 checkpoint_path: &'a Path,
537 control: &'a QueryControl,
538 max_work_rows: Option<u64>,
539}
540
541impl<'a> CheckpointedGraphBuild<'a> {
542 #[must_use]
544 pub fn new(
545 params: GraphParams,
546 seed: u64,
547 passes: GraphBuildPasses,
548 checkpoint_path: &'a Path,
549 control: &'a QueryControl,
550 ) -> Self {
551 Self {
552 params,
553 seed,
554 passes,
555 checkpoint_path,
556 control,
557 max_work_rows: None,
558 }
559 }
560
561 #[must_use]
565 pub const fn with_max_work_rows(mut self, max_work_rows: u64) -> Self {
566 self.max_work_rows = Some(max_work_rows);
567 self
568 }
569}
570
571pub fn build_graph_checkpointed(
578 store: &Store,
579 reader: &SegmentReader,
580 request: CheckpointedGraphBuild<'_>,
581 lease: &SnapshotLease,
582) -> Result<GraphBuildArtifact, GraphBuildError> {
583 let cancellation = QueryCancellation::new(request.control, lease);
584 check_cancellation(&cancellation)?;
585 if lease.graph_profile() == Ok(crate::graph::search::EpochGraphProfile::AngularClass) {
586 validate_angular_rows(reader)?;
587 }
588 let mut session = GraphBuildSession::new(
589 store.vfs.as_ref(),
590 reader,
591 request.params,
592 request.seed,
593 request.passes,
594 request.checkpoint_path,
595 store.durability_policy,
596 Some(&store.accounting),
597 )?;
598 let started_rows = session.completed_work_rows()?;
599 loop {
600 let complete = session.advance_batch(&cancellation)?;
601 let completed_rows = session
602 .completed_work_rows()?
603 .checked_sub(started_rows)
604 .ok_or_else(|| GraphBuildError::Geometry("graph work progress regressed".to_owned()))?;
605 if complete {
606 let mut artifact = session.artifact()?;
607 artifact.work_rows_completed = completed_rows;
608 return Ok(artifact);
609 }
610 if request
611 .max_work_rows
612 .is_some_and(|maximum| completed_rows >= maximum)
613 {
614 return Err(GraphBuildError::BudgetExhausted {
615 rows_completed: completed_rows,
616 });
617 }
618 }
619}
620
621fn validate_angular_rows(reader: &SegmentReader) -> Result<(), GraphBuildError> {
622 let dimensions = reader.meta().dims as usize;
623 for (row, vector) in reader.rescore_f32()?.chunks_exact(dimensions).enumerate() {
624 if let Some(squared_norm) = crate::graph::search::non_unit_squared_norm(vector) {
625 return Err(GraphBuildError::UnitNorm {
626 row_id: u32::try_from(row).map_err(|_| {
627 GraphBuildError::Geometry("angular row id exceeds u32".to_owned())
628 })?,
629 squared_norm,
630 tolerance: crate::graph::search::UNIT_NORM_SQUARED_TOLERANCE,
631 });
632 }
633 }
634 Ok(())
635}
636
637struct SegmentVectors<'a> {
638 segment_id: [u8; 16],
639 node_count: u32,
640 dimensions: usize,
641 code_stride: usize,
642 codes: &'a [u8],
643 factors: &'a [Bit4Factors],
644 rescore: &'a [f32],
645}
646
647impl<'a> SegmentVectors<'a> {
648 fn new(reader: &'a SegmentReader) -> Result<Self, GraphBuildError> {
649 if reader.meta().scheme != 4 {
650 return Err(GraphBuildError::Geometry(format!(
651 "flat Vamana requires Bit4 scheme 4, got {}",
652 reader.meta().scheme
653 )));
654 }
655 let dimensions = reader.meta().dims as usize;
656 let node_count = reader.meta().row_count;
657 if node_count == 0 {
658 return Err(GraphBuildError::Geometry(
659 "flat Vamana requires at least one row".to_owned(),
660 ));
661 }
662 Ok(Self {
663 segment_id: *reader.meta().id.as_bytes(),
664 node_count,
665 dimensions,
666 code_stride: dimensions.div_ceil(2),
667 codes: reader.bit4_codes()?,
668 factors: reader.bit4_factors()?,
669 rescore: reader.rescore_f32()?,
670 })
671 }
672
673 fn validate_node(&self, node_id: u32) -> Result<usize, GraphBuildError> {
674 if node_id >= self.node_count {
675 return Err(GraphBuildError::NodeIdOutOfRange {
676 node_id,
677 node_count: self.node_count,
678 });
679 }
680 usize::try_from(node_id)
681 .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))
682 }
683
684 fn f32_row(&self, node_id: u32) -> Result<&'a [f32], GraphBuildError> {
685 let node = self.validate_node(node_id)?;
686 let start = node
687 .checked_mul(self.dimensions)
688 .ok_or_else(|| GraphBuildError::Geometry("f32 row offset overflow".to_owned()))?;
689 let end = start
690 .checked_add(self.dimensions)
691 .ok_or_else(|| GraphBuildError::Geometry("f32 row end overflow".to_owned()))?;
692 self.rescore.get(start..end).ok_or_else(|| {
693 GraphBuildError::Geometry(format!("f32 row {node_id} is outside validated rescore"))
694 })
695 }
696
697 fn code_row(&self, node_id: u32) -> Result<&'a [u8], GraphBuildError> {
698 let node = self.validate_node(node_id)?;
699 let start = node
700 .checked_mul(self.code_stride)
701 .ok_or_else(|| GraphBuildError::Geometry("Bit4 row offset overflow".to_owned()))?;
702 let end = start
703 .checked_add(self.code_stride)
704 .ok_or_else(|| GraphBuildError::Geometry("Bit4 row end overflow".to_owned()))?;
705 self.codes.get(start..end).ok_or_else(|| {
706 GraphBuildError::Geometry(format!("Bit4 row {node_id} is outside validated codes"))
707 })
708 }
709
710 fn factor(&self, node_id: u32) -> Result<Bit4Factors, GraphBuildError> {
711 let node = self.validate_node(node_id)?;
712 self.factors.get(node).copied().ok_or_else(|| {
713 GraphBuildError::Geometry(format!("Bit4 factor {node_id} is unavailable"))
714 })
715 }
716
717 fn exact_distance(&self, left: u32, right: u32) -> Result<f64, GraphBuildError> {
718 let left = self.f32_row(left)?;
719 let right = self.f32_row(right)?;
720 Ok(left
721 .iter()
722 .zip(right)
723 .map(|(left, right)| {
724 let difference = f64::from(*left) - f64::from(*right);
725 difference * difference
726 })
727 .sum())
728 }
729}
730
731struct Adjacency {
732 slots: Vec<u32>,
733 degrees: Vec<u8>,
734 node_count: u32,
735 r_max: u8,
736}
737
738impl Adjacency {
739 fn new(node_count: u32, r_max: u8) -> Result<Self, GraphBuildError> {
740 let slot_count = (node_count as usize)
741 .checked_mul(usize::from(r_max))
742 .ok_or_else(|| GraphBuildError::Geometry("adjacency size overflow".to_owned()))?;
743 Ok(Self {
744 slots: vec![u32::MAX; slot_count],
745 degrees: vec![0; node_count as usize],
746 node_count,
747 r_max,
748 })
749 }
750
751 fn row_range(&self, node_id: u32) -> Result<std::ops::Range<usize>, GraphBuildError> {
752 if node_id >= self.node_count {
753 return Err(GraphBuildError::NodeIdOutOfRange {
754 node_id,
755 node_count: self.node_count,
756 });
757 }
758 let start = (node_id as usize)
759 .checked_mul(usize::from(self.r_max))
760 .ok_or_else(|| GraphBuildError::Geometry("adjacency offset overflow".to_owned()))?;
761 let end = start
762 .checked_add(usize::from(self.r_max))
763 .ok_or_else(|| GraphBuildError::Geometry("adjacency end overflow".to_owned()))?;
764 Ok(start..end)
765 }
766
767 fn neighbors(&self, node_id: u32) -> Result<&[u32], GraphBuildError> {
768 let node = usize::try_from(node_id)
769 .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
770 let degree = usize::from(*self.degrees.get(node).ok_or(
771 GraphBuildError::NodeIdOutOfRange {
772 node_id,
773 node_count: self.node_count,
774 },
775 )?);
776 let range = self.row_range(node_id)?;
777 let start = range.start;
778 let active_end = start
779 .checked_add(degree)
780 .ok_or_else(|| GraphBuildError::Geometry("degree end overflow".to_owned()))?;
781 self.slots.get(start..active_end).ok_or_else(|| {
782 GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
783 })
784 }
785
786 fn set_neighbors(&mut self, node_id: u32, neighbors: &[u32]) -> Result<(), GraphBuildError> {
787 if neighbors.len() > usize::from(self.r_max) {
788 return Err(GraphBuildError::Geometry(format!(
789 "node {node_id} degree {} exceeds hard cap {}",
790 neighbors.len(),
791 self.r_max
792 )));
793 }
794 for neighbor in neighbors {
795 if *neighbor >= self.node_count {
796 return Err(GraphBuildError::NodeIdOutOfRange {
797 node_id: *neighbor,
798 node_count: self.node_count,
799 });
800 }
801 }
802 let range = self.row_range(node_id)?;
803 let row = self.slots.get_mut(range).ok_or_else(|| {
804 GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
805 })?;
806 row.fill(u32::MAX);
807 let destination = row.get_mut(..neighbors.len()).ok_or_else(|| {
808 GraphBuildError::Geometry(format!("adjacency row {node_id} active slice failed"))
809 })?;
810 destination.copy_from_slice(neighbors);
811 let node = usize::try_from(node_id)
812 .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
813 let degree = self
814 .degrees
815 .get_mut(node)
816 .ok_or(GraphBuildError::NodeIdOutOfRange {
817 node_id,
818 node_count: self.node_count,
819 })?;
820 *degree = u8::try_from(neighbors.len())
821 .map_err(|_| GraphBuildError::Geometry("degree exceeds u8".to_owned()))?;
822 Ok(())
823 }
824}
825
826#[derive(Clone, Copy, Debug, Eq, PartialEq)]
827enum BuildPhase {
828 Build,
829 Refine,
830 Complete,
831}
832
833fn build_arena_bytes(node_count: u32, params: GraphParams) -> Result<usize, GraphBuildError> {
834 let rows = usize::try_from(node_count)
835 .map_err(|_| GraphBuildError::Geometry("node count exceeds usize".to_owned()))?;
836 let slots = rows
837 .checked_mul(usize::from(params.r_max()))
838 .and_then(|count| count.checked_mul(std::mem::size_of::<u32>()))
839 .ok_or_else(|| GraphBuildError::Geometry("accounted adjacency overflow".to_owned()))?;
840 let row_state = rows
841 .checked_mul(
842 std::mem::size_of::<u32>()
843 .checked_mul(2)
844 .and_then(|bytes| bytes.checked_add(2))
845 .ok_or_else(|| GraphBuildError::Geometry("row state width overflow".to_owned()))?,
846 )
847 .ok_or_else(|| GraphBuildError::Geometry("accounted row state overflow".to_owned()))?;
848 let construction = usize::from(params.l_build());
849 let scratch_per_candidate = std::mem::size_of::<ScoredNode>()
850 .checked_mul(4)
851 .and_then(|bytes| bytes.checked_add(std::mem::size_of::<u32>() * 6))
852 .ok_or_else(|| GraphBuildError::Geometry("scratch width overflow".to_owned()))?;
853 let candidate_scratch = construction
854 .checked_add(usize::from(params.r_max()))
855 .and_then(|candidates| candidates.checked_mul(scratch_per_candidate))
856 .ok_or_else(|| GraphBuildError::Geometry("scratch arena overflow".to_owned()))?;
857 let medoid_scratch = MEDOID_SAMPLE_ROWS
858 .checked_mul(std::mem::size_of::<ScoredNode>())
859 .ok_or_else(|| GraphBuildError::Geometry("medoid scratch overflow".to_owned()))?;
860 let scratch = candidate_scratch.max(medoid_scratch);
861 slots
862 .checked_add(row_state)
863 .and_then(|bytes| bytes.checked_add(ENTRY_POINT_COUNT * std::mem::size_of::<u32>()))
864 .and_then(|bytes| bytes.checked_add(scratch))
865 .ok_or_else(|| GraphBuildError::Geometry("graph build arena overflow".to_owned()))
866}
867
868struct BuildState {
869 phase: BuildPhase,
870 next_index: usize,
871 order: Vec<u32>,
872 entries: Vec<u32>,
873 adjacency: Adjacency,
874 inserted: Vec<u8>,
875 visited: Vec<u32>,
876 visit_epoch: u32,
877 memory: Option<AccountedCounter>,
878}
879
880impl BuildState {
881 fn new(
882 vectors: &SegmentVectors<'_>,
883 params: GraphParams,
884 seed: u64,
885 accounting: Option<&std::sync::Arc<Accounting>>,
886 ) -> Result<Self, GraphBuildError> {
887 let mut memory = match accounting {
888 Some(accounting) => Some(AccountedCounter::new(
889 accounting,
890 AllocationComponent::Temporary,
891 )?),
892 None => None,
893 };
894 if let Some(charge) = memory.as_mut() {
895 charge.set(build_arena_bytes(vectors.node_count, params)?)?;
896 }
897 let mut random = SplitMix64::new(seed);
898 let mut order = (0..vectors.node_count).collect::<Vec<_>>();
899 shuffle(&mut order, &mut random)?;
900 let entries = refined_entry_points(vectors, &order)?;
901 move_entries_to_front(&mut order, &entries);
902 Ok(Self {
903 phase: BuildPhase::Build,
904 next_index: 0,
905 order,
906 entries,
907 adjacency: Adjacency::new(vectors.node_count, params.r_max())?,
908 inserted: vec![0; vectors.node_count as usize],
909 visited: vec![0; vectors.node_count as usize],
910 visit_epoch: 0,
911 memory,
912 })
913 }
914
915 fn is_complete(&self, _passes: GraphBuildPasses) -> bool {
916 self.phase == BuildPhase::Complete
917 }
918
919 fn advance_batch(
920 &mut self,
921 vectors: &SegmentVectors<'_>,
922 params: GraphParams,
923 seed: u64,
924 passes: GraphBuildPasses,
925 cancellation: Option<&QueryCancellation<'_>>,
926 ) -> Result<bool, GraphBuildError> {
927 if self.phase == BuildPhase::Complete {
928 return Ok(true);
929 }
930 let batch_rows = usize::try_from(params.checkpoint_batch_rows()).map_err(|_| {
931 GraphBuildError::Geometry("checkpoint batch rows exceed usize".to_owned())
932 })?;
933 let end = self
934 .next_index
935 .checked_add(batch_rows)
936 .map(|end| end.min(self.order.len()))
937 .ok_or_else(|| GraphBuildError::Geometry("batch end overflow".to_owned()))?;
938 let alpha = match self.phase {
939 BuildPhase::Build => params.alpha_build(),
940 BuildPhase::Refine => params.alpha_refine(),
941 BuildPhase::Complete => return Ok(true),
942 };
943 let pass_seed = match self.phase {
944 BuildPhase::Build => seed,
945 BuildPhase::Refine => seed ^ 0xa1fa_1200_0000_0002,
946 BuildPhase::Complete => seed,
947 };
948 for position in self.next_index..end {
949 if position.is_multiple_of(CANCELLATION_CHECK_ROWS)
950 && let Some(cancellation) = cancellation
951 {
952 check_cancellation(cancellation)?;
953 }
954 let order_position = match self.phase {
955 BuildPhase::Build => position,
956 BuildPhase::Refine => {
957 self.order.len().checked_sub(position + 1).ok_or_else(|| {
958 GraphBuildError::Geometry("refinement order underflow".to_owned())
959 })?
960 }
961 BuildPhase::Complete => position,
962 };
963 let node_id = *self.order.get(order_position).ok_or_else(|| {
964 GraphBuildError::Geometry(format!(
965 "build order position {order_position} is unavailable"
966 ))
967 })?;
968 process_node(
969 vectors,
970 params,
971 pass_seed,
972 alpha,
973 node_id,
974 &self.entries,
975 &mut self.adjacency,
976 &mut self.inserted,
977 &mut self.visited,
978 &mut self.visit_epoch,
979 )?;
980 }
981 self.next_index = end;
982 if self.next_index == self.order.len() {
983 match (self.phase, passes) {
984 (BuildPhase::Build, GraphBuildPasses::Two) => {
985 self.phase = BuildPhase::Refine;
986 self.next_index = 0;
987 self.inserted.fill(1);
988 }
989 (BuildPhase::Build, GraphBuildPasses::One) | (BuildPhase::Refine, _) => {
990 self.phase = BuildPhase::Complete;
991 }
992 (BuildPhase::Complete, _) => {}
993 }
994 }
995 Ok(self.phase == BuildPhase::Complete)
996 }
997}
998
999fn check_cancellation(cancellation: &QueryCancellation<'_>) -> Result<(), GraphBuildError> {
1000 match cancellation.check_graph() {
1001 Ok(()) => Ok(()),
1002 Err(ScanError::Cancelled { .. }) => Err(GraphBuildError::Cancelled { partial: false }),
1003 Err(ScanError::Timeout { .. }) => Err(GraphBuildError::Timeout { partial: false }),
1004 Err(ScanError::ReadCancelled { .. }) => {
1005 Err(GraphBuildError::ReadCancelled { partial: false })
1006 }
1007 Err(error) => Err(GraphBuildError::Geometry(format!(
1008 "query cancellation seam returned non-cancellation error: {error}"
1009 ))),
1010 }
1011}
1012
1013fn checkpoint_temp_path(path: &Path) -> PathBuf {
1014 let mut temporary = path.as_os_str().to_os_string();
1015 temporary.push(".tmp");
1016 PathBuf::from(temporary)
1017}
1018
1019#[allow(clippy::too_many_arguments)]
1020fn write_checkpoint(
1021 vfs: &dyn Vfs,
1022 policy: DurabilityPolicy,
1023 path: &Path,
1024 vectors: &SegmentVectors<'_>,
1025 params: GraphParams,
1026 seed: u64,
1027 passes: GraphBuildPasses,
1028 state: &BuildState,
1029) -> Result<(), GraphBuildError> {
1030 let directory = path.parent().ok_or_else(|| GraphBuildError::CheckpointIo {
1031 path: path.to_path_buf(),
1032 source: std::io::Error::new(
1033 std::io::ErrorKind::InvalidInput,
1034 "checkpoint path has no parent directory",
1035 ),
1036 })?;
1037 let slot_bytes = state
1038 .adjacency
1039 .slots
1040 .len()
1041 .checked_mul(std::mem::size_of::<u32>())
1042 .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1043 let payload_bytes = state
1044 .adjacency
1045 .degrees
1046 .len()
1047 .checked_add(slot_bytes)
1048 .ok_or_else(|| GraphBuildError::Geometry("checkpoint payload overflow".to_owned()))?;
1049 let total_bytes = CHECKPOINT_HEADER_BYTES
1050 .checked_add(payload_bytes)
1051 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1052 .ok_or_else(|| GraphBuildError::Geometry("checkpoint length overflow".to_owned()))?;
1053 let mut bytes = Vec::with_capacity(total_bytes);
1054 bytes.extend_from_slice(&CHECKPOINT_MAGIC);
1055 bytes.extend_from_slice(&CHECKPOINT_VERSION.to_le_bytes());
1056 bytes.push(phase_code(state.phase));
1057 bytes.push(pass_code(passes));
1058 bytes.push(params.r_target());
1059 bytes.push(params.r_max());
1060 bytes.extend_from_slice(&0_u16.to_le_bytes());
1061 bytes.extend_from_slice(¶ms.l_build().to_le_bytes());
1062 bytes.extend_from_slice(&0_u16.to_le_bytes());
1063 bytes.extend_from_slice(¶ms.checkpoint_batch_rows().to_le_bytes());
1064 bytes.extend_from_slice(&vectors.node_count.to_le_bytes());
1065 let dimensions = u32::try_from(vectors.dimensions)
1066 .map_err(|_| GraphBuildError::Geometry("checkpoint dimensions exceed u32".to_owned()))?;
1067 bytes.extend_from_slice(&dimensions.to_le_bytes());
1068 let next_index = u32::try_from(state.next_index)
1069 .map_err(|_| GraphBuildError::Geometry("checkpoint position exceeds u32".to_owned()))?;
1070 bytes.extend_from_slice(&next_index.to_le_bytes());
1071 bytes.extend_from_slice(&0_u32.to_le_bytes());
1072 bytes.extend_from_slice(&seed.to_le_bytes());
1073 bytes.extend_from_slice(¶ms.alpha_build().to_bits().to_le_bytes());
1074 bytes.extend_from_slice(¶ms.alpha_refine().to_bits().to_le_bytes());
1075 bytes.extend_from_slice(&vectors.segment_id);
1076 bytes.extend_from_slice(
1077 &u64::try_from(payload_bytes)
1078 .map_err(|_| GraphBuildError::Geometry("checkpoint payload exceeds u64".to_owned()))?
1079 .to_le_bytes(),
1080 );
1081 if bytes.len() != CHECKPOINT_HEADER_BYTES {
1082 return Err(GraphBuildError::Geometry(format!(
1083 "checkpoint header encoded {} bytes, expected {CHECKPOINT_HEADER_BYTES}",
1084 bytes.len()
1085 )));
1086 }
1087 bytes.extend_from_slice(&state.adjacency.degrees);
1088 for neighbor in &state.adjacency.slots {
1089 bytes.extend_from_slice(&neighbor.to_le_bytes());
1090 }
1091 bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
1092 let temporary = checkpoint_temp_path(path);
1093 if let Err(source) = vfs.write(&temporary, &bytes) {
1094 let _ = vfs.delete(&temporary);
1095 return Err(GraphBuildError::CheckpointIo {
1096 path: temporary,
1097 source,
1098 });
1099 }
1100 if let SyncRequirement::Sync(kind) = policy.data_file_sync()
1101 && let Err(source) = vfs.sync(&temporary, kind)
1102 {
1103 let _ = vfs.delete(&temporary);
1104 return Err(GraphBuildError::CheckpointIo {
1105 path: temporary,
1106 source,
1107 });
1108 }
1109 if let Err(source) = vfs.rename(&temporary, path) {
1110 let _ = vfs.delete(&temporary);
1111 return Err(GraphBuildError::CheckpointIo {
1112 path: path.to_path_buf(),
1113 source,
1114 });
1115 }
1116 match policy.directory_sync() {
1117 SyncRequirement::Skip => Ok(()),
1118 SyncRequirement::Sync(kind) => {
1119 vfs.sync(directory, kind)
1120 .map_err(|source| GraphBuildError::CheckpointIo {
1121 path: directory.to_path_buf(),
1122 source,
1123 })
1124 }
1125 }
1126}
1127
1128fn checkpoint_encoded_bytes(state: &BuildState) -> Result<usize, GraphBuildError> {
1129 let slot_bytes = state
1130 .adjacency
1131 .slots
1132 .len()
1133 .checked_mul(std::mem::size_of::<u32>())
1134 .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1135 CHECKPOINT_HEADER_BYTES
1136 .checked_add(state.adjacency.degrees.len())
1137 .and_then(|bytes| bytes.checked_add(slot_bytes))
1138 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1139 .ok_or_else(|| GraphBuildError::Geometry("checkpoint encoded bytes overflow".to_owned()))
1140}
1141
1142fn remove_checkpoint(vfs: &dyn Vfs, path: &Path) -> Result<(), GraphBuildError> {
1143 let temporary = checkpoint_temp_path(path);
1144 match vfs.delete(&temporary) {
1145 Ok(()) => {}
1146 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1147 Err(source) => {
1148 return Err(GraphBuildError::CheckpointIo {
1149 path: temporary,
1150 source,
1151 });
1152 }
1153 }
1154 match vfs.delete(path) {
1155 Ok(()) => Ok(()),
1156 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
1157 Err(source) => Err(GraphBuildError::CheckpointIo {
1158 path: path.to_path_buf(),
1159 source,
1160 }),
1161 }
1162}
1163
1164fn decode_checkpoint(
1165 bytes: &[u8],
1166 vectors: &SegmentVectors<'_>,
1167 params: GraphParams,
1168 seed: u64,
1169 passes: GraphBuildPasses,
1170 accounting: Option<&std::sync::Arc<Accounting>>,
1171) -> Result<BuildState, GraphBuildError> {
1172 validate_graph_build_checkpoint(bytes)?;
1173 let minimum = CHECKPOINT_HEADER_BYTES
1174 .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1175 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1176 if bytes.len() < minimum {
1177 return Err(GraphBuildError::CheckpointCorrupt(format!(
1178 "truncated: need at least {minimum} bytes, got {}",
1179 bytes.len()
1180 )));
1181 }
1182 if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1183 return Err(GraphBuildError::CheckpointCorrupt(
1184 "magic does not match ZEVAMCP1".to_owned(),
1185 ));
1186 }
1187 let checksum_start = bytes
1188 .len()
1189 .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1190 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1191 let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1192 let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1193 GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1194 })?;
1195 let actual_checksum = xxh3_64(checksummed);
1196 if stored_checksum != actual_checksum {
1197 return Err(GraphBuildError::CheckpointCorrupt(format!(
1198 "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1199 )));
1200 }
1201 let version = read_checkpoint_u16(bytes, 8, "version")?;
1202 if version != CHECKPOINT_VERSION {
1203 return Err(GraphBuildError::CheckpointCorrupt(format!(
1204 "version {version}, expected {CHECKPOINT_VERSION}"
1205 )));
1206 }
1207 let phase = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1208 let stored_passes = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1209 if stored_passes != passes {
1210 return Err(GraphBuildError::CheckpointCorrupt(
1211 "pass count does not match requested build".to_owned(),
1212 ));
1213 }
1214 let stored_r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1215 let stored_r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1216 let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1217 let stored_l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1218 let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1219 let stored_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1220 let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1221 let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1222 let next_index = read_checkpoint_u32(bytes, 32, "next index")?;
1223 let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved index bytes")?;
1224 let stored_seed = read_checkpoint_u64(bytes, 40, "seed")?;
1225 let stored_alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1226 let stored_alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1227 let segment_id = bytes
1228 .get(56..72)
1229 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("segment id is truncated".to_owned()))?;
1230 let payload_bytes = read_checkpoint_u64(bytes, 72, "payload length")?;
1231 if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1232 return Err(GraphBuildError::CheckpointCorrupt(
1233 "reserved header bytes are non-zero".to_owned(),
1234 ));
1235 }
1236 if stored_r_target != params.r_target()
1237 || stored_r_max != params.r_max()
1238 || stored_l_build != params.l_build()
1239 || stored_batch != params.checkpoint_batch_rows()
1240 || stored_alpha_build.to_bits() != params.alpha_build().to_bits()
1241 || stored_alpha_refine.to_bits() != params.alpha_refine().to_bits()
1242 || node_count != vectors.node_count
1243 || dimensions != vectors.dimensions as u32
1244 || stored_seed != seed
1245 || segment_id != vectors.segment_id
1246 {
1247 return Err(GraphBuildError::CheckpointCorrupt(
1248 "checkpoint identity or graph parameters do not match input".to_owned(),
1249 ));
1250 }
1251 if passes == GraphBuildPasses::One && phase == BuildPhase::Refine {
1252 return Err(GraphBuildError::CheckpointCorrupt(
1253 "one-pass build cannot resume refinement".to_owned(),
1254 ));
1255 }
1256 let actual_payload = checksum_start
1257 .checked_sub(CHECKPOINT_HEADER_BYTES)
1258 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("payload underflow".to_owned()))?;
1259 if usize::try_from(payload_bytes).ok() != Some(actual_payload) {
1260 return Err(GraphBuildError::CheckpointCorrupt(format!(
1261 "declared payload {payload_bytes} bytes, actual {actual_payload}"
1262 )));
1263 }
1264 let degree_bytes = node_count as usize;
1265 let degrees_end = CHECKPOINT_HEADER_BYTES
1266 .checked_add(degree_bytes)
1267 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree end overflow".to_owned()))?;
1268 let degrees = bytes
1269 .get(CHECKPOINT_HEADER_BYTES..degrees_end)
1270 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree bytes are truncated".to_owned()))?
1271 .to_vec();
1272 let expected_slots = (node_count as usize)
1273 .checked_mul(usize::from(params.r_max()))
1274 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1275 let slot_payload = bytes.get(degrees_end..checksum_start).ok_or_else(|| {
1276 GraphBuildError::CheckpointCorrupt("neighbor payload is unavailable".to_owned())
1277 })?;
1278 let expected_slot_bytes = expected_slots
1279 .checked_mul(std::mem::size_of::<u32>())
1280 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot bytes overflow".to_owned()))?;
1281 if slot_payload.len() != expected_slot_bytes {
1282 return Err(GraphBuildError::CheckpointCorrupt(format!(
1283 "neighbor payload {} bytes, expected {expected_slot_bytes}",
1284 slot_payload.len()
1285 )));
1286 }
1287 let slots = slot_payload
1288 .chunks_exact(4)
1289 .map(|chunk| {
1290 let raw: [u8; 4] = chunk.try_into().map_err(|_| {
1291 GraphBuildError::CheckpointCorrupt("neighbor width is not four bytes".to_owned())
1292 })?;
1293 Ok(u32::from_le_bytes(raw))
1294 })
1295 .collect::<Result<Vec<_>, GraphBuildError>>()?;
1296 let adjacency = Adjacency {
1297 slots,
1298 degrees,
1299 node_count,
1300 r_max: params.r_max(),
1301 };
1302 validate_checkpoint_adjacency(&adjacency)?;
1303 let mut state = BuildState::new(vectors, params, seed, accounting)?;
1304 state.phase = phase;
1305 state.next_index = usize::try_from(next_index)
1306 .map_err(|_| GraphBuildError::CheckpointCorrupt("next index exceeds usize".to_owned()))?;
1307 if state.next_index > state.order.len() {
1308 return Err(GraphBuildError::CheckpointCorrupt(format!(
1309 "next index {} exceeds row count {}",
1310 state.next_index,
1311 state.order.len()
1312 )));
1313 }
1314 state.adjacency = adjacency;
1315 state.inserted.fill(u8::from(phase != BuildPhase::Build));
1316 if phase == BuildPhase::Build {
1317 for node_id in state.order.iter().take(state.next_index) {
1318 let node = vectors.validate_node(*node_id)?;
1319 let marker = state.inserted.get_mut(node).ok_or_else(|| {
1320 GraphBuildError::CheckpointCorrupt("inserted marker is unavailable".to_owned())
1321 })?;
1322 *marker = 1;
1323 }
1324 }
1325 Ok(state)
1326}
1327
1328pub fn validate_graph_build_checkpoint(bytes: &[u8]) -> Result<(), GraphBuildError> {
1332 let minimum = CHECKPOINT_HEADER_BYTES
1333 .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1334 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1335 if bytes.len() < minimum {
1336 return Err(GraphBuildError::CheckpointCorrupt(format!(
1337 "truncated: need at least {minimum} bytes, got {}",
1338 bytes.len()
1339 )));
1340 }
1341 if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1342 return Err(GraphBuildError::CheckpointCorrupt(
1343 "magic does not match ZEVAMCP1".to_owned(),
1344 ));
1345 }
1346 let checksum_start = bytes
1347 .len()
1348 .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1349 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1350 let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1351 let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1352 GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1353 })?;
1354 let actual_checksum = xxh3_64(checksummed);
1355 if stored_checksum != actual_checksum {
1356 return Err(GraphBuildError::CheckpointCorrupt(format!(
1357 "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1358 )));
1359 }
1360 let version = read_checkpoint_u16(bytes, 8, "version")?;
1361 if version != CHECKPOINT_VERSION {
1362 return Err(GraphBuildError::CheckpointCorrupt(format!(
1363 "version {version}, expected {CHECKPOINT_VERSION}"
1364 )));
1365 }
1366 let _ = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1367 let _ = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1368 let r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1369 let r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1370 let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1371 let l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1372 let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1373 let checkpoint_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1374 let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1375 let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1376 let next_index = read_checkpoint_u32(bytes, 32, "next position")?;
1377 let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved position bytes")?;
1378 if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1379 return Err(GraphBuildError::CheckpointCorrupt(
1380 "reserved header bytes are nonzero".to_owned(),
1381 ));
1382 }
1383 if r_target == 0 || r_target > r_max || l_build < u16::from(r_target) {
1384 return Err(GraphBuildError::CheckpointCorrupt(
1385 "stored degree and construction width are invalid".to_owned(),
1386 ));
1387 }
1388 if checkpoint_batch == 0 || node_count == 0 || dimensions == 0 || next_index > node_count {
1389 return Err(GraphBuildError::CheckpointCorrupt(
1390 "stored batch, shape, or next position is invalid".to_owned(),
1391 ));
1392 }
1393 let alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1394 let alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1395 if !alpha_build.is_finite()
1396 || !alpha_refine.is_finite()
1397 || alpha_build < 1.0
1398 || alpha_refine < alpha_build
1399 {
1400 return Err(GraphBuildError::CheckpointCorrupt(
1401 "stored alpha values are invalid".to_owned(),
1402 ));
1403 }
1404 let payload_bytes = usize::try_from(read_checkpoint_u64(bytes, 72, "payload bytes")?)
1405 .map_err(|_| GraphBuildError::CheckpointCorrupt("payload exceeds usize".to_owned()))?;
1406 let node_count_usize = usize::try_from(node_count)
1407 .map_err(|_| GraphBuildError::CheckpointCorrupt("node count exceeds usize".to_owned()))?;
1408 let slot_count = node_count_usize
1409 .checked_mul(usize::from(r_max))
1410 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1411 let expected_payload = slot_count
1412 .checked_mul(std::mem::size_of::<u32>())
1413 .and_then(|slot_bytes| slot_bytes.checked_add(node_count_usize))
1414 .ok_or_else(|| {
1415 GraphBuildError::CheckpointCorrupt("payload geometry overflow".to_owned())
1416 })?;
1417 if payload_bytes != expected_payload {
1418 return Err(GraphBuildError::CheckpointCorrupt(format!(
1419 "payload bytes {payload_bytes}, expected {expected_payload}"
1420 )));
1421 }
1422 let expected_length = CHECKPOINT_HEADER_BYTES
1423 .checked_add(payload_bytes)
1424 .and_then(|length| length.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1425 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("file length overflow".to_owned()))?;
1426 if bytes.len() != expected_length {
1427 return Err(GraphBuildError::CheckpointCorrupt(format!(
1428 "file length {}, expected {expected_length}",
1429 bytes.len()
1430 )));
1431 }
1432 let degrees_start = CHECKPOINT_HEADER_BYTES;
1433 let slots_start = degrees_start
1434 .checked_add(node_count_usize)
1435 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot offset overflow".to_owned()))?;
1436 for owner in 0..node_count_usize {
1437 let degree = usize::from(read_checkpoint_u8(
1438 bytes,
1439 degrees_start + owner,
1440 "node degree",
1441 )?);
1442 if degree > usize::from(r_max) {
1443 return Err(GraphBuildError::CheckpointCorrupt(format!(
1444 "node {owner} degree {degree} exceeds {r_max}"
1445 )));
1446 }
1447 let row_start = owner
1448 .checked_mul(usize::from(r_max))
1449 .and_then(|slot| slot.checked_mul(std::mem::size_of::<u32>()))
1450 .and_then(|offset| slots_start.checked_add(offset))
1451 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("row offset overflow".to_owned()))?;
1452 let mut active = Vec::with_capacity(degree);
1453 for slot in 0..usize::from(r_max) {
1454 let offset = slot
1455 .checked_mul(std::mem::size_of::<u32>())
1456 .and_then(|offset| row_start.checked_add(offset))
1457 .ok_or_else(|| {
1458 GraphBuildError::CheckpointCorrupt("neighbor offset overflow".to_owned())
1459 })?;
1460 let neighbor = read_checkpoint_u32(bytes, offset, "neighbor")?;
1461 if slot < degree {
1462 if neighbor >= node_count
1463 || neighbor as usize == owner
1464 || active.contains(&neighbor)
1465 {
1466 return Err(GraphBuildError::CheckpointCorrupt(format!(
1467 "node {owner} has invalid active neighbor {neighbor}"
1468 )));
1469 }
1470 active.push(neighbor);
1471 } else if neighbor != u32::MAX {
1472 return Err(GraphBuildError::CheckpointCorrupt(format!(
1473 "node {owner} inactive slot {slot} is not the sentinel"
1474 )));
1475 }
1476 }
1477 }
1478 Ok(())
1479}
1480
1481fn validate_checkpoint_adjacency(adjacency: &Adjacency) -> Result<(), GraphBuildError> {
1482 for node_id in 0..adjacency.node_count {
1483 let neighbors = adjacency.neighbors(node_id)?;
1484 if neighbors.len() > usize::from(adjacency.r_max) {
1485 return Err(GraphBuildError::CheckpointCorrupt(format!(
1486 "node {node_id} exceeds maximum degree"
1487 )));
1488 }
1489 let mut seen = Vec::with_capacity(neighbors.len());
1490 for neighbor in neighbors {
1491 if *neighbor >= adjacency.node_count || *neighbor == node_id || seen.contains(neighbor)
1492 {
1493 return Err(GraphBuildError::CheckpointCorrupt(format!(
1494 "node {node_id} has an out-of-range, self, or duplicate neighbor {neighbor}"
1495 )));
1496 }
1497 seen.push(*neighbor);
1498 }
1499 let range = adjacency.row_range(node_id)?;
1500 let inactive_start = range.start.checked_add(neighbors.len()).ok_or_else(|| {
1501 GraphBuildError::CheckpointCorrupt("inactive offset overflow".to_owned())
1502 })?;
1503 let inactive = adjacency
1504 .slots
1505 .get(inactive_start..range.end)
1506 .ok_or_else(|| {
1507 GraphBuildError::CheckpointCorrupt("inactive slots are unavailable".to_owned())
1508 })?;
1509 if inactive.iter().any(|neighbor| *neighbor != u32::MAX) {
1510 return Err(GraphBuildError::CheckpointCorrupt(format!(
1511 "node {node_id} has non-sentinel inactive slots"
1512 )));
1513 }
1514 }
1515 Ok(())
1516}
1517
1518fn phase_code(phase: BuildPhase) -> u8 {
1519 match phase {
1520 BuildPhase::Build => 1,
1521 BuildPhase::Refine => 2,
1522 BuildPhase::Complete => 3,
1523 }
1524}
1525
1526fn decode_phase(value: u8) -> Result<BuildPhase, GraphBuildError> {
1527 match value {
1528 1 => Ok(BuildPhase::Build),
1529 2 => Ok(BuildPhase::Refine),
1530 3 => Ok(BuildPhase::Complete),
1531 _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1532 "unknown phase {value}"
1533 ))),
1534 }
1535}
1536
1537fn pass_code(passes: GraphBuildPasses) -> u8 {
1538 match passes {
1539 GraphBuildPasses::One => 1,
1540 GraphBuildPasses::Two => 2,
1541 }
1542}
1543
1544fn decode_passes(value: u8) -> Result<GraphBuildPasses, GraphBuildError> {
1545 match value {
1546 1 => Ok(GraphBuildPasses::One),
1547 2 => Ok(GraphBuildPasses::Two),
1548 _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1549 "unknown pass count {value}"
1550 ))),
1551 }
1552}
1553
1554fn read_checkpoint_u8(bytes: &[u8], offset: usize, field: &str) -> Result<u8, GraphBuildError> {
1555 bytes.get(offset).copied().ok_or_else(|| {
1556 GraphBuildError::CheckpointCorrupt(format!("{field} is truncated at byte {offset}"))
1557 })
1558}
1559
1560fn read_checkpoint_u16(bytes: &[u8], offset: usize, field: &str) -> Result<u16, GraphBuildError> {
1561 let end = offset
1562 .checked_add(2)
1563 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1564 let raw: [u8; 2] = bytes
1565 .get(offset..end)
1566 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1567 .try_into()
1568 .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1569 Ok(u16::from_le_bytes(raw))
1570}
1571
1572fn read_checkpoint_u32(bytes: &[u8], offset: usize, field: &str) -> Result<u32, GraphBuildError> {
1573 let end = offset
1574 .checked_add(4)
1575 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1576 let raw: [u8; 4] = bytes
1577 .get(offset..end)
1578 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1579 .try_into()
1580 .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1581 Ok(u32::from_le_bytes(raw))
1582}
1583
1584fn read_checkpoint_u64(bytes: &[u8], offset: usize, field: &str) -> Result<u64, GraphBuildError> {
1585 let end = offset
1586 .checked_add(8)
1587 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1588 let raw: [u8; 8] = bytes
1589 .get(offset..end)
1590 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1591 .try_into()
1592 .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1593 Ok(u64::from_le_bytes(raw))
1594}
1595
1596#[derive(Clone, Copy, Debug)]
1597struct ScoredNode {
1598 node_id: u32,
1599 distance: f64,
1600}
1601
1602#[allow(clippy::too_many_arguments)]
1603fn process_node(
1604 vectors: &SegmentVectors<'_>,
1605 params: GraphParams,
1606 seed: u64,
1607 alpha: f32,
1608 node_id: u32,
1609 entries: &[u32],
1610 adjacency: &mut Adjacency,
1611 inserted: &mut [u8],
1612 visited: &mut [u32],
1613 visit_epoch: &mut u32,
1614) -> Result<(), GraphBuildError> {
1615 vectors.validate_node(node_id)?;
1616 let candidates = search_candidates(
1617 vectors,
1618 adjacency,
1619 inserted,
1620 node_id,
1621 entries,
1622 usize::from(params.l_build()),
1623 seed,
1624 visited,
1625 visit_epoch,
1626 )?;
1627 let mut candidate_ids = candidates
1628 .into_iter()
1629 .map(|candidate| candidate.node_id)
1630 .collect::<Vec<_>>();
1631 candidate_ids.extend_from_slice(adjacency.neighbors(node_id)?);
1632 let pruned = robust_prune(
1633 vectors,
1634 node_id,
1635 &candidate_ids,
1636 alpha,
1637 usize::from(params.r_target()),
1638 )?;
1639 adjacency.set_neighbors(node_id, &pruned)?;
1640 let node = vectors.validate_node(node_id)?;
1641 let inserted_node = inserted.get_mut(node).ok_or_else(|| {
1642 GraphBuildError::Geometry(format!("inserted marker {node_id} is unavailable"))
1643 })?;
1644 *inserted_node = 1;
1645 add_reciprocal_edges(vectors, adjacency, node_id, &pruned, params, alpha)?;
1646 Ok(())
1647}
1648
1649#[allow(clippy::too_many_arguments)]
1650fn search_candidates(
1651 vectors: &SegmentVectors<'_>,
1652 adjacency: &Adjacency,
1653 inserted: &[u8],
1654 query_id: u32,
1655 entries: &[u32],
1656 width: usize,
1657 seed: u64,
1658 visited: &mut [u32],
1659 visit_epoch: &mut u32,
1660) -> Result<Vec<ScoredNode>, GraphBuildError> {
1661 let query = vectors.f32_row(query_id)?;
1662 let query_norm_squared = query
1663 .iter()
1664 .map(|value| f64::from(*value) * f64::from(*value))
1665 .sum::<f64>();
1666 let prepared = prepare_bit4_query(query, seed ^ u64::from(query_id))?;
1667 *visit_epoch = visit_epoch.wrapping_add(1);
1668 if *visit_epoch == 0 {
1669 visited.fill(0);
1670 *visit_epoch = 1;
1671 }
1672 let epoch = *visit_epoch;
1673 let mut result = Vec::with_capacity(width);
1674 let mut frontier = Vec::with_capacity(width.saturating_mul(2));
1675 for &entry in entries {
1676 let entry_index = vectors.validate_node(entry)?;
1677 if entry == query_id
1678 || inserted.get(entry_index).copied() != Some(1)
1679 || visited.get(entry_index).copied() == Some(epoch)
1680 {
1681 continue;
1682 }
1683 if let Some(marker) = visited.get_mut(entry_index) {
1684 *marker = epoch;
1685 }
1686 let scored = score_node(vectors, &prepared, query_norm_squared, entry)?;
1687 insert_result(&mut result, scored, width);
1688 frontier.push(scored);
1689 }
1690 if frontier.is_empty()
1691 && let Some(start) = inserted.iter().position(|present| *present == 1)
1692 {
1693 let start = u32::try_from(start)
1694 .map_err(|_| GraphBuildError::Geometry("inserted start exceeds u32".to_owned()))?;
1695 let scored = score_node(vectors, &prepared, query_norm_squared, start)?;
1696 if let Some(marker) = visited.get_mut(start as usize) {
1697 *marker = epoch;
1698 }
1699 insert_result(&mut result, scored, width);
1700 frontier.push(scored);
1701 }
1702 while !frontier.is_empty() {
1703 frontier.sort_unstable_by(scored_worst_first);
1704 let Some(candidate) = frontier.pop() else {
1705 break;
1706 };
1707 let worst = result.last().map(|node| node.distance);
1708 if result.len() >= width && worst.is_some_and(|distance| candidate.distance > distance) {
1709 break;
1710 }
1711 for &neighbor in adjacency.neighbors(candidate.node_id)? {
1712 let neighbor_index = vectors.validate_node(neighbor)?;
1713 if neighbor == query_id
1714 || inserted.get(neighbor_index).copied() != Some(1)
1715 || visited.get(neighbor_index).copied() == Some(epoch)
1716 {
1717 continue;
1718 }
1719 if let Some(marker) = visited.get_mut(neighbor_index) {
1720 *marker = epoch;
1721 }
1722 let scored = score_node(vectors, &prepared, query_norm_squared, neighbor)?;
1723 let qualifies = result.len() < width
1724 || result
1725 .last()
1726 .is_some_and(|current_worst| scored.distance < current_worst.distance);
1727 if qualifies {
1728 insert_result(&mut result, scored, width);
1729 frontier.push(scored);
1730 }
1731 }
1732 }
1733 Ok(result)
1734}
1735
1736fn score_node(
1737 vectors: &SegmentVectors<'_>,
1738 query: &crate::quant::Bit4Query,
1739 query_norm_squared: f64,
1740 node_id: u32,
1741) -> Result<ScoredNode, GraphBuildError> {
1742 vectors.validate_node(node_id)?;
1743 let factors = vectors.factor(node_id)?;
1744 let dot = f64::from(est_dot_bit4(query, vectors.code_row(node_id)?, factors)?);
1745 let norm = factors.norm();
1746 Ok(ScoredNode {
1747 node_id,
1748 distance: (query_norm_squared + norm * norm - 2.0 * dot).max(0.0),
1749 })
1750}
1751
1752fn insert_result(result: &mut Vec<ScoredNode>, node: ScoredNode, width: usize) {
1753 result.push(node);
1754 result.sort_unstable_by(scored_best_first);
1755 if result.len() > width {
1756 let _ = result.pop();
1757 }
1758}
1759
1760fn scored_best_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
1761 left.distance
1762 .total_cmp(&right.distance)
1763 .then_with(|| left.node_id.cmp(&right.node_id))
1764}
1765
1766fn scored_worst_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
1767 scored_best_first(right, left)
1768}
1769
1770fn robust_prune(
1771 vectors: &SegmentVectors<'_>,
1772 owner: u32,
1773 candidates: &[u32],
1774 alpha: f32,
1775 target: usize,
1776) -> Result<Vec<u32>, GraphBuildError> {
1777 robust_prune_rows(
1778 vectors.rescore,
1779 vectors.dimensions,
1780 vectors.node_count,
1781 owner,
1782 candidates,
1783 alpha,
1784 target,
1785 )
1786}
1787
1788pub(crate) fn robust_prune_rows(
1790 rescore: &[f32],
1791 dimensions: usize,
1792 node_count: u32,
1793 owner: u32,
1794 candidates: &[u32],
1795 alpha: f32,
1796 target: usize,
1797) -> Result<Vec<u32>, GraphBuildError> {
1798 validate_exact_rows(rescore, dimensions, node_count)?;
1799 validate_exact_node(owner, node_count)?;
1800 let mut remaining = Vec::with_capacity(candidates.len());
1801 for &candidate in candidates {
1802 validate_exact_node(candidate, node_count)?;
1803 if candidate != owner && !remaining.contains(&candidate) {
1804 remaining.push(candidate);
1805 }
1806 }
1807 let mut owner_distances = remaining
1808 .iter()
1809 .map(|candidate| {
1810 exact_row_distance(rescore, dimensions, owner, *candidate).map(|distance| ScoredNode {
1811 node_id: *candidate,
1812 distance,
1813 })
1814 })
1815 .collect::<Result<Vec<_>, _>>()?;
1816 owner_distances.sort_unstable_by(scored_best_first);
1817 let mut selected = Vec::with_capacity(target);
1818 let alpha_squared = f64::from(alpha) * f64::from(alpha);
1819 while selected.len() < target {
1820 let Some(best) = owner_distances.first().copied() else {
1821 break;
1822 };
1823 selected.push(best.node_id);
1824 owner_distances.retain(|candidate| {
1825 if candidate.node_id == best.node_id {
1826 return false;
1827 }
1828 exact_row_distance(rescore, dimensions, best.node_id, candidate.node_id)
1829 .map_or(true, |between| alpha_squared * between > candidate.distance)
1830 });
1831 }
1832 Ok(selected)
1833}
1834
1835fn validate_exact_rows(
1836 rescore: &[f32],
1837 dimensions: usize,
1838 node_count: u32,
1839) -> Result<(), GraphBuildError> {
1840 if dimensions == 0 {
1841 return Err(GraphBuildError::Geometry(
1842 "alpha prune requires non-zero dimensions".to_owned(),
1843 ));
1844 }
1845 let expected = (node_count as usize)
1846 .checked_mul(dimensions)
1847 .ok_or_else(|| GraphBuildError::Geometry("exact row length overflow".to_owned()))?;
1848 if rescore.len() != expected {
1849 return Err(GraphBuildError::Geometry(format!(
1850 "exact rows contain {} values, expected {expected}",
1851 rescore.len()
1852 )));
1853 }
1854 Ok(())
1855}
1856
1857fn validate_exact_node(node_id: u32, node_count: u32) -> Result<(), GraphBuildError> {
1858 if node_id >= node_count {
1859 return Err(GraphBuildError::NodeIdOutOfRange {
1860 node_id,
1861 node_count,
1862 });
1863 }
1864 Ok(())
1865}
1866
1867fn exact_row_distance(
1868 rescore: &[f32],
1869 dimensions: usize,
1870 left: u32,
1871 right: u32,
1872) -> Result<f64, GraphBuildError> {
1873 let row = |node_id: u32| {
1874 let start = (node_id as usize)
1875 .checked_mul(dimensions)
1876 .ok_or_else(|| GraphBuildError::Geometry("exact row offset overflow".to_owned()))?;
1877 let end = start
1878 .checked_add(dimensions)
1879 .ok_or_else(|| GraphBuildError::Geometry("exact row end overflow".to_owned()))?;
1880 rescore
1881 .get(start..end)
1882 .ok_or_else(|| GraphBuildError::Geometry(format!("exact row {node_id} is unavailable")))
1883 };
1884 Ok(row(left)?
1885 .iter()
1886 .zip(row(right)?)
1887 .map(|(left, right)| {
1888 let difference = f64::from(*left) - f64::from(*right);
1889 difference * difference
1890 })
1891 .sum())
1892}
1893
1894fn add_reciprocal_edges(
1895 vectors: &SegmentVectors<'_>,
1896 adjacency: &mut Adjacency,
1897 owner: u32,
1898 neighbors: &[u32],
1899 params: GraphParams,
1900 alpha: f32,
1901) -> Result<(), GraphBuildError> {
1902 for &neighbor in neighbors {
1903 vectors.validate_node(neighbor)?;
1904 let existing = adjacency.neighbors(neighbor)?.to_vec();
1905 if existing.contains(&owner) {
1906 continue;
1907 }
1908 let updated = if existing.len() < usize::from(params.r_max()) {
1909 let mut updated = existing;
1910 updated.push(owner);
1911 updated
1912 } else {
1913 let mut candidates = existing;
1914 candidates.push(owner);
1915 robust_prune(
1916 vectors,
1917 neighbor,
1918 &candidates,
1919 alpha,
1920 usize::from(params.r_target()),
1921 )?
1922 };
1923 adjacency.set_neighbors(neighbor, &updated)?;
1924 }
1925 Ok(())
1926}
1927
1928fn refined_entry_points(
1929 vectors: &SegmentVectors<'_>,
1930 shuffled: &[u32],
1931) -> Result<Vec<u32>, GraphBuildError> {
1932 let sample = shuffled
1933 .get(..shuffled.len().min(MEDOID_SAMPLE_ROWS))
1934 .ok_or_else(|| GraphBuildError::Geometry("medoid sample is unavailable".to_owned()))?;
1935 let mut medoid_scores = sample
1936 .iter()
1937 .map(|candidate| {
1938 sample
1939 .iter()
1940 .try_fold(0.0_f64, |sum, other| {
1941 vectors
1942 .exact_distance(*candidate, *other)
1943 .map(|distance| sum + distance)
1944 })
1945 .map(|distance| ScoredNode {
1946 node_id: *candidate,
1947 distance,
1948 })
1949 })
1950 .collect::<Result<Vec<_>, _>>()?;
1951 medoid_scores.sort_unstable_by(scored_best_first);
1952 let medoid = medoid_scores
1953 .first()
1954 .map(|node| node.node_id)
1955 .ok_or_else(|| GraphBuildError::Geometry("medoid sample contains no rows".to_owned()))?;
1956 let mut entries = vec![medoid];
1957 while entries.len() < ENTRY_POINT_COUNT && entries.len() < sample.len() {
1958 let mut farthest = None::<ScoredNode>;
1959 for &candidate in sample {
1960 if entries.contains(&candidate) {
1961 continue;
1962 }
1963 let nearest = entries.iter().try_fold(f64::INFINITY, |nearest, entry| {
1964 vectors
1965 .exact_distance(candidate, *entry)
1966 .map(|distance| nearest.min(distance))
1967 })?;
1968 let scored = ScoredNode {
1969 node_id: candidate,
1970 distance: nearest,
1971 };
1972 if farthest.is_none_or(|current| {
1973 scored.distance > current.distance
1974 || (scored.distance == current.distance && scored.node_id < current.node_id)
1975 }) {
1976 farthest = Some(scored);
1977 }
1978 }
1979 let Some(next) = farthest else {
1980 break;
1981 };
1982 entries.push(next.node_id);
1983 }
1984 Ok(entries)
1985}
1986
1987fn move_entries_to_front(order: &mut Vec<u32>, entries: &[u32]) {
1988 order.retain(|node_id| !entries.contains(node_id));
1989 for &entry in entries.iter().rev() {
1990 order.insert(0, entry);
1991 }
1992}
1993
1994fn encode_artifact(
1995 vectors: &SegmentVectors<'_>,
1996 params: GraphParams,
1997 entries: &[u32],
1998 adjacency: &Adjacency,
1999 mut memory: Option<AccountedCounter>,
2000) -> Result<GraphBuildArtifact, GraphBuildError> {
2001 let dimensions = u32::try_from(vectors.dimensions)
2002 .map_err(|_| GraphBuildError::Geometry("dimensions exceed u32".to_owned()))?;
2003 let padded_dimensions = dimensions
2004 .checked_add(127)
2005 .map(|value| value / 128 * 128)
2006 .ok_or_else(|| GraphBuildError::Geometry("padded dimensions overflow".to_owned()))?;
2007 let layout = GraphNodeLayout::new(dimensions, padded_dimensions, params.r_max())?;
2008 let padded_row_bytes = layout.code_bytes();
2009 let padded_length = (vectors.node_count as usize)
2010 .checked_mul(padded_row_bytes)
2011 .ok_or_else(|| GraphBuildError::Geometry("padded code bytes overflow".to_owned()))?;
2012 let encoded_length = (vectors.node_count as usize)
2013 .checked_mul(layout.stride() as usize)
2014 .and_then(|bytes| bytes.checked_add(NODE_BLOCK_TRAILER_LEN))
2015 .ok_or_else(|| GraphBuildError::Geometry("encoded graph bytes overflow".to_owned()))?;
2016 let node_inputs = (vectors.node_count as usize)
2017 .checked_mul(std::mem::size_of::<GraphNodeBlockInput<'_>>())
2018 .ok_or_else(|| GraphBuildError::Geometry("node input bytes overflow".to_owned()))?;
2019 let entry_bytes = entries
2020 .len()
2021 .checked_mul(std::mem::size_of::<u32>())
2022 .ok_or_else(|| GraphBuildError::Geometry("entry bytes overflow".to_owned()))?;
2023 if let Some(charge) = memory.as_mut() {
2024 let existing = usize::try_from(charge.bytes())
2025 .map_err(|_| GraphBuildError::Geometry("accounted bytes exceed usize".to_owned()))?;
2026 let peak = existing
2027 .checked_add(padded_length)
2028 .and_then(|bytes| bytes.checked_add(node_inputs))
2029 .and_then(|bytes| bytes.checked_add(encoded_length))
2030 .and_then(|bytes| bytes.checked_add(entry_bytes))
2031 .ok_or_else(|| GraphBuildError::Geometry("encode arena overflow".to_owned()))?;
2032 charge.set(peak)?;
2033 }
2034 let mut padded_codes = vec![0_u8; padded_length];
2035 for node_id in 0..vectors.node_count {
2036 let source = vectors.code_row(node_id)?;
2037 let start = (node_id as usize)
2038 .checked_mul(padded_row_bytes)
2039 .ok_or_else(|| GraphBuildError::Geometry("padded code offset overflow".to_owned()))?;
2040 let end = start
2041 .checked_add(source.len())
2042 .ok_or_else(|| GraphBuildError::Geometry("padded code end overflow".to_owned()))?;
2043 let destination = padded_codes.get_mut(start..end).ok_or_else(|| {
2044 GraphBuildError::Geometry(format!("padded code row {node_id} is unavailable"))
2045 })?;
2046 destination.copy_from_slice(source);
2047 }
2048 let mut nodes = Vec::with_capacity(vectors.node_count as usize);
2049 for node_id in 0..vectors.node_count {
2050 let start = (node_id as usize)
2051 .checked_mul(padded_row_bytes)
2052 .ok_or_else(|| GraphBuildError::Geometry("encoded code offset overflow".to_owned()))?;
2053 let end = start
2054 .checked_add(padded_row_bytes)
2055 .ok_or_else(|| GraphBuildError::Geometry("encoded code end overflow".to_owned()))?;
2056 let codes = padded_codes.get(start..end).ok_or_else(|| {
2057 GraphBuildError::Geometry(format!("encoded code row {node_id} is unavailable"))
2058 })?;
2059 nodes.push(GraphNodeBlockInput {
2060 codes,
2061 factors: vectors.factor(node_id)?,
2062 flags: u8::from(entries.contains(&node_id)),
2063 neighbors: adjacency.neighbors(node_id)?,
2064 });
2065 }
2066 let encoded = encode_node_blocks(GraphNodeBlockBuild {
2067 layout,
2068 nodes: &nodes,
2069 })?;
2070 let encoded_region = encoded.into_bytes();
2071 let entry_points = entries.to_vec();
2072 if let Some(charge) = memory.as_mut() {
2073 let retained = encoded_region
2074 .capacity()
2075 .checked_add(
2076 entry_points
2077 .capacity()
2078 .checked_mul(std::mem::size_of::<u32>())
2079 .ok_or_else(|| {
2080 GraphBuildError::Geometry("retained entry bytes overflow".to_owned())
2081 })?,
2082 )
2083 .ok_or_else(|| GraphBuildError::Geometry("retained graph bytes overflow".to_owned()))?;
2084 charge.set(retained)?;
2085 }
2086 Ok(GraphBuildArtifact {
2087 encoded_region,
2088 entry_points,
2089 work_rows_completed: 0,
2090 _memory: memory,
2091 })
2092}
2093
2094fn shuffle(values: &mut [u32], random: &mut SplitMix64) -> Result<(), GraphBuildError> {
2095 for upper in (1..values.len()).rev() {
2096 let modulus = u64::try_from(upper + 1)
2097 .map_err(|_| GraphBuildError::Geometry("shuffle width exceeds u64".to_owned()))?;
2098 let selected = usize::try_from(random.next_u64() % modulus)
2099 .map_err(|_| GraphBuildError::Geometry("shuffle index exceeds usize".to_owned()))?;
2100 values.swap(upper, selected);
2101 }
2102 Ok(())
2103}
2104
2105#[derive(Clone, Copy, Debug)]
2106struct SplitMix64 {
2107 state: u64,
2108}
2109
2110impl SplitMix64 {
2111 const fn new(seed: u64) -> Self {
2112 Self { state: seed }
2113 }
2114
2115 fn next_u64(&mut self) -> u64 {
2116 self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
2117 let mut value = self.state;
2118 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
2119 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
2120 value ^ (value >> 31)
2121 }
2122}
2123
2124#[cfg(test)]
2125#[allow(
2126 clippy::expect_used,
2127 clippy::indexing_slicing,
2128 clippy::panic,
2129 clippy::unwrap_used
2130)]
2131mod tests {
2132 use proptest::prelude::*;
2133 use proptest::test_runner::{Config, RngSeed, TestRunner};
2134 use rand::{Rng, RngCore};
2135 use xxhash_rust::xxh3::xxh3_64;
2136
2137 use crate::graph::block::decode_node_blocks;
2138 use crate::lifecycle::QueryCancellation;
2139 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
2140 use crate::lifecycle::{
2141 CancelToken, ManualMonotonicClock, OpenOptions, QueryControl, Store, StoreTestDependencies,
2142 };
2143 use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
2144 use crate::quant::{Bit4Factors, quantize_bit4};
2145 use crate::segment::SegmentId;
2146 use crate::segment::reader::SegmentReader;
2147 use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
2148 use crate::vfs::{CountingVfs, StdVfs, Vfs};
2149 use std::sync::Arc;
2150
2151 use super::{
2152 Adjacency, CheckpointedGraphBuild, GraphBuildError, GraphBuildPasses, GraphBuildSession,
2153 SegmentVectors, build_graph, build_graph_checkpointed, decode_checkpoint,
2154 validate_graph_build_checkpoint,
2155 };
2156 use crate::graph::GraphParams;
2157
2158 fn fixture_reader(directory: &std::path::Path, rows: usize) -> SegmentReader {
2159 let dimensions = 128_usize;
2160 let mut random = crate::test_support::seeded_rng(
2161 "graph::build::vamana_build_is_deterministic_under_seed::fixture",
2162 );
2163 let mut rescore = Vec::with_capacity(rows * dimensions);
2164 let mut codes = Vec::with_capacity(rows * dimensions.div_ceil(2));
2165 let mut factors = Vec::<Bit4Factors>::with_capacity(rows);
2166 for _ in 0..rows {
2167 let row = (0..dimensions)
2168 .map(|_| random.random_range(-2.0_f32..2.0_f32))
2169 .collect::<Vec<_>>();
2170 let start = codes.len();
2171 codes.resize(start + dimensions.div_ceil(2), 0);
2172 factors.push(quantize_bit4(&row, &mut codes[start..]).expect("fixture row quantizes"));
2173 rescore.extend(row);
2174 }
2175 let mut columns = ColumnStoreBuilder::new(Schema::new(Vec::new()).expect("empty schema"));
2176 for row in 0..rows {
2177 columns
2178 .push_row(row as i64, &[])
2179 .expect("timestamp-only fixture row");
2180 }
2181 let columns = columns.finish().expect("fixture columns");
2182 let alive = AliveSet::new(rows as u32);
2183 let id = SegmentId::new(19, [7; 10]);
2184 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2185 .expect("derived policy");
2186 write_segment(
2187 &StdVfs,
2188 directory,
2189 SegmentBuild {
2190 id,
2191 scheme: 4,
2192 dims: dimensions as u32,
2193 codes: &codes,
2194 factors: SegmentFactors::Bit4(&factors),
2195 rescore: &rescore,
2196 columns: &columns,
2197 alive: &alive,
2198 },
2199 policy,
2200 )
2201 .expect("sealed Bit4 fixture");
2202 SegmentReader::open(&StdVfs, &directory.join(id.file_name()), id).expect("fixture reader")
2203 }
2204
2205 #[test]
2206 fn graph_checkpoint_io_goes_through_the_store_vfs() {
2207 let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2208 let reader = fixture_reader(segment_directory.path(), 4);
2209 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2210 let checkpoint = store_directory.path().join("graph-build.checkpoint");
2211 let vfs = Arc::new(CountingVfs::new(StdVfs).with_read_path_filter(checkpoint.clone()));
2212 let store = Store::open_with_test_dependencies(
2213 store_directory.path(),
2214 OpenOptions::default(),
2215 StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2216 )
2217 .expect("checkpoint store");
2218 let lease = store.snapshot().expect("checkpoint snapshot");
2219 let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2220 let control = QueryControl::Cancel(CancelToken::new());
2221
2222 vfs.reset();
2223 let interrupted = build_graph_checkpointed(
2224 &store,
2225 &reader,
2226 CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control)
2227 .with_max_work_rows(1),
2228 &lease,
2229 );
2230 assert!(matches!(
2231 interrupted,
2232 Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2233 ));
2234 build_graph_checkpointed(
2235 &store,
2236 &reader,
2237 CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control),
2238 &lease,
2239 )
2240 .expect("resumed graph build");
2241
2242 assert!(vfs.read_calls() >= 1, "checkpoint read bypassed Vfs");
2243 assert_eq!(
2244 vfs.filtered_read_calls(),
2245 1,
2246 "graph-build.checkpoint read bypassed Vfs"
2247 );
2248 assert!(vfs.write_calls() >= 1, "checkpoint write bypassed Vfs");
2249 assert!(vfs.rename_calls() >= 1, "checkpoint rename bypassed Vfs");
2250 assert!(vfs.delete_calls() >= 1, "checkpoint delete bypassed Vfs");
2251 drop(lease);
2252 store.close().expect("close checkpoint store");
2253 }
2254
2255 #[test]
2256 fn graph_checkpoint_publish_syncs_the_temp_file_and_its_directory() {
2257 let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2258 let reader = fixture_reader(segment_directory.path(), 4);
2259 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2260 let checkpoint = store_directory.path().join("graph-build.checkpoint");
2261 let vfs = Arc::new(CountingVfs::new(StdVfs));
2262 let store = Store::open_with_test_dependencies(
2263 store_directory.path(),
2264 OpenOptions::default().with_durability(DurabilityMode::Durable, CommitTier::Durable),
2265 StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2266 )
2267 .expect("durable checkpoint store");
2268 let lease = store.snapshot().expect("checkpoint snapshot");
2269 let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2270 let control = QueryControl::Cancel(CancelToken::new());
2271
2272 vfs.reset();
2273 let interrupted = build_graph_checkpointed(
2274 &store,
2275 &reader,
2276 CheckpointedGraphBuild::new(params, 0x03, GraphBuildPasses::One, &checkpoint, &control)
2277 .with_max_work_rows(1),
2278 &lease,
2279 );
2280 assert!(matches!(
2281 interrupted,
2282 Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2283 ));
2284 assert!(
2285 vfs.full_sync_calls() >= 2,
2286 "checkpoint publish skipped syncs"
2287 );
2288 assert!(vfs.rename_calls() >= 1, "checkpoint publish skipped rename");
2289 }
2290
2291 fn checkpoint_with_mutation(
2292 mut bytes: Vec<u8>,
2293 mutation: impl FnOnce(&mut Vec<u8>),
2294 ) -> Vec<u8> {
2295 mutation(&mut bytes);
2296 let checksum_start = bytes.len() - super::CHECKPOINT_CHECKSUM_BYTES;
2297 let checksum = xxh3_64(&bytes[..checksum_start]);
2298 bytes[checksum_start..].copy_from_slice(&checksum.to_le_bytes());
2299 bytes
2300 }
2301
2302 #[test]
2303 fn graph_build_errors_preserve_actionable_values_and_sources() {
2304 let io_error = GraphBuildError::CheckpointIo {
2305 path: std::path::PathBuf::from("blocked.checkpoint"),
2306 source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "blocked"),
2307 };
2308 assert!(io_error.to_string().contains("blocked.checkpoint"));
2309 assert!(std::error::Error::source(&io_error).is_some());
2310
2311 let errors = [
2312 GraphBuildError::CheckpointCorrupt("bad header".to_owned()),
2313 GraphBuildError::BudgetExhausted { rows_completed: 16 },
2314 GraphBuildError::Cancelled { partial: false },
2315 GraphBuildError::Timeout { partial: false },
2316 GraphBuildError::ReadCancelled { partial: false },
2317 GraphBuildError::Geometry("bad shape".to_owned()),
2318 GraphBuildError::NodeIdOutOfRange {
2319 node_id: 9,
2320 node_count: 3,
2321 },
2322 ];
2323 for error in errors {
2324 assert!(!error.to_string().is_empty());
2325 assert!(std::error::Error::source(&error).is_none());
2326 }
2327
2328 let sourced_errors = [
2329 GraphBuildError::from(crate::segment::SegmentError::Geometry(
2330 "bad segment".to_owned(),
2331 )),
2332 GraphBuildError::from(crate::quant::QuantError::EmptyVector),
2333 GraphBuildError::from(crate::graph::block::GraphNodeError::InvalidHeader(
2334 "bad node header".to_owned(),
2335 )),
2336 GraphBuildError::from(crate::lifecycle::StoreError::NotDirectory {
2337 path: std::path::PathBuf::from("not-a-store"),
2338 }),
2339 ];
2340 for error in sourced_errors {
2341 assert!(!error.to_string().is_empty());
2342 assert!(std::error::Error::source(&error).is_some());
2343 }
2344 }
2345
2346 #[test]
2347 fn built_graph_publishes_through_the_frozen_m2_writer() {
2348 let directory = tempfile::tempdir().expect("publication fixture directory");
2349 let reader = fixture_reader(directory.path(), 32);
2350 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 32).expect("publication params");
2351 let artifact = build_graph(
2352 &reader,
2353 params,
2354 0x0001_9000_3a71_fac7,
2355 GraphBuildPasses::Two,
2356 )
2357 .expect("publication graph");
2358 let output_id = SegmentId::new(19, [8; 10]);
2359 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2360 .expect("publication policy");
2361 let meta = artifact
2362 .write_segment_with_graph(&StdVfs, directory.path(), &reader, output_id, policy)
2363 .expect("published graph segment");
2364 assert_eq!(meta.id, output_id);
2365 let published = SegmentReader::open(
2366 &StdVfs,
2367 &directory.path().join(output_id.file_name()),
2368 output_id,
2369 )
2370 .expect("published reader");
2371 let graph = published
2372 .graph_node_blocks()
2373 .expect("published graph region");
2374 assert_eq!(graph.node_count(), 32);
2375 assert_eq!(graph.layout().max_degree(), 12);
2376 }
2377
2378 #[test]
2379 fn checked_adjacency_rejects_invalid_node_and_degree_inputs() {
2380 let directory = tempfile::tempdir().expect("checked vector directory");
2381 let reader = fixture_reader(directory.path(), 3);
2382 let vectors = SegmentVectors::new(&reader).expect("checked vectors");
2383 assert!(matches!(
2384 vectors.validate_node(3),
2385 Err(GraphBuildError::NodeIdOutOfRange { .. })
2386 ));
2387
2388 let mut adjacency = Adjacency::new(3, 2).expect("small adjacency");
2389 assert!(matches!(
2390 adjacency.neighbors(3),
2391 Err(GraphBuildError::NodeIdOutOfRange { .. })
2392 ));
2393 assert!(matches!(
2394 adjacency.set_neighbors(0, &[1, 2, 1]),
2395 Err(GraphBuildError::Geometry(_))
2396 ));
2397 assert!(matches!(
2398 adjacency.set_neighbors(0, &[3]),
2399 Err(GraphBuildError::NodeIdOutOfRange { .. })
2400 ));
2401 adjacency
2402 .set_neighbors(0, &[1, 2])
2403 .expect("valid adjacency row");
2404 assert_eq!(adjacency.neighbors(0).expect("active row"), &[1, 2]);
2405
2406 let mut self_edge = Adjacency::new(3, 2).expect("self-edge adjacency");
2407 self_edge.degrees[0] = 1;
2408 self_edge.slots[0] = 0;
2409 assert!(matches!(
2410 super::validate_checkpoint_adjacency(&self_edge),
2411 Err(GraphBuildError::CheckpointCorrupt(_))
2412 ));
2413
2414 let mut inactive_value = Adjacency::new(3, 2).expect("inactive-slot adjacency");
2415 inactive_value.slots[0] = 1;
2416 assert!(matches!(
2417 super::validate_checkpoint_adjacency(&inactive_value),
2418 Err(GraphBuildError::CheckpointCorrupt(_))
2419 ));
2420 assert!(matches!(
2421 super::decode_phase(3),
2422 Ok(super::BuildPhase::Complete)
2423 ));
2424 assert!(matches!(
2425 super::read_checkpoint_u8(&[], 0, "truncated"),
2426 Err(GraphBuildError::CheckpointCorrupt(_))
2427 ));
2428 }
2429
2430 #[test]
2431 fn checkpoint_parser_rejects_semantic_corruption_and_identity_drift() {
2432 let directory = tempfile::tempdir().expect("checkpoint parser directory");
2433 let reader = fixture_reader(directory.path(), 16);
2434 let checkpoint = directory.path().join("semantic.checkpoint");
2435 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2436 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2437 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2438 let lease = store.snapshot().expect("checkpoint lease");
2439 let control = QueryControl::Cancel(CancelToken::new());
2440 let cancellation = QueryCancellation::new(&control, &lease);
2441 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2442 .expect("derived policy");
2443 let mut session = GraphBuildSession::new(
2444 &StdVfs,
2445 &reader,
2446 params,
2447 0x0001_9000_3c0d_ec01,
2448 GraphBuildPasses::Two,
2449 &checkpoint,
2450 policy,
2451 None,
2452 )
2453 .expect("checkpoint session");
2454 assert!(
2455 !session
2456 .advance_batch(&cancellation)
2457 .expect("checkpoint batch")
2458 );
2459 let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2460 validate_graph_build_checkpoint(&valid).expect("valid checkpoint parses");
2461
2462 let mut bad_checksum = valid.clone();
2463 bad_checksum[40] ^= 1;
2464 let corruptions = [
2465 bad_checksum,
2466 checkpoint_with_mutation(valid.clone(), |bytes| {
2467 bytes[8..10].copy_from_slice(&2_u16.to_le_bytes());
2468 }),
2469 checkpoint_with_mutation(valid.clone(), |bytes| bytes[10] = 9),
2470 checkpoint_with_mutation(valid.clone(), |bytes| bytes[11] = 9),
2471 checkpoint_with_mutation(valid.clone(), |bytes| bytes[14] = 1),
2472 checkpoint_with_mutation(valid.clone(), |bytes| bytes[12] = 0),
2473 checkpoint_with_mutation(valid.clone(), |bytes| bytes[20..24].fill(0)),
2474 checkpoint_with_mutation(valid.clone(), |bytes| {
2475 bytes[32..36].copy_from_slice(&17_u32.to_le_bytes());
2476 }),
2477 checkpoint_with_mutation(valid.clone(), |bytes| {
2478 bytes[48..52].copy_from_slice(&f32::NAN.to_bits().to_le_bytes());
2479 }),
2480 checkpoint_with_mutation(valid.clone(), |bytes| bytes[72..80].fill(0)),
2481 checkpoint_with_mutation(valid.clone(), |bytes| {
2482 bytes[80] = 1;
2483 bytes[96..100].copy_from_slice(&0_u32.to_le_bytes());
2484 }),
2485 checkpoint_with_mutation(valid.clone(), |bytes| {
2486 bytes[80] = 0;
2487 bytes[96..100].copy_from_slice(&1_u32.to_le_bytes());
2488 }),
2489 ];
2490 for corrupt in corruptions {
2491 assert!(matches!(
2492 validate_graph_build_checkpoint(&corrupt),
2493 Err(GraphBuildError::CheckpointCorrupt(_))
2494 ));
2495 }
2496
2497 let vectors = SegmentVectors::new(&reader).expect("checkpoint vectors");
2498 assert!(matches!(
2499 decode_checkpoint(
2500 &valid,
2501 &vectors,
2502 params,
2503 0x0001_9000_3c0d_ec02,
2504 GraphBuildPasses::Two,
2505 None,
2506 ),
2507 Err(GraphBuildError::CheckpointCorrupt(_))
2508 ));
2509 assert!(matches!(
2510 decode_checkpoint(
2511 &valid,
2512 &vectors,
2513 params,
2514 0x0001_9000_3c0d_ec01,
2515 GraphBuildPasses::One,
2516 None,
2517 ),
2518 Err(GraphBuildError::CheckpointCorrupt(_))
2519 ));
2520 let one_pass_refine = checkpoint_with_mutation(valid, |bytes| {
2521 bytes[10] = 2;
2522 bytes[11] = 1;
2523 });
2524 assert!(matches!(
2525 decode_checkpoint(
2526 &one_pass_refine,
2527 &vectors,
2528 params,
2529 0x0001_9000_3c0d_ec01,
2530 GraphBuildPasses::One,
2531 None,
2532 ),
2533 Err(GraphBuildError::CheckpointCorrupt(_))
2534 ));
2535 }
2536
2537 #[test]
2538 fn corrupt_checkpoint_refuses_once_clears_the_file_and_the_next_session_starts_fresh() {
2539 let directory = tempfile::tempdir().expect("corrupt checkpoint directory");
2540 let reader = fixture_reader(directory.path(), 16);
2541 let checkpoint = directory.path().join("corrupt.checkpoint");
2542 let temporary = super::checkpoint_temp_path(&checkpoint);
2543 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2544 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2545 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2546 let lease = store.snapshot().expect("checkpoint lease");
2547 let control = QueryControl::Cancel(CancelToken::new());
2548 let cancellation = QueryCancellation::new(&control, &lease);
2549 let mut session = GraphBuildSession::new(
2550 &StdVfs,
2551 &reader,
2552 params,
2553 0x0001_9000_c077_0001,
2554 GraphBuildPasses::Two,
2555 &checkpoint,
2556 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2557 .expect("derived policy"),
2558 None,
2559 )
2560 .expect("checkpoint session");
2561 assert!(
2562 !session
2563 .advance_batch(&cancellation)
2564 .expect("checkpoint batch")
2565 );
2566 drop(session);
2567
2568 let mut corrupt = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2569 corrupt[40] ^= 1;
2570 StdVfs
2571 .write(&checkpoint, &corrupt)
2572 .expect("write corrupt checkpoint");
2573 StdVfs
2574 .write(&temporary, b"stale temporary checkpoint")
2575 .expect("write stale temporary checkpoint");
2576
2577 assert!(matches!(
2578 GraphBuildSession::new(
2579 &StdVfs,
2580 &reader,
2581 params,
2582 0x0001_9000_c077_0001,
2583 GraphBuildPasses::Two,
2584 &checkpoint,
2585 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2586 .expect("derived policy"),
2587 None,
2588 ),
2589 Err(GraphBuildError::CheckpointCorrupt(_))
2590 ));
2591 assert_eq!(
2592 StdVfs
2593 .read(&checkpoint)
2594 .expect_err("corrupt checkpoint was retained")
2595 .kind(),
2596 std::io::ErrorKind::NotFound
2597 );
2598 assert_eq!(
2599 StdVfs
2600 .read(&temporary)
2601 .expect_err("stale checkpoint temporary was retained")
2602 .kind(),
2603 std::io::ErrorKind::NotFound
2604 );
2605
2606 let fresh = GraphBuildSession::new(
2607 &StdVfs,
2608 &reader,
2609 params,
2610 0x0001_9000_c077_0001,
2611 GraphBuildPasses::Two,
2612 &checkpoint,
2613 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2614 .expect("derived policy"),
2615 None,
2616 )
2617 .expect("next session starts fresh");
2618 assert_eq!(fresh.completed_work_rows().expect("fresh work rows"), 0);
2619 }
2620
2621 #[test]
2622 fn accounting_rejection_on_resume_preserves_the_valid_checkpoint() {
2623 let directory = tempfile::tempdir().expect("accounting checkpoint directory");
2624 let reader = fixture_reader(directory.path(), 16);
2625 let checkpoint = directory.path().join("accounting.checkpoint");
2626 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2627 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2628 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2629 let lease = store.snapshot().expect("checkpoint lease");
2630 let control = QueryControl::Cancel(CancelToken::new());
2631 let cancellation = QueryCancellation::new(&control, &lease);
2632 let mut session = GraphBuildSession::new(
2633 &StdVfs,
2634 &reader,
2635 params,
2636 0x0001_9000_acc0_0001,
2637 GraphBuildPasses::Two,
2638 &checkpoint,
2639 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2640 .expect("derived policy"),
2641 None,
2642 )
2643 .expect("checkpoint session");
2644 assert!(
2645 !session
2646 .advance_batch(&cancellation)
2647 .expect("checkpoint batch")
2648 );
2649 drop(session);
2650 let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2651
2652 let refused_directory = tempfile::tempdir().expect("refused store directory");
2653 let refused = Store::open(
2654 refused_directory.path(),
2655 OpenOptions::default().with_max_temp_bytes(1),
2656 )
2657 .expect("refused store");
2658 assert!(matches!(
2659 GraphBuildSession::new(
2660 &StdVfs,
2661 &reader,
2662 params,
2663 0x0001_9000_acc0_0001,
2664 GraphBuildPasses::Two,
2665 &checkpoint,
2666 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2667 .expect("derived policy"),
2668 Some(&refused.accounting),
2669 ),
2670 Err(GraphBuildError::Store(_))
2671 ));
2672 assert_eq!(
2673 StdVfs
2674 .read(&checkpoint)
2675 .expect("accounting rejection removed the valid checkpoint"),
2676 valid
2677 );
2678 }
2679
2680 #[test]
2681 fn incomplete_session_and_checkpoint_io_fail_typed() {
2682 let directory = tempfile::tempdir().expect("typed failure directory");
2683 let reader = fixture_reader(directory.path(), 16);
2684 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("typed failure params");
2685 let control = QueryControl::Cancel(CancelToken::new());
2686 let store_directory = tempfile::tempdir().expect("typed failure store");
2687 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2688 let lease = store.snapshot().expect("typed failure lease");
2689 let cancellation = QueryCancellation::new(&control, &lease);
2690 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2691 .expect("derived policy");
2692
2693 let unfinished = GraphBuildSession::new(
2694 &StdVfs,
2695 &reader,
2696 params,
2697 0x0001_9000_3100_0001,
2698 GraphBuildPasses::Two,
2699 &directory.path().join("unfinished.checkpoint"),
2700 policy,
2701 None,
2702 )
2703 .expect("unfinished session");
2704 assert!(matches!(
2705 unfinished.artifact(),
2706 Err(GraphBuildError::Geometry(_))
2707 ));
2708
2709 assert!(matches!(
2710 GraphBuildSession::new(
2711 &StdVfs,
2712 &reader,
2713 params,
2714 0x0001_9000_3100_0001,
2715 GraphBuildPasses::Two,
2716 directory.path(),
2717 policy,
2718 None,
2719 ),
2720 Err(GraphBuildError::CheckpointIo { .. })
2721 ));
2722
2723 let missing_parent = directory.path().join("missing").join("write.checkpoint");
2724 let mut unwritable = GraphBuildSession::new(
2725 &StdVfs,
2726 &reader,
2727 params,
2728 0x0001_9000_3100_0001,
2729 GraphBuildPasses::Two,
2730 &missing_parent,
2731 policy,
2732 None,
2733 )
2734 .expect("unwritable session starts before checkpoint write");
2735 assert!(matches!(
2736 unwritable.advance_batch(&cancellation),
2737 Err(GraphBuildError::CheckpointIo { .. })
2738 ));
2739
2740 let timeout = QueryControl::Cancel(CancelToken::new());
2741 timeout.mark_timed_out();
2742 let timed_out = QueryCancellation::new(&timeout, &lease);
2743 assert!(matches!(
2744 super::check_cancellation(&timed_out),
2745 Err(GraphBuildError::Timeout { partial: false })
2746 ));
2747 }
2748
2749 #[test]
2750 fn vamana_build_is_deterministic_under_seed() {
2751 let directory = tempfile::tempdir().expect("fixture directory");
2752 let reader = fixture_reader(directory.path(), 48);
2753 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("test params");
2754 let mut random = crate::test_support::seeded_rng(
2755 "graph::build::vamana_build_is_deterministic_under_seed",
2756 );
2757 let seed = random.next_u64();
2758 let first = build_graph(&reader, params, seed, GraphBuildPasses::Two)
2759 .expect("first deterministic build");
2760 let second = build_graph(&reader, params, seed, GraphBuildPasses::Two)
2761 .expect("second deterministic build");
2762 assert_eq!(first.encoded_region(), second.encoded_region());
2763 }
2764
2765 #[test]
2766 fn cancelled_build_leaves_no_partial_artifact() {
2767 let directory = tempfile::tempdir().expect("sealed segment directory");
2768 let reader = fixture_reader(directory.path(), 48);
2769 let segment_path = directory.path().join(reader.meta().id.file_name());
2770 let before = StdVfs
2771 .read(&segment_path)
2772 .expect("sealed bytes before build");
2773 let checkpoint = directory.path().join("graph-build.checkpoint");
2774 let checkpoint_temp = directory.path().join("graph-build.checkpoint.tmp");
2775 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
2776 let store_directory = tempfile::tempdir().expect("store directory");
2777 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2778 let lease = store.snapshot().expect("snapshot lease");
2779 let token = CancelToken::new();
2780 let control = QueryControl::Cancel(token.clone());
2781 let cancellation = QueryCancellation::new(&control, &lease);
2782 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2783 .expect("derived policy");
2784 let mut session = GraphBuildSession::new(
2785 &StdVfs,
2786 &reader,
2787 params,
2788 0x19_0003,
2789 GraphBuildPasses::Two,
2790 &checkpoint,
2791 policy,
2792 None,
2793 )
2794 .expect("build session");
2795
2796 assert!(!session.advance_batch(&cancellation).expect("first batch"));
2797 token.cancel();
2798 assert!(matches!(
2799 session.advance_batch(&cancellation),
2800 Err(GraphBuildError::Cancelled { partial: false })
2801 ));
2802 assert_eq!(
2803 StdVfs
2804 .read(&segment_path)
2805 .expect("sealed bytes after cancel"),
2806 before
2807 );
2808 assert!(
2809 checkpoint.exists(),
2810 "completed batch checkpoint must remain resumable"
2811 );
2812 assert!(
2813 !checkpoint_temp.exists(),
2814 "checkpoint temp must not survive"
2815 );
2816 }
2817
2818 #[test]
2819 fn build_resumes_from_checkpoint_to_the_same_graph() {
2820 let directory = tempfile::tempdir().expect("sealed segment directory");
2821 let reader = fixture_reader(directory.path(), 64);
2822 let checkpoint = directory.path().join("resume.checkpoint");
2823 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
2824 let store_directory = tempfile::tempdir().expect("store directory");
2825 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2826 let lease = store.snapshot().expect("snapshot lease");
2827 let control = QueryControl::Cancel(CancelToken::new());
2828 let cancellation = QueryCancellation::new(&control, &lease);
2829 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2830 .expect("derived policy");
2831 let mut interrupted = GraphBuildSession::new(
2832 &StdVfs,
2833 &reader,
2834 params,
2835 0x19_0003_00c0_ffee,
2836 GraphBuildPasses::Two,
2837 &checkpoint,
2838 policy,
2839 None,
2840 )
2841 .expect("interrupted session");
2842 assert!(
2843 !interrupted
2844 .advance_batch(&cancellation)
2845 .expect("first batch")
2846 );
2847 drop(interrupted);
2848
2849 let mut resumed = GraphBuildSession::new(
2850 &StdVfs,
2851 &reader,
2852 params,
2853 0x19_0003_00c0_ffee,
2854 GraphBuildPasses::Two,
2855 &checkpoint,
2856 policy,
2857 None,
2858 )
2859 .expect("resumed session");
2860 while !resumed.advance_batch(&cancellation).expect("resumed batch") {}
2861 let resumed = resumed.artifact().expect("resumed artifact");
2862 let uninterrupted =
2863 build_graph(&reader, params, 0x19_0003_00c0_ffee, GraphBuildPasses::Two)
2864 .expect("uninterrupted artifact");
2865 assert_eq!(resumed.encoded_region(), uninterrupted.encoded_region());
2866 assert!(!checkpoint.exists(), "completed build removes checkpoint");
2867 }
2868
2869 #[test]
2870 fn checkpointed_build_reports_exact_work_for_each_declared_pass() {
2871 let directory = tempfile::tempdir().expect("work-count fixture directory");
2872 let rows = 24_usize;
2873 let reader = fixture_reader(directory.path(), rows);
2874 let params = GraphParams::new(6, 10, 1.0, 1.2, 16, 6).expect("work-count params");
2875 let store_directory = tempfile::tempdir().expect("work-count store directory");
2876 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2877 let lease = store.snapshot().expect("work-count lease");
2878 let control = QueryControl::Cancel(CancelToken::new());
2879
2880 for (passes, expected_rows, suffix) in [
2881 (GraphBuildPasses::One, rows as u64, "one"),
2882 (GraphBuildPasses::Two, (rows * 2) as u64, "two"),
2883 ] {
2884 let checkpoint = directory
2885 .path()
2886 .join(format!("work-count-{suffix}.checkpoint"));
2887 let artifact = build_graph_checkpointed(
2888 &store,
2889 &reader,
2890 CheckpointedGraphBuild::new(params, 0x19_0003_c0a7, passes, &checkpoint, &control),
2891 &lease,
2892 )
2893 .expect("checkpointed build completes");
2894
2895 assert_eq!(artifact.work_rows_completed(), expected_rows);
2896 assert_eq!(
2897 decode_node_blocks(artifact.encoded_region())
2898 .expect("work-count graph decodes")
2899 .node_count(),
2900 rows as u32
2901 );
2902 assert!(
2903 !checkpoint.exists(),
2904 "completed build removes {suffix} checkpoint"
2905 );
2906 }
2907 }
2908
2909 #[test]
2910 fn graph_build_handles_the_minimum_shape_and_rejects_an_empty_segment() {
2911 let directory = tempfile::tempdir().expect("minimum-shape directory");
2912 let empty = fixture_reader(directory.path(), 0);
2913 assert!(matches!(
2914 SegmentVectors::new(&empty),
2915 Err(GraphBuildError::Geometry(detail)) if detail.contains("at least one row")
2916 ));
2917
2918 let one_row_directory = tempfile::tempdir().expect("one-row directory");
2919 let one_row = fixture_reader(one_row_directory.path(), 1);
2920 let params = GraphParams::new(1, 1, 1.0, 1.0, 1, 1).expect("one-row params");
2921 let artifact = build_graph(&one_row, params, 0x19_0003_0000_0001, GraphBuildPasses::One)
2922 .expect("one-row build succeeds");
2923 let graph = decode_node_blocks(artifact.encoded_region()).expect("one-row graph decodes");
2924 let node = graph.block(0).expect("row zero exists");
2925 assert_eq!(graph.node_count(), 1);
2926 assert_eq!(node.degree(), 0);
2927 assert_eq!(node.flags() & 1, 1);
2928 assert_eq!(artifact.entry_points(), &[0]);
2929 }
2930
2931 #[test]
2932 fn every_node_has_at_most_r_max_neighbours() {
2933 let mut seeded = crate::test_support::seeded_rng(
2934 "graph::build::every_node_has_at_most_r_max_neighbours",
2935 );
2936 let mut runner = TestRunner::new(Config {
2937 cases: 24,
2938 rng_seed: RngSeed::Fixed(seeded.next_u64()),
2939 ..Config::default()
2940 });
2941 let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
2942 let result = runner.run(&strategy, |(rows, r_target, seed)| {
2943 let directory = tempfile::tempdir().expect("property directory");
2944 let reader = fixture_reader(directory.path(), rows);
2945 let r_max = r_target.saturating_add(4);
2946 let params = GraphParams::new(
2947 r_target,
2948 r_max,
2949 1.0,
2950 1.2,
2951 u16::from(r_max).saturating_add(8),
2952 rows as u32,
2953 )
2954 .expect("property params");
2955 let artifact =
2956 build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
2957 let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
2958 prop_assert_eq!(graph.layout().max_degree(), r_max);
2959 for node_id in 0..graph.node_count() {
2960 let block = graph.block(node_id).expect("property node");
2961 prop_assert!(block.degree() <= r_max);
2962 }
2963 Ok(())
2964 });
2965 assert!(result.is_ok(), "property result: {result:?}");
2966 }
2967
2968 #[test]
2969 fn no_node_lists_itself_or_a_duplicate() {
2970 let mut seeded =
2971 crate::test_support::seeded_rng("graph::build::no_node_lists_itself_or_a_duplicate");
2972 let mut runner = TestRunner::new(Config {
2973 cases: 24,
2974 rng_seed: RngSeed::Fixed(seeded.next_u64()),
2975 ..Config::default()
2976 });
2977 let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
2978 let result = runner.run(&strategy, |(rows, r_target, seed)| {
2979 let directory = tempfile::tempdir().expect("property directory");
2980 let reader = fixture_reader(directory.path(), rows);
2981 let r_max = r_target.saturating_add(4);
2982 let params = GraphParams::new(
2983 r_target,
2984 r_max,
2985 1.0,
2986 1.2,
2987 u16::from(r_max).saturating_add(8),
2988 rows as u32,
2989 )
2990 .expect("property params");
2991 let artifact =
2992 build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
2993 let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
2994 for node_id in 0..graph.node_count() {
2995 let block = graph.block(node_id).expect("property node");
2996 let mut unique = std::collections::BTreeSet::new();
2997 for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
2998 prop_assert_ne!(neighbor, node_id);
2999 prop_assert!(unique.insert(neighbor));
3000 }
3001 }
3002 Ok(())
3003 });
3004 assert!(result.is_ok(), "property result: {result:?}");
3005 }
3006
3007 #[test]
3008 fn graph_is_connected_from_the_entry_points() {
3009 let directory = tempfile::tempdir().expect("connectivity directory");
3010 let reader = fixture_reader(directory.path(), 96);
3011 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 96).expect("connectivity params");
3012 let artifact = build_graph(
3013 &reader,
3014 params,
3015 0x0001_9000_3c01_1ec7,
3016 GraphBuildPasses::Two,
3017 )
3018 .expect("connectivity graph");
3019 let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3020 let mut reached = vec![false; graph.node_count() as usize];
3021 let mut pending = std::collections::VecDeque::new();
3022 for &entry in artifact.entry_points() {
3023 pending.push_back(entry);
3024 }
3025 while let Some(node_id) = pending.pop_front() {
3026 let node = node_id as usize;
3027 if reached[node] {
3028 continue;
3029 }
3030 reached[node] = true;
3031 let block = graph.block(node_id).expect("reachable node");
3032 for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
3033 if !reached[neighbor as usize] {
3034 pending.push_back(neighbor);
3035 }
3036 }
3037 }
3038 let reached_count = reached.iter().filter(|seen| **seen).count();
3039 assert_eq!(reached_count, graph.node_count() as usize);
3040 }
3041
3042 #[test]
3043 fn graph_build_memory_is_budgeted_and_exactly_accounted() {
3044 let directory = tempfile::tempdir().expect("accounting fixture directory");
3045 let reader = fixture_reader(directory.path(), 48);
3046 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("accounting params");
3047 let control = QueryControl::Cancel(CancelToken::new());
3048
3049 let refused_directory = tempfile::tempdir().expect("refused store directory");
3050 let refused = Store::open(
3051 refused_directory.path(),
3052 OpenOptions::default().with_max_temp_bytes(1),
3053 )
3054 .expect("refused store");
3055 let refused_lease = refused.snapshot().expect("refused lease");
3056 let refused_result = build_graph_checkpointed(
3057 &refused,
3058 &reader,
3059 CheckpointedGraphBuild::new(
3060 params,
3061 0x19_0003_acc0,
3062 GraphBuildPasses::One,
3063 &directory.path().join("refused.checkpoint"),
3064 &control,
3065 ),
3066 &refused_lease,
3067 );
3068 assert!(matches!(refused_result, Err(GraphBuildError::Store(_))));
3069 assert_eq!(refused.stats().expect("refused stats").temporary_bytes, 0);
3070
3071 let store_directory = tempfile::tempdir().expect("accounted store directory");
3072 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3073 let lease = store.snapshot().expect("accounted lease");
3074 let artifact = build_graph_checkpointed(
3075 &store,
3076 &reader,
3077 CheckpointedGraphBuild::new(
3078 params,
3079 0x19_0003_acc0,
3080 GraphBuildPasses::One,
3081 &directory.path().join("accounted.checkpoint"),
3082 &control,
3083 ),
3084 &lease,
3085 )
3086 .expect("accounted graph");
3087 let expected =
3088 artifact.encoded_region().len() + std::mem::size_of_val(artifact.entry_points());
3089 assert_eq!(
3090 store.stats().expect("live graph stats").temporary_bytes,
3091 expected as u64
3092 );
3093 drop(artifact);
3094 assert_eq!(
3095 store.stats().expect("released graph stats").temporary_bytes,
3096 0
3097 );
3098 }
3099
3100 #[test]
3101 fn arbitrary_checkpoint_bytes_fail_typed_without_panicking() {
3102 let mut seeded = crate::test_support::seeded_rng(
3103 "graph::build::arbitrary_checkpoint_bytes_fail_typed_without_panicking",
3104 );
3105 let mut runner = TestRunner::new(Config {
3106 cases: 64,
3107 rng_seed: RngSeed::Fixed(seeded.next_u64()),
3108 ..Config::default()
3109 });
3110 let result = runner.run(&proptest::collection::vec(any::<u8>(), 0..2048), |bytes| {
3111 prop_assert!(matches!(
3112 validate_graph_build_checkpoint(&bytes),
3113 Err(GraphBuildError::CheckpointCorrupt(_))
3114 ));
3115 Ok(())
3116 });
3117 assert!(result.is_ok(), "property result: {result:?}");
3118 }
3119}