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: Arc<Accounted<Vec<SegmentReader>>>,
305 query_segment_count: usize,
306 cancelled: AtomicBool,
307 reader_signal: Arc<ReaderSignal>,
308 _mapping_reservation: Option<Arc<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: Arc::new(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 = if probe_segment_headers {
359 super::namespace_batch::manifest_for_open(vfs, directory)?
360 } else {
361 directory.join(MANIFEST_FILE)
362 };
363 match vfs.open(&manifest_path) {
364 Ok(_) => {}
365 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
366 return Ok(Self::empty_with_signal(
367 0,
368 ReaderSignal::accounted(accounting)?,
369 ));
370 }
371 Err(source) => {
372 return Err(StoreError::Io {
373 path: manifest_path,
374 source,
375 });
376 }
377 }
378 let wal = WalReader::open(vfs, &directory.join(STORE_WAL_FILE)).map_err(StoreError::Wal)?;
379 let durable_end = wal.durable_end();
380 if !matches!(
381 wal.terminator(),
382 None | Some(crate::wal::replay::ReplayTerminator::InvalidHeader(
383 crate::wal::header::WalHeaderError::Missing
384 ))
385 ) {
386 wal.into_clean().map_err(StoreError::WalRecovery)?;
387 }
388 let manifest =
389 load_manifest(vfs, &manifest_path, durable_end).map_err(StoreError::Manifest)?;
390 if probe_segment_headers {
391 for expected in &manifest.segments {
392 let path = directory.join(expected.id.file_name());
393 validate_header_with_vfs(vfs, &path, expected).map_err(StoreError::Segment)?;
394 }
395 }
396 Self::from_manifest(vfs, directory, &manifest, accounting)
397 }
398
399 pub(crate) fn from_manifest(
400 vfs: &dyn Vfs,
401 directory: &Path,
402 manifest: &Manifest,
403 accounting: &Arc<Accounting>,
404 ) -> Result<Self, StoreError> {
405 let mut segments = Accounted::try_with_capacity(
406 accounting,
407 manifest.segments.len(),
408 AllocationComponent::Snapshot,
409 )?;
410 let query_segment_count = manifest
411 .segments
412 .iter()
413 .filter(|segment| segment_is_published(manifest.epoch_alias, segment))
414 .count();
415 let ordered_segments = manifest
416 .segments
417 .iter()
418 .filter(|segment| segment_is_published(manifest.epoch_alias, segment))
419 .chain(
420 manifest
421 .segments
422 .iter()
423 .filter(|segment| !segment_is_published(manifest.epoch_alias, segment)),
424 );
425 let collection_schema = Arc::new(manifest.schema.clone());
426 for expected in ordered_segments {
427 let path = directory.join(expected.id.file_name());
428 let reader = SegmentReader::open_accounted(
429 vfs,
430 &path,
431 expected,
432 accounting,
433 &collection_schema,
434 |allocation_bytes| {
435 let bytes = u64::try_from(allocation_bytes).map_err(|_| {
436 StoreError::BudgetExceeded {
437 needed: u64::MAX,
438 budget: u64::MAX,
439 component: "snapshot",
440 }
441 })?;
442 segments.reserve_additional_bytes(bytes)
443 },
444 )?;
445 let _ = reader.stored_text().map_err(StoreError::Segment)?;
449 segments.push(reader)?;
450 }
451 let mapped_bytes = segments.iter().try_fold(0_u64, |total, segment| {
452 let bytes =
453 u64::try_from(segment.mapped_bytes()).map_err(|_| StoreError::Statistics {
454 component: "mapped bytes",
455 source: std::io::Error::other("mapped byte count exceeds u64"),
456 })?;
457 total
458 .checked_add(bytes)
459 .ok_or_else(|| StoreError::Statistics {
460 component: "mapped bytes",
461 source: std::io::Error::other("mapped byte count overflow"),
462 })
463 })?;
464 let mapping_reservation = accounting.track_mapping(mapped_bytes)?;
465 let graph_profile = manifest.epoch_alias.map_or(
466 Err(crate::graph::search::GraphProfileError::EpochUnstamped),
467 |alias| {
468 manifest
469 .epochs
470 .iter()
471 .find(|epoch| epoch.id == alias.embedding && epoch.tokenizer == alias.tokenizer)
472 .ok_or(
473 crate::graph::search::GraphProfileError::EpochNotRegistered {
474 epoch: alias,
475 },
476 )
477 .and_then(|epoch| {
478 crate::graph::search::select_epoch_graph_profile(
479 epoch,
480 crate::graph::search::GraphDistanceMetric::SquaredL2,
481 )
482 })
483 },
484 );
485 Ok(Self {
486 generation: manifest.generation,
487 absorbed_through: manifest.log_seq,
488 epoch_alias: manifest.epoch_alias,
489 graph_profile,
490 schema: manifest.schema.clone(),
491 segments: Arc::new(segments),
492 query_segment_count,
493 cancelled: AtomicBool::new(false),
494 reader_signal: ReaderSignal::accounted(accounting)?,
495 _mapping_reservation: Some(Arc::new(mapping_reservation)),
496 #[cfg(test)]
497 release_probe: SnapshotReleaseProbe(None),
498 })
499 }
500
501 pub(super) fn fork_read_view(&self, accounting: &Arc<Accounting>) -> Result<Self, StoreError> {
503 Ok(Self {
504 generation: self.generation,
505 absorbed_through: self.absorbed_through,
506 epoch_alias: self.epoch_alias,
507 graph_profile: self.graph_profile,
508 schema: self.schema.clone(),
509 segments: Arc::clone(&self.segments),
510 query_segment_count: self.query_segment_count,
511 cancelled: AtomicBool::new(false),
512 reader_signal: ReaderSignal::accounted(accounting)?,
513 _mapping_reservation: self._mapping_reservation.clone(),
514 #[cfg(test)]
515 release_probe: SnapshotReleaseProbe(None),
516 })
517 }
518
519 #[cfg(test)]
520 pub(crate) fn set_teardown_probe(&mut self, probe: Arc<TeardownProbe>) {
521 self.release_probe.0 = Some(probe);
522 }
523
524 #[must_use]
526 pub const fn generation(&self) -> u64 {
527 self.generation
528 }
529
530 pub(crate) const fn absorbed_through(&self) -> u64 {
531 self.absorbed_through
532 }
533
534 #[cfg(any(test, feature = "test-seams"))]
536 #[doc(hidden)]
537 #[must_use]
538 pub const fn storage_absorbed_through(&self) -> u64 {
539 self.absorbed_through
540 }
541
542 pub(crate) const fn epoch_alias(&self) -> Option<crate::epoch::EpochIdentity> {
543 self.epoch_alias
544 }
545
546 pub(crate) const fn graph_profile(
547 &self,
548 ) -> Result<crate::graph::search::EpochGraphProfile, crate::graph::search::GraphProfileError>
549 {
550 self.graph_profile
551 }
552
553 pub(crate) const fn schema(&self) -> &crate::meta::Schema {
554 &self.schema
555 }
556
557 #[must_use]
559 pub fn segments(&self) -> &[SegmentReader] {
560 match self.segments.get(..self.query_segment_count) {
561 Some(segments) => segments,
562 None => &[],
563 }
564 }
565
566 pub(crate) fn all_segments(&self) -> &[SegmentReader] {
567 &self.segments
568 }
569
570 pub(crate) fn drain_readers(self: &Arc<Self>, grace: Duration) -> Result<(), StoreError> {
571 let deadline = Instant::now().checked_add(grace);
572 let mut signal =
573 self.reader_signal
574 .mutex
575 .lock()
576 .map_err(|_| StoreError::Synchronization {
577 component: "snapshot readers",
578 })?;
579 while Arc::strong_count(self) > 1 {
580 let Some(deadline) = deadline else {
581 break;
582 };
583 let now = Instant::now();
584 if now >= deadline {
585 break;
586 }
587 let remaining = deadline.saturating_duration_since(now);
588 let waited = self
589 .reader_signal
590 .changed
591 .wait_timeout(signal, remaining)
592 .map_err(|_| StoreError::Synchronization {
593 component: "snapshot readers",
594 })?;
595 signal = waited.0;
596 }
597 if Arc::strong_count(self) > 1 {
598 self.cancelled.store(true, Ordering::Release);
599 self.reader_signal.changed.notify_all();
600 }
601 while Arc::strong_count(self) > 1 {
602 signal = self.reader_signal.changed.wait(signal).map_err(|_| {
603 StoreError::Synchronization {
604 component: "snapshot readers",
605 }
606 })?;
607 }
608 drop(signal);
609 Ok(())
610 }
611
612 pub(crate) fn cancel_readers(&self) {
613 self.cancelled.store(true, Ordering::Release);
614 self.reader_signal.changed.notify_all();
615 }
616}
617
618fn segment_is_published(
619 alias: Option<crate::epoch::EpochIdentity>,
620 segment: &crate::segment::SegmentMeta,
621) -> bool {
622 alias.is_none_or(|identity| segment.epoch_id == Some(identity.embedding))
623}
624
625struct ReaderSignal {
629 mutex: Mutex<()>,
630 changed: Condvar,
631 _charge: Option<super::stats::AccountedCounter>,
632 #[cfg(test)]
633 release_observed: std::sync::atomic::AtomicUsize,
634}
635
636impl ReaderSignal {
637 fn new(charge: Option<super::stats::AccountedCounter>) -> Self {
638 Self {
639 mutex: Mutex::new(()),
640 changed: Condvar::new(),
641 _charge: charge,
642 #[cfg(test)]
643 release_observed: std::sync::atomic::AtomicUsize::new(0),
644 }
645 }
646
647 fn accounted(accounting: &Arc<Accounting>) -> Result<Arc<Self>, StoreError> {
648 let mut charge =
649 super::stats::AccountedCounter::new(accounting, AllocationComponent::Snapshot)?;
650 charge.set(std::mem::size_of::<Self>() + 2 * std::mem::size_of::<usize>())?;
651 Ok(Arc::new(Self::new(Some(charge))))
652 }
653}
654
655struct ReaderRelease {
656 signal: Arc<ReaderSignal>,
657 #[cfg(test)]
658 snapshot: std::sync::Weak<PublishedSnapshot>,
659}
660
661impl Drop for ReaderRelease {
662 fn drop(&mut self) {
663 let _guard = self
664 .signal
665 .mutex
666 .lock()
667 .unwrap_or_else(std::sync::PoisonError::into_inner);
668 #[cfg(test)]
669 self.signal
670 .release_observed
671 .store(self.snapshot.strong_count(), Ordering::Relaxed);
672 self.signal.changed.notify_all();
673 }
674}
675
676pub(super) struct ReadSnapshot {
679 snapshot: Arc<PublishedSnapshot>,
680 _release: ReaderRelease,
681}
682
683impl ReadSnapshot {
684 pub(super) fn new(snapshot: Arc<PublishedSnapshot>) -> Self {
685 let release = ReaderRelease {
686 signal: Arc::clone(&snapshot.reader_signal),
687 #[cfg(test)]
688 snapshot: Arc::downgrade(&snapshot),
689 };
690 Self {
691 snapshot,
692 _release: release,
693 }
694 }
695}
696
697impl Deref for ReadSnapshot {
698 type Target = Arc<PublishedSnapshot>;
699 fn deref(&self) -> &Self::Target {
700 &self.snapshot
701 }
702}
703
704pub struct SnapshotLease {
706 snapshot: ReadSnapshot,
707 generation: u64,
708}
709
710impl SnapshotLease {
711 pub(crate) fn new(snapshot: Arc<PublishedSnapshot>) -> Self {
712 let generation = snapshot.generation();
713 Self {
714 snapshot: ReadSnapshot::new(snapshot),
715 generation,
716 }
717 }
718
719 pub(crate) fn new_at(snapshot: Arc<PublishedSnapshot>, generation: u64) -> Self {
720 Self {
721 snapshot: ReadSnapshot::new(snapshot),
722 generation,
723 }
724 }
725
726 #[must_use]
728 pub const fn generation(&self) -> u64 {
729 self.generation
730 }
731
732 pub fn check_active(&self) -> Result<(), StoreError> {
734 if self.is_cancelled() {
735 Err(StoreError::ReadCancelled)
736 } else {
737 Ok(())
738 }
739 }
740
741 pub(crate) fn is_cancelled(&self) -> bool {
742 self.snapshot.cancelled.load(Ordering::Relaxed)
743 }
744
745 pub fn wait_for_close_cancellation(&self) -> Result<(), StoreError> {
747 let mut signal =
748 self.snapshot
749 .reader_signal
750 .mutex
751 .lock()
752 .map_err(|_| StoreError::Synchronization {
753 component: "snapshot readers",
754 })?;
755 while !self.snapshot.cancelled.load(Ordering::Acquire) {
756 signal = self
757 .snapshot
758 .reader_signal
759 .changed
760 .wait(signal)
761 .map_err(|_| StoreError::Synchronization {
762 component: "snapshot readers",
763 })?;
764 }
765 drop(signal);
766 Ok(())
767 }
768}
769
770impl Deref for SnapshotLease {
771 type Target = PublishedSnapshot;
772
773 fn deref(&self) -> &Self::Target {
774 &self.snapshot
775 }
776}
777
778#[cfg(test)]
779#[allow(clippy::expect_used)]
780mod tests {
781 use std::sync::Arc;
782 use std::sync::atomic::Ordering;
783
784 use tempfile::tempdir;
785
786 use super::{InMemorySegment, InMemorySegmentFactors, PublishedSnapshot};
787 use crate::ingest::{DocId, DocumentVersion, IngestBatch, IngestDocument, Revision};
788 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
789 use crate::lifecycle::{
790 ManualMonotonicClock, OpenOptions, Store, StoreError, StoreTestDependencies,
791 };
792 use crate::manifest::Manifest;
793 use crate::manifest::io::commit_manifest;
794 use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
795 use crate::quant::Bit4Factors;
796 use crate::segment::SegmentId;
797 use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
798 use crate::vfs::crash::{CrashOperation, RecordingVfs};
799 use crate::vfs::{CountingVfs, StdVfs, SyncKind};
800
801 #[test]
802 fn astra_05_snapshot_release_notifies_after_ownership_drops() {
803 let snapshot = Arc::new(PublishedSnapshot::empty(1));
804 let lease = super::SnapshotLease::new(snapshot.clone());
805 drop(lease);
806 assert_eq!(
807 snapshot
808 .reader_signal
809 .release_observed
810 .load(std::sync::atomic::Ordering::Relaxed),
811 1,
812 "close notification must observe only the draining owner"
813 );
814 let admission = super::ReadSnapshot::new(snapshot.clone());
817 let partition = super::SnapshotLease::new(snapshot.clone());
818 drop(partition);
819 assert_eq!(
820 snapshot
821 .reader_signal
822 .release_observed
823 .load(Ordering::Relaxed),
824 2
825 );
826 drop(admission);
827 assert_eq!(
828 snapshot
829 .reader_signal
830 .release_observed
831 .load(Ordering::Relaxed),
832 1
833 );
834 }
835
836 #[test]
837 fn sealed_segment_open_is_routed_through_vfs_open_for_map() {
838 let directory = tempdir().expect("mapped segment directory");
839 let schema = Schema::new(Vec::new()).expect("timestamp-only schema");
840 let mut builder = ColumnStoreBuilder::new(schema.clone());
841 builder.push_row(7, &[]).expect("fixture row");
842 let columns = builder.finish().expect("fixture columns");
843 let alive = AliveSet::new(1);
844 let id = SegmentId::new(0x06, [0x06; 10]);
845 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
846 .expect("derived policy");
847 let segment = write_segment(
848 &StdVfs,
849 directory.path(),
850 SegmentBuild {
851 id,
852 scheme: 4,
853 dims: 4,
854 codes: &[0_u8; 2],
855 factors: SegmentFactors::Bit4(&[Bit4Factors::from_persisted(1.0, 1.0, 1.0)]),
856 rescore: &[0.0_f32; 4],
857 columns: &columns,
858 alive: &alive,
859 },
860 policy,
861 )
862 .expect("sealed segment");
863 commit_manifest(
864 &StdVfs,
865 directory.path(),
866 &Manifest {
867 generation: 1,
868 log_seq: 0,
869 segments: vec![segment],
870 epochs: Vec::new(),
871 epoch_alias: None,
872 schema,
873 },
874 policy,
875 )
876 .expect("segment manifest");
877 let vfs = Arc::new(CountingVfs::new(StdVfs));
878
879 let store = Store::open_with_test_dependencies(
880 directory.path(),
881 OpenOptions::default(),
882 StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
883 )
884 .expect("store maps sealed segment");
885
886 assert_eq!(
887 vfs.open_for_map_calls(),
888 1,
889 "sealed segment mapping bypassed Vfs::open_for_map"
890 );
891 store.close().expect("close mapped store");
892 }
893
894 #[test]
895 fn publish_swaps_a_whole_snapshot_while_existing_readers_keep_their_generation() {
896 let directory = tempdir().expect("store directory");
897 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
898 let old_reader = store.snapshot().expect("old reader");
899
900 store
901 .publish_snapshot(PublishedSnapshot::empty(9))
902 .expect("publish generation");
903 let new_reader = store.snapshot().expect("new reader");
904
905 assert_eq!(old_reader.generation(), 0);
906 assert_eq!(new_reader.generation(), 9);
907 drop(old_reader);
908 drop(new_reader);
909 store.close().expect("close");
910 }
911
912 #[test]
913 fn publish_after_close_returns_the_typed_closed_error() {
914 let directory = tempdir().expect("store directory");
915 let store = Store::open(directory.path(), OpenOptions::default()).expect("open");
916 store.close().expect("close");
917
918 let result = store.publish_snapshot(PublishedSnapshot::empty(10));
919
920 assert!(matches!(result, Err(StoreError::Closed)));
921 }
922
923 #[test]
924 fn seal_uses_the_opened_durable_policy_for_segment_and_manifest() {
925 const DIMS: usize = 4;
926
927 let directory = tempdir().expect("store directory");
928 let schema = Schema::new(Vec::new()).expect("timestamp-only schema");
929 let mut builder = ColumnStoreBuilder::new(schema.clone());
930 builder.push_row(7, &[]).expect("fixture row");
931 let columns = builder.finish().expect("fixture columns");
932 let alive = AliveSet::new(1);
933 let initial_id = SegmentId::new(0x0102_0304_0506, [0x41; 10]);
934 let codes = [0x88_u8; DIMS.div_ceil(2)];
935 let factors = [Bit4Factors::from_persisted(1.0, 1.0, 1.0)];
936 let rescore = [0.0_f32; DIMS];
937 let derived = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::Ordered)
938 .expect("derived policy");
939 let initial = write_segment(
940 &StdVfs,
941 directory.path(),
942 SegmentBuild {
943 id: initial_id,
944 scheme: 4,
945 dims: DIMS as u32,
946 codes: &codes,
947 factors: SegmentFactors::Bit4(&factors),
948 rescore: &rescore,
949 columns: &columns,
950 alive: &alive,
951 },
952 derived,
953 )
954 .expect("initial segment");
955 commit_manifest(
956 &StdVfs,
957 directory.path(),
958 &Manifest {
959 generation: 1,
960 log_seq: 0,
961 segments: vec![initial],
962 epochs: Vec::new(),
963 epoch_alias: None,
964 schema: schema.clone(),
965 },
966 derived,
967 )
968 .expect("initial manifest");
969 let store = Store::open(
970 directory.path(),
971 OpenOptions::new().with_durability(DurabilityMode::Durable, CommitTier::Durable),
972 )
973 .expect("durable store open");
974 let replacement_id = SegmentId::new(0x0102_0304_0507, [0x42; 10]);
975 let prepared = store
976 .prepare_segment(InMemorySegment {
977 id: replacement_id,
978 scheme: 4,
979 dims: DIMS as u32,
980 codes: codes.to_vec(),
981 factors: InMemorySegmentFactors::Bit4(factors.to_vec()),
982 rescore: rescore.to_vec(),
983 columns: &columns,
984 alive: &alive,
985 })
986 .expect("prepare replacement");
987 let recorder = RecordingVfs::new(StdVfs);
988
989 store
990 .seal_snapshot_with_vfs(prepared, &recorder)
991 .expect("durable seal");
992
993 let sync_kinds = recorder
994 .operations()
995 .expect("recorded seal operations")
996 .into_iter()
997 .filter_map(|operation| match operation {
998 CrashOperation::Sync { kind, .. } => Some(kind),
999 _ => None,
1000 })
1001 .collect::<Vec<_>>();
1002 assert_eq!(
1003 sync_kinds,
1004 vec![SyncKind::Full; 4],
1005 "seal must preserve the opened policy's exact SyncKind sequence"
1006 );
1007 store.close().expect("close");
1008 }
1009
1010 #[test]
1011 fn first_durable_ingest_syncs_new_wal_directory_entry_before_ack() {
1012 let directory = tempdir().expect("store directory");
1013 let recorder = Arc::new(RecordingVfs::new(StdVfs));
1014 let store = Store::open_with_test_dependencies(
1015 directory.path(),
1016 OpenOptions::new().with_durability(DurabilityMode::Durable, CommitTier::Durable),
1017 StoreTestDependencies::new(recorder.clone(), Arc::new(ManualMonotonicClock::new())),
1018 )
1019 .expect("durable store open");
1020 let operations_before_ingest = recorder.operations().expect("open operations").len();
1021
1022 store
1023 .ingest(IngestBatch::new(vec![IngestDocument::new(
1024 DocumentVersion::new(DocId::new(1), Revision::new(1)),
1025 vec![1.0, 2.0, 3.0, 4.0],
1026 )]))
1027 .expect("durable ingest ack");
1028
1029 let syncs = recorder
1030 .operations()
1031 .expect("ingest operations")
1032 .into_iter()
1033 .skip(operations_before_ingest)
1034 .filter_map(|operation| match operation {
1035 CrashOperation::Sync { path, kind } => Some((path, kind)),
1036 _ => None,
1037 })
1038 .collect::<Vec<_>>();
1039 assert_eq!(
1040 syncs,
1041 vec![
1042 (directory.path().join("wal.ze"), SyncKind::Full),
1043 (directory.path().to_path_buf(), SyncKind::Full),
1044 ],
1045 "the first durable WAL group must sync its new directory entry before ack"
1046 );
1047 store.close().expect("close");
1048 }
1049}