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