1use std::ops::Deref;
4use std::path::Path;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Condvar, Mutex};
7use std::time::{Duration, Instant};
8
9use crate::manifest::Manifest;
10use crate::manifest::io::{DurableLog, MANIFEST_FILE, load_manifest};
11use crate::meta::{AliveSet, ColumnStore};
12use crate::quant::Bit4Factors;
13use crate::segment::SegmentId;
14use crate::segment::layout::Int8Factors;
15use crate::segment::reader::{SegmentReader, validate_header_with_vfs};
16use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
17use crate::vfs::Vfs;
18use crate::wal::WalReader;
19
20#[cfg(test)]
21use super::close::TeardownProbe;
22use super::stats::{Accounted, Accounting, AllocationComponent, MappingReservation};
23use super::{Store, StoreError, StoreState};
24
25#[cfg(test)]
26struct SnapshotReleaseProbe(Option<Arc<TeardownProbe>>);
27
28#[cfg(test)]
29impl Drop for SnapshotReleaseProbe {
30 fn drop(&mut self) {
31 if let Some(probe) = &self.0 {
32 probe.record_snapshot_released();
33 }
34 }
35}
36
37const STORE_WAL_FILE: &str = "wal.ze";
38
39pub enum InMemorySegmentFactors {
44 Bit4(Vec<Bit4Factors>),
46 Int8(Vec<Int8Factors>),
48}
49
50pub struct InMemorySegment<'a> {
56 pub id: SegmentId,
58 pub scheme: u16,
60 pub dims: u32,
62 pub codes: Vec<u8>,
64 pub factors: InMemorySegmentFactors,
66 pub rescore: Vec<f32>,
68 pub columns: &'a ColumnStore,
70 pub alive: &'a AliveSet,
72}
73
74enum PreparedFactors {
75 Bit4(Accounted<Vec<Bit4Factors>>),
76 Int8(Accounted<Vec<Int8Factors>>),
77}
78
79pub struct PreparedSegment<'a> {
81 accounting: Arc<Accounting>,
82 id: SegmentId,
83 scheme: u16,
84 dims: u32,
85 codes: Accounted<Vec<u8>>,
86 factors: PreparedFactors,
87 rescore: Accounted<Vec<f32>>,
88 columns: &'a ColumnStore,
89 alive: &'a AliveSet,
90 resident_bytes: u64,
91}
92
93impl PreparedSegment<'_> {
94 #[must_use]
96 pub const fn resident_bytes(&self) -> u64 {
97 self.resident_bytes
98 }
99
100 fn as_build(&self) -> SegmentBuild<'_> {
101 let factors = match &self.factors {
102 PreparedFactors::Bit4(values) => SegmentFactors::Bit4(values.as_slice()),
103 PreparedFactors::Int8(values) => SegmentFactors::Int8(values.as_slice()),
104 };
105 SegmentBuild {
106 id: self.id,
107 scheme: self.scheme,
108 dims: self.dims,
109 codes: self.codes.as_slice(),
110 factors,
111 rescore: self.rescore.as_slice(),
112 columns: self.columns,
113 alive: self.alive,
114 }
115 }
116}
117
118impl Store {
119 pub fn prepare_segment<'a>(
124 &self,
125 segment: InMemorySegment<'a>,
126 ) -> Result<PreparedSegment<'a>, StoreError> {
127 let state = self
128 .state
129 .lock()
130 .map_err(|_| StoreError::Synchronization { component: "state" })?;
131 match *state {
132 StoreState::Open => {}
133 StoreState::Closing => return Err(StoreError::Closing),
134 StoreState::Closed => return Err(StoreError::Closed),
135 }
136 let writer_lock = self
137 .writer_lock
138 .lock()
139 .map_err(|_| StoreError::Synchronization {
140 component: "writer lock",
141 })?;
142 if writer_lock.is_none() {
143 return Err(StoreError::ReadOnly);
144 }
145 let InMemorySegment {
146 id,
147 scheme,
148 dims,
149 codes,
150 factors,
151 rescore,
152 columns,
153 alive,
154 } = segment;
155 let codes =
156 Accounted::try_from_vec(&self.accounting, codes, AllocationComponent::Snapshot)?;
157 let factors = match factors {
158 InMemorySegmentFactors::Bit4(values) => PreparedFactors::Bit4(Accounted::try_from_vec(
159 &self.accounting,
160 values,
161 AllocationComponent::Snapshot,
162 )?),
163 InMemorySegmentFactors::Int8(values) => PreparedFactors::Int8(Accounted::try_from_vec(
164 &self.accounting,
165 values,
166 AllocationComponent::Snapshot,
167 )?),
168 };
169 let rescore =
170 Accounted::try_from_vec(&self.accounting, rescore, AllocationComponent::Snapshot)?;
171 let factor_bytes = match &factors {
172 PreparedFactors::Bit4(values) => values.resident_bytes(),
173 PreparedFactors::Int8(values) => values.resident_bytes(),
174 };
175 let resident_bytes = codes
176 .resident_bytes()
177 .checked_add(factor_bytes)
178 .and_then(|bytes| bytes.checked_add(rescore.resident_bytes()))
179 .ok_or(StoreError::BudgetExceeded {
180 needed: u64::MAX,
181 budget: u64::MAX,
182 component: "snapshot",
183 })?;
184 drop(writer_lock);
185 drop(state);
186 Ok(PreparedSegment {
187 accounting: Arc::clone(&self.accounting),
188 id,
189 scheme,
190 dims,
191 codes,
192 factors,
193 rescore,
194 columns,
195 alive,
196 resident_bytes,
197 })
198 }
199
200 pub fn seal_snapshot(&self, segment: PreparedSegment<'_>) -> Result<u64, StoreError> {
207 self.seal_snapshot_with_vfs(segment, self.vfs.as_ref())
208 }
209
210 fn seal_snapshot_with_vfs(
211 &self,
212 segment: PreparedSegment<'_>,
213 vfs: &dyn Vfs,
214 ) -> Result<u64, StoreError> {
215 let state = self
216 .state
217 .lock()
218 .map_err(|_| StoreError::Synchronization { component: "state" })?;
219 match *state {
220 StoreState::Open => {}
221 StoreState::Closing => return Err(StoreError::Closing),
222 StoreState::Closed => return Err(StoreError::Closed),
223 }
224 let writer_lock = self
225 .writer_lock
226 .lock()
227 .map_err(|_| StoreError::Synchronization {
228 component: "writer lock",
229 })?;
230 if writer_lock.is_none() {
231 return Err(StoreError::ReadOnly);
232 }
233 if !Arc::ptr_eq(&segment.accounting, &self.accounting) {
234 return Err(StoreError::ForeignPreparedSegment);
235 }
236 let wal = self
237 .wal_writer
238 .lock()
239 .map_err(|_| StoreError::Synchronization {
240 component: "WAL writer",
241 })?;
242 let writer = wal.as_ref().ok_or(StoreError::ReadOnly)?;
243 let generation = self
244 .active
245 .lock()
246 .map_err(|_| StoreError::Synchronization {
247 component: "active segment",
248 })?
249 .as_ref()
250 .ok_or(StoreError::Closed)?
251 .generation
252 .checked_add(1)
253 .ok_or(StoreError::GenerationOverflow)?;
254 #[cfg(feature = "graph-cypher")]
255 let mut committed =
256 crate::ingest::load_current_manifest(vfs, &self.directory, u64::MAX, 0, &self.schema)?;
257 #[cfg(feature = "graph-cypher")]
258 let graph_log_seq = if committed.graph.is_some() {
259 committed.log_seq
260 } else {
261 0
262 };
263 #[cfg(feature = "graph-cypher")]
264 if committed.graph.is_some() {
265 let boundary = writer.durable_end();
266 committed
267 .record_generation_bump(boundary)
268 .map_err(StoreError::Manifest)?;
269 }
270 #[cfg(feature = "graph-cypher")]
271 let graph = committed.graph;
272 let mut meta = write_segment(
273 vfs,
274 &self.directory,
275 segment.as_build(),
276 self.durability_policy,
277 )
278 .map_err(StoreError::Segment)?;
279 let epoch_alias = self.epoch_identity();
280 meta.epoch_id = epoch_alias.map(|identity| identity.embedding);
281 let mut publication = writer.manifest_publication()?;
282 publication
283 .commit_manifest(
284 vfs,
285 &self.directory,
286 &Manifest {
287 #[cfg(feature = "graph-cypher")]
288 graph,
289 generation,
290 #[cfg(feature = "graph-cypher")]
291 log_seq: graph_log_seq,
292 #[cfg(not(feature = "graph-cypher"))]
293 log_seq: 0,
294 segments: vec![meta],
295 epochs: self.epoch_registry(&[]),
296 epoch_alias,
297 schema: segment.columns.schema().clone(),
298 },
299 self.durability_policy,
300 )
301 .map_err(StoreError::Manifest)?;
302 let remapped = PublishedSnapshot::load_on_vfs(&self.directory, &self.accounting, vfs)?;
303 let mut published = self
304 .snapshot
305 .write()
306 .map_err(|_| StoreError::Synchronization {
307 component: "published snapshot",
308 })?;
309 let previous = published.replace(Arc::new(remapped));
310 drop(published);
311 drop(previous);
312 drop(segment);
313 let mut active_guard = self
314 .active
315 .lock()
316 .map_err(|_| StoreError::Synchronization {
317 component: "active segment",
318 })?;
319 active_guard.as_mut().ok_or(StoreError::Closed)?.generation = generation;
320 publication.complete();
321 drop(wal);
322 drop(active_guard);
323 drop(writer_lock);
324 drop(state);
325 Ok(generation)
326 }
327}
328
329#[cfg(feature = "graph-cypher")]
330pub(crate) struct SnapshotGraphManifest {
331 graph: crate::manifest::GraphManifest,
332 _charge: super::stats::AccountedCounter,
333}
334
335#[cfg(feature = "graph-cypher")]
336impl Deref for SnapshotGraphManifest {
337 type Target = crate::manifest::GraphManifest;
338 fn deref(&self) -> &Self::Target {
339 &self.graph
340 }
341}
342
343#[cfg(feature = "graph-cypher")]
344impl SnapshotGraphManifest {
345 fn copy(
346 graph: &crate::manifest::GraphManifest,
347 accounting: &Arc<Accounting>,
348 ) -> Result<Arc<Self>, StoreError> {
349 let bytes = |graph: &crate::manifest::GraphManifest| {
350 graph
351 .backing_bytes()
352 .and_then(|bytes| {
353 bytes
354 .checked_add(std::mem::size_of::<Self>() + 2 * std::mem::size_of::<usize>())
355 })
356 .ok_or(StoreError::AllocationFailed {
357 needed: u64::MAX,
358 component: "snapshot graph manifest",
359 })
360 };
361 let mut charge =
362 super::stats::AccountedCounter::new(accounting, AllocationComponent::Snapshot)?;
363 charge.set(bytes(graph)?)?;
364 #[cfg(feature = "allocation-audit")]
365 let graph = crate::allocation_audit::attributed(|| graph.clone());
366 #[cfg(not(feature = "allocation-audit"))]
367 let graph = graph.clone();
368 charge.set(bytes(&graph)?)?;
369 let owner = Self {
370 graph,
371 _charge: charge,
372 };
373 #[cfg(feature = "allocation-audit")]
374 let owner = crate::allocation_audit::attributed(|| Arc::new(owner));
375 #[cfg(not(feature = "allocation-audit"))]
376 let owner = Arc::new(owner);
377 Ok(owner)
378 }
379}
380
381pub struct PublishedSnapshot {
383 generation: u64,
384 absorbed_through: u64,
385 pub(crate) graph_absorbed_through: u64,
386 #[cfg(feature = "graph-cypher")]
387 pub(crate) graph_enabled: bool,
388 #[cfg(feature = "graph-cypher")]
389 pub(crate) graph_manifest: Option<Arc<SnapshotGraphManifest>>,
390 epoch_alias: Option<crate::epoch::EpochIdentity>,
391 graph_profile:
392 Result<crate::graph::search::EpochGraphProfile, crate::graph::search::GraphProfileError>,
393 schema: crate::meta::Schema,
394 segments: Arc<Accounted<Vec<SegmentReader>>>,
395 query_segment_count: usize,
396 cancelled: AtomicBool,
397 reader_signal: Arc<ReaderSignal>,
398 _mapping_reservation: Option<Arc<MappingReservation>>,
399 #[cfg(test)]
400 release_probe: SnapshotReleaseProbe,
401}
402
403impl PublishedSnapshot {
404 #[cfg(test)]
405 pub(crate) fn empty(generation: u64) -> Self {
406 Self::empty_with_signal(generation, Arc::new(ReaderSignal::new(None)))
407 }
408
409 fn empty_with_signal(generation: u64, reader_signal: Arc<ReaderSignal>) -> Self {
410 Self {
411 generation,
412 absorbed_through: 0,
413 graph_absorbed_through: 0,
414 #[cfg(feature = "graph-cypher")]
415 graph_enabled: false,
416 #[cfg(feature = "graph-cypher")]
417 graph_manifest: None,
418 epoch_alias: None,
419 graph_profile: Err(crate::graph::search::GraphProfileError::EpochUnstamped),
420 schema: crate::meta::Schema::timestamp_only(),
421 segments: Arc::new(Accounted::unaccounted_empty()),
422 query_segment_count: 0,
423 cancelled: AtomicBool::new(false),
424 reader_signal,
425 _mapping_reservation: None,
426 #[cfg(test)]
427 release_probe: SnapshotReleaseProbe(None),
428 }
429 }
430
431 pub(crate) fn load_on_vfs(
432 directory: &Path,
433 accounting: &Arc<Accounting>,
434 vfs: &dyn Vfs,
435 ) -> Result<Self, StoreError> {
436 Self::load_impl(directory, accounting, vfs, false, None)
437 }
438
439 pub(crate) fn load_for_open_on_vfs(
440 directory: &Path,
441 accounting: &Arc<Accounting>,
442 vfs: &dyn Vfs,
443 preparation: Option<&super::namespace_batch::PrivatePreparation>,
444 ) -> Result<Self, StoreError> {
445 Self::load_impl(directory, accounting, vfs, true, preparation)
446 }
447
448 fn load_impl(
449 directory: &Path,
450 accounting: &Arc<Accounting>,
451 vfs: &dyn Vfs,
452 probe_segment_headers: bool,
453 preparation: Option<&super::namespace_batch::PrivatePreparation>,
454 ) -> Result<Self, StoreError> {
455 let manifest_path = if probe_segment_headers {
456 super::namespace_batch::manifest_for_open(vfs, directory, preparation)?
457 } else {
458 directory.join(MANIFEST_FILE)
459 };
460 match vfs.open(&manifest_path) {
461 Ok(_) => {}
462 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
463 return Ok(Self::empty_with_signal(
464 0,
465 ReaderSignal::accounted(accounting)?,
466 ));
467 }
468 Err(source) => {
469 return Err(StoreError::Io {
470 path: manifest_path,
471 source,
472 });
473 }
474 }
475 let wal = WalReader::open(vfs, &directory.join(STORE_WAL_FILE)).map_err(StoreError::Wal)?;
476 if let Some(crate::wal::replay::ReplayTerminator::InvalidHeader(reason)) = wal.terminator()
479 && reason != crate::wal::header::WalHeaderError::Missing
480 {
481 let error = crate::wal::WalRecoveryError::InvalidHeader(reason);
482 #[cfg(any(test, feature = "test-seams"))]
483 super::record_storage_wal_recovery_fault(&error);
484 return Err(StoreError::WalRecovery(error));
485 }
486 let durable_end = wal.durable_end();
487 let manifest =
488 load_manifest(vfs, &manifest_path, durable_end).map_err(StoreError::Manifest)?;
489 if !matches!(
490 wal.terminator(),
491 None | Some(crate::wal::replay::ReplayTerminator::InvalidHeader(
492 crate::wal::header::WalHeaderError::Missing
493 ))
494 ) {
495 #[cfg(feature = "graph-cypher")]
496 if manifest.graph.is_some() {
497 wal.into_graph_prefix()
498 } else {
499 wal.into_clean()
500 }
501 .map_err(StoreError::WalRecovery)?;
502 #[cfg(not(feature = "graph-cypher"))]
503 wal.into_clean().map_err(StoreError::WalRecovery)?;
504 }
505 if probe_segment_headers {
506 for expected in &manifest.segments {
507 let path = directory.join(expected.id.file_name());
508 validate_header_with_vfs(vfs, &path, expected).map_err(StoreError::Segment)?;
509 }
510 }
511 Self::from_manifest(vfs, directory, &manifest, accounting)
512 }
513
514 pub(crate) fn from_manifest(
515 vfs: &dyn Vfs,
516 directory: &Path,
517 manifest: &Manifest,
518 accounting: &Arc<Accounting>,
519 ) -> Result<Self, StoreError> {
520 #[cfg(feature = "graph-cypher")]
521 if let Some(graph) = &manifest.graph {
522 super::enable_graph::validate_objects(vfs, directory, graph)?;
523 }
524 let mut segments = Accounted::try_with_capacity(
525 accounting,
526 manifest.segments.len(),
527 AllocationComponent::Snapshot,
528 )?;
529 let query_segment_count = manifest
530 .segments
531 .iter()
532 .filter(|segment| segment_is_published(manifest.epoch_alias, segment))
533 .count();
534 let ordered_segments = manifest
535 .segments
536 .iter()
537 .filter(|segment| segment_is_published(manifest.epoch_alias, segment))
538 .chain(
539 manifest
540 .segments
541 .iter()
542 .filter(|segment| !segment_is_published(manifest.epoch_alias, segment)),
543 );
544 let collection_schema = Arc::new(manifest.schema.clone());
545 for expected in ordered_segments {
546 let path = directory.join(expected.id.file_name());
547 let reader = SegmentReader::open_accounted(
548 vfs,
549 &path,
550 expected,
551 accounting,
552 &collection_schema,
553 |allocation_bytes| {
554 let bytes = u64::try_from(allocation_bytes).map_err(|_| {
555 StoreError::BudgetExceeded {
556 needed: u64::MAX,
557 budget: u64::MAX,
558 component: "snapshot",
559 }
560 })?;
561 segments.reserve_additional_bytes(bytes)
562 },
563 )?;
564 let _ = reader.stored_text().map_err(StoreError::Segment)?;
568 segments.push(reader)?;
569 }
570 let mapped_bytes = segments.iter().try_fold(0_u64, |total, segment| {
571 let bytes =
572 u64::try_from(segment.mapped_bytes()).map_err(|_| StoreError::Statistics {
573 component: "mapped bytes",
574 source: std::io::Error::other("mapped byte count exceeds u64"),
575 })?;
576 total
577 .checked_add(bytes)
578 .ok_or_else(|| StoreError::Statistics {
579 component: "mapped bytes",
580 source: std::io::Error::other("mapped byte count overflow"),
581 })
582 })?;
583 let mapping_reservation = accounting.track_mapping(mapped_bytes)?;
584 let graph_profile = manifest.epoch_alias.map_or(
585 Err(crate::graph::search::GraphProfileError::EpochUnstamped),
586 |alias| {
587 manifest
588 .epochs
589 .iter()
590 .find(|epoch| epoch.id == alias.embedding && epoch.tokenizer == alias.tokenizer)
591 .ok_or(
592 crate::graph::search::GraphProfileError::EpochNotRegistered {
593 epoch: alias,
594 },
595 )
596 .and_then(|epoch| {
597 crate::graph::search::select_epoch_graph_profile(
598 epoch,
599 crate::graph::search::GraphDistanceMetric::SquaredL2,
600 )
601 })
602 },
603 );
604 Ok(Self {
605 generation: manifest.generation,
606 absorbed_through: manifest.log_seq,
607 #[cfg(feature = "graph-cypher")]
608 graph_enabled: manifest.graph.is_some(),
609 #[cfg(feature = "graph-cypher")]
610 graph_manifest: manifest
611 .graph
612 .as_ref()
613 .map(|graph| SnapshotGraphManifest::copy(graph, accounting))
614 .transpose()?,
615 graph_absorbed_through: {
616 #[cfg(feature = "graph-cypher")]
617 {
618 manifest
619 .graph
620 .as_ref()
621 .map_or(manifest.log_seq, |graph| graph.graph_absorbed_through)
624 }
625 #[cfg(not(feature = "graph-cypher"))]
626 {
627 manifest.log_seq
628 }
629 },
630 epoch_alias: manifest.epoch_alias,
631 graph_profile,
632 schema: manifest.schema.clone(),
633 segments: Arc::new(segments),
634 query_segment_count,
635 cancelled: AtomicBool::new(false),
636 reader_signal: ReaderSignal::accounted(accounting)?,
637 _mapping_reservation: Some(Arc::new(mapping_reservation)),
638 #[cfg(test)]
639 release_probe: SnapshotReleaseProbe(None),
640 })
641 }
642
643 pub(super) fn fork_read_view(&self, accounting: &Arc<Accounting>) -> Result<Self, StoreError> {
645 Ok(Self {
646 generation: self.generation,
647 absorbed_through: self.absorbed_through,
648 graph_absorbed_through: self.graph_absorbed_through,
649 #[cfg(feature = "graph-cypher")]
650 graph_enabled: self.graph_enabled,
651 #[cfg(feature = "graph-cypher")]
652 graph_manifest: self.graph_manifest.clone(),
653 epoch_alias: self.epoch_alias,
654 graph_profile: self.graph_profile,
655 schema: self.schema.clone(),
656 segments: Arc::clone(&self.segments),
657 query_segment_count: self.query_segment_count,
658 cancelled: AtomicBool::new(false),
659 reader_signal: ReaderSignal::accounted(accounting)?,
660 _mapping_reservation: self._mapping_reservation.clone(),
661 #[cfg(test)]
662 release_probe: SnapshotReleaseProbe(None),
663 })
664 }
665
666 #[cfg(test)]
667 pub(crate) fn set_teardown_probe(&mut self, probe: Arc<TeardownProbe>) {
668 self.release_probe.0 = Some(probe);
669 }
670
671 #[must_use]
673 pub const fn generation(&self) -> u64 {
674 self.generation
675 }
676
677 pub(crate) const fn absorbed_through(&self) -> u64 {
678 self.absorbed_through
679 }
680
681 #[cfg(any(test, feature = "test-seams"))]
683 #[doc(hidden)]
684 #[must_use]
685 pub const fn storage_absorbed_through(&self) -> u64 {
686 self.absorbed_through
687 }
688
689 pub(crate) const fn epoch_alias(&self) -> Option<crate::epoch::EpochIdentity> {
690 self.epoch_alias
691 }
692
693 pub(crate) const fn graph_profile(
694 &self,
695 ) -> Result<crate::graph::search::EpochGraphProfile, crate::graph::search::GraphProfileError>
696 {
697 self.graph_profile
698 }
699
700 pub(crate) const fn schema(&self) -> &crate::meta::Schema {
701 &self.schema
702 }
703
704 #[must_use]
706 pub fn segments(&self) -> &[SegmentReader] {
707 match self.segments.get(..self.query_segment_count) {
708 Some(segments) => segments,
709 None => &[],
710 }
711 }
712
713 pub(crate) fn all_segments(&self) -> &[SegmentReader] {
714 &self.segments
715 }
716
717 pub(crate) fn drain_readers(self: &Arc<Self>, grace: Duration) -> Result<(), StoreError> {
718 let deadline = Instant::now().checked_add(grace);
719 let mut signal =
720 self.reader_signal
721 .mutex
722 .lock()
723 .map_err(|_| StoreError::Synchronization {
724 component: "snapshot readers",
725 })?;
726 while Arc::strong_count(self) > 1 {
727 let Some(deadline) = deadline else {
728 break;
729 };
730 let now = Instant::now();
731 if now >= deadline {
732 break;
733 }
734 let remaining = deadline.saturating_duration_since(now);
735 let waited = self
736 .reader_signal
737 .changed
738 .wait_timeout(signal, remaining)
739 .map_err(|_| StoreError::Synchronization {
740 component: "snapshot readers",
741 })?;
742 signal = waited.0;
743 }
744 if Arc::strong_count(self) > 1 {
745 self.cancelled.store(true, Ordering::Release);
746 self.reader_signal.changed.notify_all();
747 }
748 while Arc::strong_count(self) > 1 {
749 signal = self.reader_signal.changed.wait(signal).map_err(|_| {
750 StoreError::Synchronization {
751 component: "snapshot readers",
752 }
753 })?;
754 }
755 drop(signal);
756 Ok(())
757 }
758
759 pub(crate) fn cancel_readers(&self) {
760 self.cancelled.store(true, Ordering::Release);
761 self.reader_signal.changed.notify_all();
762 }
763}
764
765fn segment_is_published(
766 alias: Option<crate::epoch::EpochIdentity>,
767 segment: &crate::segment::SegmentMeta,
768) -> bool {
769 alias.is_none_or(|identity| segment.epoch_id == Some(identity.embedding))
770}
771
772struct ReaderSignal {
776 mutex: Mutex<()>,
777 changed: Condvar,
778 _charge: Option<super::stats::AccountedCounter>,
779 #[cfg(test)]
780 release_observed: std::sync::atomic::AtomicUsize,
781}
782
783impl ReaderSignal {
784 fn new(charge: Option<super::stats::AccountedCounter>) -> Self {
785 Self {
786 mutex: Mutex::new(()),
787 changed: Condvar::new(),
788 _charge: charge,
789 #[cfg(test)]
790 release_observed: std::sync::atomic::AtomicUsize::new(0),
791 }
792 }
793
794 fn accounted(accounting: &Arc<Accounting>) -> Result<Arc<Self>, StoreError> {
795 let mut charge =
796 super::stats::AccountedCounter::new(accounting, AllocationComponent::Snapshot)?;
797 charge.set(std::mem::size_of::<Self>() + 2 * std::mem::size_of::<usize>())?;
798 Ok(Arc::new(Self::new(Some(charge))))
799 }
800}
801
802struct ReaderRelease {
803 signal: Arc<ReaderSignal>,
804 #[cfg(test)]
805 snapshot: std::sync::Weak<PublishedSnapshot>,
806}
807
808impl Drop for ReaderRelease {
809 fn drop(&mut self) {
810 let _guard = self
811 .signal
812 .mutex
813 .lock()
814 .unwrap_or_else(std::sync::PoisonError::into_inner);
815 #[cfg(test)]
816 self.signal
817 .release_observed
818 .store(self.snapshot.strong_count(), Ordering::Relaxed);
819 self.signal.changed.notify_all();
820 }
821}
822
823pub(super) struct ReadSnapshot {
826 snapshot: Arc<PublishedSnapshot>,
827 _release: ReaderRelease,
828}
829
830impl ReadSnapshot {
831 pub(super) fn new(snapshot: Arc<PublishedSnapshot>) -> Self {
832 let release = ReaderRelease {
833 signal: Arc::clone(&snapshot.reader_signal),
834 #[cfg(test)]
835 snapshot: Arc::downgrade(&snapshot),
836 };
837 Self {
838 snapshot,
839 _release: release,
840 }
841 }
842}
843
844impl Deref for ReadSnapshot {
845 type Target = Arc<PublishedSnapshot>;
846 fn deref(&self) -> &Self::Target {
847 &self.snapshot
848 }
849}
850
851pub struct SnapshotLease {
853 snapshot: ReadSnapshot,
854 generation: u64,
855}
856
857impl SnapshotLease {
858 pub(crate) fn new(snapshot: Arc<PublishedSnapshot>) -> Self {
859 let generation = snapshot.generation();
860 Self {
861 snapshot: ReadSnapshot::new(snapshot),
862 generation,
863 }
864 }
865
866 pub(crate) fn new_at(snapshot: Arc<PublishedSnapshot>, generation: u64) -> Self {
867 Self {
868 snapshot: ReadSnapshot::new(snapshot),
869 generation,
870 }
871 }
872
873 #[must_use]
875 pub const fn generation(&self) -> u64 {
876 self.generation
877 }
878
879 pub fn check_active(&self) -> Result<(), StoreError> {
881 if self.is_cancelled() {
882 Err(StoreError::ReadCancelled)
883 } else {
884 Ok(())
885 }
886 }
887
888 pub(crate) fn is_cancelled(&self) -> bool {
889 self.snapshot.cancelled.load(Ordering::Relaxed)
890 }
891
892 pub fn wait_for_close_cancellation(&self) -> Result<(), StoreError> {
894 let mut signal =
895 self.snapshot
896 .reader_signal
897 .mutex
898 .lock()
899 .map_err(|_| StoreError::Synchronization {
900 component: "snapshot readers",
901 })?;
902 while !self.snapshot.cancelled.load(Ordering::Acquire) {
903 signal = self
904 .snapshot
905 .reader_signal
906 .changed
907 .wait(signal)
908 .map_err(|_| StoreError::Synchronization {
909 component: "snapshot readers",
910 })?;
911 }
912 drop(signal);
913 Ok(())
914 }
915}
916
917impl Deref for SnapshotLease {
918 type Target = PublishedSnapshot;
919
920 fn deref(&self) -> &Self::Target {
921 &self.snapshot
922 }
923}
924
925#[cfg(test)]
926#[allow(clippy::expect_used)]
927mod tests {
928 use std::sync::Arc;
929 use std::sync::atomic::Ordering;
930
931 use tempfile::tempdir;
932
933 use super::{InMemorySegment, InMemorySegmentFactors, PublishedSnapshot};
934 use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
935 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
936 use crate::lifecycle::{
937 ManualMonotonicClock, OpenOptions, Store, StoreError, StoreTestDependencies,
938 };
939 use crate::manifest::Manifest;
940 use crate::manifest::io::commit_manifest;
941 use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
942 use crate::quant::Bit4Factors;
943 use crate::segment::SegmentId;
944 use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
945 use crate::vfs::crash::{CrashOperation, RecordingVfs};
946 use crate::vfs::{CountingVfs, StdVfs, SyncKind};
947
948 #[test]
949 fn astra_05_snapshot_release_notifies_after_ownership_drops() {
950 let snapshot = Arc::new(PublishedSnapshot::empty(1));
951 let lease = super::SnapshotLease::new(snapshot.clone());
952 drop(lease);
953 assert_eq!(
954 snapshot
955 .reader_signal
956 .release_observed
957 .load(std::sync::atomic::Ordering::Relaxed),
958 1,
959 "close notification must observe only the draining owner"
960 );
961 let admission = super::ReadSnapshot::new(snapshot.clone());
964 let partition = super::SnapshotLease::new(snapshot.clone());
965 drop(partition);
966 assert_eq!(
967 snapshot
968 .reader_signal
969 .release_observed
970 .load(Ordering::Relaxed),
971 2
972 );
973 drop(admission);
974 assert_eq!(
975 snapshot
976 .reader_signal
977 .release_observed
978 .load(Ordering::Relaxed),
979 1
980 );
981 }
982
983 #[test]
984 fn sealed_segment_open_is_routed_through_vfs_open_for_map() {
985 let directory = tempdir().expect("mapped segment directory");
986 let schema = Schema::new(Vec::new()).expect("timestamp-only schema");
987 let mut builder = ColumnStoreBuilder::new(schema.clone());
988 builder.push_row(7, &[]).expect("fixture row");
989 let columns = builder.finish().expect("fixture columns");
990 let alive = AliveSet::new(1);
991 let id = SegmentId::new(0x06, [0x06; 10]);
992 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
993 .expect("derived policy");
994 let segment = write_segment(
995 &StdVfs,
996 directory.path(),
997 SegmentBuild {
998 id,
999 scheme: 4,
1000 dims: 4,
1001 codes: &[0_u8; 2],
1002 factors: SegmentFactors::Bit4(&[Bit4Factors::from_persisted(1.0, 1.0, 1.0)]),
1003 rescore: &[0.0_f32; 4],
1004 columns: &columns,
1005 alive: &alive,
1006 },
1007 policy,
1008 )
1009 .expect("sealed segment");
1010 commit_manifest(
1011 &StdVfs,
1012 directory.path(),
1013 &Manifest {
1014 #[cfg(feature = "graph-cypher")]
1015 graph: None,
1016 generation: 1,
1017 log_seq: 0,
1018 segments: vec![segment],
1019 epochs: Vec::new(),
1020 epoch_alias: None,
1021 schema,
1022 },
1023 policy,
1024 )
1025 .expect("segment manifest");
1026 let vfs = Arc::new(CountingVfs::new(StdVfs));
1027
1028 let store = Store::open_with_test_dependencies(
1029 directory.path(),
1030 OpenOptions::default(),
1031 StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
1032 )
1033 .expect("store maps sealed segment");
1034
1035 assert_eq!(
1036 vfs.open_for_map_calls(),
1037 1,
1038 "sealed segment mapping bypassed Vfs::open_for_map"
1039 );
1040 store.close().expect("close mapped store");
1041 }
1042
1043 #[test]
1044 fn publish_swaps_a_whole_snapshot_while_existing_readers_keep_their_generation() {
1045 let directory = tempdir().expect("store directory");
1046 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1047 let old_reader = store.snapshot().expect("old reader");
1048
1049 store
1050 .publish_snapshot(PublishedSnapshot::empty(9))
1051 .expect("publish generation");
1052 let new_reader = store.snapshot().expect("new reader");
1053
1054 assert_eq!(old_reader.generation(), 0);
1055 assert_eq!(new_reader.generation(), 9);
1056 drop(old_reader);
1057 drop(new_reader);
1058 store.close().expect("close");
1059 }
1060
1061 #[test]
1062 fn publish_after_close_returns_the_typed_closed_error() {
1063 let directory = tempdir().expect("store directory");
1064 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
1065 store.close().expect("close");
1066
1067 let result = store.publish_snapshot(PublishedSnapshot::empty(10));
1068
1069 assert!(matches!(result, Err(StoreError::Closed)));
1070 }
1071
1072 #[test]
1073 fn seal_uses_the_opened_durable_policy_for_segment_and_manifest() {
1074 const DIMS: usize = 4;
1075
1076 let directory = tempdir().expect("store directory");
1077 let schema = Schema::new(Vec::new()).expect("timestamp-only schema");
1078 let mut builder = ColumnStoreBuilder::new(schema.clone());
1079 builder.push_row(7, &[]).expect("fixture row");
1080 let columns = builder.finish().expect("fixture columns");
1081 let alive = AliveSet::new(1);
1082 let initial_id = SegmentId::new(0x0102_0304_0506, [0x41; 10]);
1083 let codes = [0x88_u8; DIMS.div_ceil(2)];
1084 let factors = [Bit4Factors::from_persisted(1.0, 1.0, 1.0)];
1085 let rescore = [0.0_f32; DIMS];
1086 let derived = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::Ordered)
1087 .expect("derived policy");
1088 let initial = write_segment(
1089 &StdVfs,
1090 directory.path(),
1091 SegmentBuild {
1092 id: initial_id,
1093 scheme: 4,
1094 dims: DIMS as u32,
1095 codes: &codes,
1096 factors: SegmentFactors::Bit4(&factors),
1097 rescore: &rescore,
1098 columns: &columns,
1099 alive: &alive,
1100 },
1101 derived,
1102 )
1103 .expect("initial segment");
1104 commit_manifest(
1105 &StdVfs,
1106 directory.path(),
1107 &Manifest {
1108 #[cfg(feature = "graph-cypher")]
1109 graph: None,
1110 generation: 1,
1111 log_seq: 0,
1112 segments: vec![initial],
1113 epochs: Vec::new(),
1114 epoch_alias: None,
1115 schema: schema.clone(),
1116 },
1117 derived,
1118 )
1119 .expect("initial manifest");
1120 let store = Store::open(
1121 directory.path(),
1122 OpenOptions::new().with_durability(DurabilityMode::Durable, CommitTier::Durable),
1123 )
1124 .expect("durable store open");
1125 let replacement_id = SegmentId::new(0x0102_0304_0507, [0x42; 10]);
1126 let prepared = store
1127 .prepare_segment(InMemorySegment {
1128 id: replacement_id,
1129 scheme: 4,
1130 dims: DIMS as u32,
1131 codes: codes.to_vec(),
1132 factors: InMemorySegmentFactors::Bit4(factors.to_vec()),
1133 rescore: rescore.to_vec(),
1134 columns: &columns,
1135 alive: &alive,
1136 })
1137 .expect("prepare replacement");
1138 let recorder = RecordingVfs::new(StdVfs);
1139
1140 store
1141 .seal_snapshot_with_vfs(prepared, &recorder)
1142 .expect("durable seal");
1143
1144 let sync_kinds = recorder
1145 .operations()
1146 .expect("recorded seal operations")
1147 .into_iter()
1148 .filter_map(|operation| match operation {
1149 CrashOperation::Sync { kind, .. } => Some(kind),
1150 _ => None,
1151 })
1152 .collect::<Vec<_>>();
1153 assert_eq!(
1154 sync_kinds,
1155 vec![SyncKind::Full; 4],
1156 "seal must preserve the opened policy's exact SyncKind sequence"
1157 );
1158 store.close().expect("close");
1159 }
1160
1161 #[test]
1162 fn first_durable_ingest_syncs_new_wal_directory_entry_before_ack() {
1163 let directory = tempdir().expect("store directory");
1164 let recorder = Arc::new(RecordingVfs::new(StdVfs));
1165 let store = Store::open_with_test_dependencies(
1166 directory.path(),
1167 OpenOptions::new().with_durability(DurabilityMode::Durable, CommitTier::Durable),
1168 StoreTestDependencies::new(recorder.clone(), Arc::new(ManualMonotonicClock::new())),
1169 )
1170 .expect("durable store open");
1171 let operations_before_ingest = recorder.operations().expect("open operations").len();
1172
1173 store
1174 .ingest(IngestBatch::new(vec![IngestDocument::new(
1175 DocumentVersion::new(DocId::new(1), Revision::new(1)),
1176 vec![1.0, 2.0, 3.0, 4.0],
1177 )]))
1178 .expect("durable ingest ack");
1179
1180 let syncs = recorder
1181 .operations()
1182 .expect("ingest operations")
1183 .into_iter()
1184 .skip(operations_before_ingest)
1185 .filter_map(|operation| match operation {
1186 CrashOperation::Sync { path, kind } => Some((path, kind)),
1187 _ => None,
1188 })
1189 .collect::<Vec<_>>();
1190 assert_eq!(
1191 syncs,
1192 vec![
1193 (directory.path().join("wal.ze"), SyncKind::Full),
1194 (directory.path().to_path_buf(), SyncKind::Full),
1195 ],
1196 "the first durable WAL group must sync its new directory entry before ack"
1197 );
1198 store.close().expect("close");
1199 }
1200}
1201
1202#[cfg(all(test, feature = "graph-cypher"))]
1203#[allow(clippy::unwrap_used)]
1204mod unified_capacity {
1205 use super::*;
1206
1207 #[test]
1208 fn retained_readers_share_the_manifest_graph_capacity() {
1209 let directory = tempfile::tempdir().unwrap();
1210 let store = Store::open(directory.path(), super::super::OpenOptions::new()).unwrap();
1211 store.enable_graph().unwrap();
1212 let original = store.snapshot.read().unwrap().as_ref().unwrap().clone();
1213 let fork = original.fork_read_view(&store.accounting).unwrap();
1214 assert_eq!(
1215 original.graph_manifest.as_ref().unwrap().objects.as_ptr(),
1216 fork.graph_manifest.as_ref().unwrap().objects.as_ptr(),
1217 "immutable manifest inventory has one capacity owner"
1218 );
1219 drop(fork);
1220 drop(original);
1221 store.close().unwrap();
1222 }
1223}