1use std::path::Path;
4use std::sync::Arc;
5
6use crate::fts::sealed::{SealedSegment, SealedSegmentError};
7use crate::lifecycle::durability::{DurabilityPolicy, SyncRequirement};
8use crate::lifecycle::{CancelToken, Store, StoreError, StoreState};
9use crate::manifest::Manifest;
10use crate::manifest::io::{MANIFEST_FILE, commit_manifest, load_manifest};
11use crate::meta::{ColumnStore, ColumnStoreBuilder, Schema};
12use crate::segment::writer::{
13 SegmentBuild, SegmentDocumentVersions, SegmentFactors, SegmentPayloads, SegmentPostings,
14 SegmentStoredMetadata, SegmentStoredText, write_segment_with_documents_payloads,
15};
16use crate::segment::{ClusteringKeyRange, SegmentId};
17use crate::vfs::Vfs;
18use crate::wal::LogSeq;
19
20use super::ActiveState;
21
22struct SealPayloadBuffers<'a> {
23 postings: Option<Vec<u8>>,
24 metadata: Option<SegmentStoredMetadata<'a>>,
25 text: Option<SegmentStoredText<'a>>,
26}
27
28#[cfg(any(test, feature = "test-seams"))]
29type TestSealStorage = (Vec<u8>, Vec<crate::segment::layout::Int8Factors>);
30
31impl Store {
32 pub fn seal(&self) -> Result<u64, StoreError> {
37 self.seal_inner(None, self.vfs.as_ref())
38 }
39
40 pub fn seal_with_cancel(&self, cancel: &CancelToken) -> Result<u64, StoreError> {
44 self.seal_inner(Some(cancel), self.vfs.as_ref())
45 }
46
47 #[doc(hidden)]
49 pub fn seal_with_cancel_on_vfs(
50 &self,
51 cancel: &CancelToken,
52 vfs: &dyn Vfs,
53 ) -> Result<u64, StoreError> {
54 self.seal_inner(Some(cancel), vfs)
55 }
56
57 #[cfg(any(test, feature = "test-seams"))]
58 fn test_seal_storage(
59 &self,
60 current: &ActiveState,
61 dims: u32,
62 ) -> Result<Option<TestSealStorage>, StoreError> {
63 match self.vector_seal_scheme {
64 Some(crate::quant::QuantScheme::Int8) => {
65 let dimension = usize::try_from(dims).map_err(|_| StoreError::ActiveRowOverflow)?;
66 let vectors = current.segment.vectors();
67 if dimension == 0 || !vectors.len().is_multiple_of(dimension) {
68 return Err(StoreError::Segment(crate::segment::SegmentError::Geometry(
69 format!(
70 "Int8 seal vector length {} is not divisible by dimension {dimension}",
71 vectors.len()
72 ),
73 )));
74 }
75 let mut signed_codes = vec![0_i8; vectors.len()];
76 let mut factors = Vec::with_capacity(vectors.len() / dimension);
77 for (row, codes) in vectors
78 .chunks_exact(dimension)
79 .zip(signed_codes.chunks_exact_mut(dimension))
80 {
81 let (scale, offset) =
82 crate::quant::quantize_int8(row, codes).map_err(|error| {
83 StoreError::Segment(crate::segment::SegmentError::Geometry(format!(
84 "Int8 seal quantization failed: {error}"
85 )))
86 })?;
87 factors.push(crate::segment::layout::Int8Factors { scale, offset });
88 }
89 let codes = signed_codes
90 .into_iter()
91 .map(|code| code as u8)
92 .collect::<Vec<_>>();
93 Ok(Some((codes, factors)))
94 }
95 Some(crate::quant::QuantScheme::Bit4) | None => Ok(None),
96 Some(scheme) => Err(StoreError::Segment(crate::segment::SegmentError::Geometry(
97 format!("unsupported test vector seal scheme {scheme:?}"),
98 ))),
99 }
100 }
101
102 fn abort_seal(
103 &self,
104 vfs: &dyn Vfs,
105 id: SegmentId,
106 _invocation: Option<u64>,
107 _current: &ActiveState,
108 _absorbed_through: u64,
109 ) -> Result<(), StoreError> {
110 cleanup_uncommitted_segment(vfs, &self.directory, id, self.durability_policy)?;
111 #[cfg(any(test, feature = "test-seams"))]
112 if let Some(invocation_id) = _invocation {
113 let active_rows = u64::try_from(_current.segment.row_count())
114 .map_err(|_| StoreError::ActiveRowOverflow)?;
115 if let Some(controller) = self.ingest_retention_fault_controller.as_ref() {
116 controller
117 .push_receipt(super::IngestRetentionFaultReceiptV1::seal_cancellation(
118 invocation_id,
119 active_rows,
120 _absorbed_through,
121 *id.as_bytes(),
122 ))
123 .map_err(|_| StoreError::Synchronization {
124 component: "ingest-retention controller",
125 })?;
126 }
127 }
128 Ok(())
129 }
130
131 fn seal_inner(&self, cancel: Option<&CancelToken>, vfs: &dyn Vfs) -> Result<u64, StoreError> {
132 let state = self
133 .state
134 .lock()
135 .map_err(|_| StoreError::Synchronization { component: "state" })?;
136 match *state {
137 StoreState::Open => {}
138 StoreState::Closing => return Err(StoreError::Closing),
139 StoreState::Closed => return Err(StoreError::Closed),
140 }
141 let writer_lock = self
142 .writer_lock
143 .lock()
144 .map_err(|_| StoreError::Synchronization {
145 component: "writer lock",
146 })?;
147 if writer_lock.is_none() {
148 return Err(StoreError::ReadOnly);
149 }
150 check_cancelled(cancel)?;
151 let mut wal = self
152 .wal_writer
153 .lock()
154 .map_err(|_| StoreError::Synchronization {
155 component: "WAL writer",
156 })?;
157 let writer = wal.as_mut().ok_or(StoreError::ReadOnly)?;
158 let absorbed_through = writer.durable_end();
159 let mut active = self
160 .active
161 .lock()
162 .map_err(|_| StoreError::Synchronization {
163 component: "active segment",
164 })?;
165 let current = active.as_ref().ok_or(StoreError::Closed)?;
166 if current.segment.is_empty() {
167 return Ok(current.generation);
170 }
171 let generation = current
172 .generation
173 .checked_add(1)
174 .ok_or(StoreError::GenerationOverflow)?;
175 let manifest = load_current_manifest(
176 vfs,
177 &self.directory,
178 absorbed_through,
179 current.generation,
180 &self.schema,
181 )?;
182 let columns = active_columns(&manifest.schema, ¤t.segment, cancel)?;
183 let alive = current.segment.alive()?;
184 let clustering_key_range = clustering_key_range(current.segment.timestamps(), &alive)?;
185 let dims = current
186 .segment
187 .dims()
188 .ok_or(StoreError::ActiveRowOverflow)?;
189 let dims = u32::try_from(dims).map_err(|_| StoreError::ActiveRowOverflow)?;
190 let id = seal_id(generation, absorbed_through);
191 #[cfg(any(test, feature = "test-seams"))]
192 let int8_storage = self.test_seal_storage(current, dims)?;
193 #[cfg(any(test, feature = "test-seams"))]
194 let (scheme, codes, factors) = match int8_storage.as_ref() {
195 Some((codes, factors)) => (
196 u16::from(crate::quant::QuantScheme::Int8.id()),
197 codes.as_slice(),
198 SegmentFactors::Int8(factors.as_slice()),
199 ),
200 None => (
201 u16::from(crate::quant::QuantScheme::Bit4.id()),
202 current.segment.codes(),
203 SegmentFactors::Bit4(current.segment.factors()),
204 ),
205 };
206 #[cfg(not(any(test, feature = "test-seams")))]
207 let (scheme, codes, factors) = (
208 u16::from(crate::quant::QuantScheme::Bit4.id()),
209 current.segment.codes(),
210 SegmentFactors::Bit4(current.segment.factors()),
211 );
212 let build = SegmentBuild {
213 id,
214 scheme,
215 dims,
216 codes,
217 factors,
218 rescore: current.segment.vectors(),
219 columns: &columns,
220 alive: &alive,
221 };
222 let documents = SegmentDocumentVersions {
223 doc_ids: current.segment.doc_ids(),
224 revisions: current.segment.revisions(),
225 };
226 let payloads = build_seal_payloads(¤t.segment)?;
227 let written = write_segment_with_documents_payloads(
228 vfs,
229 &self.directory,
230 build,
231 SegmentPayloads {
232 documents,
233 metadata: payloads.metadata,
234 text: payloads.text,
235 postings: payloads
236 .postings
237 .as_deref()
238 .map(|bytes| SegmentPostings { bytes }),
239 },
240 self.durability_policy,
241 );
242 let mut meta = match written {
243 Ok(meta) => meta,
244 Err(error) => {
245 cleanup_uncommitted_segment(vfs, &self.directory, id, self.durability_policy)?;
246 return Err(StoreError::Segment(error));
247 }
248 };
249 meta.clustering_key_range = clustering_key_range;
250 meta.epoch_id = manifest.epoch_alias.map(|identity| identity.embedding);
251 #[cfg(any(test, feature = "test-seams"))]
252 let forced_late_cancellation =
253 match self.ingest_retention_fault_controller.as_ref() {
254 Some(controller) => controller.seal_cancellation_plan().map_err(|_| {
255 StoreError::Synchronization {
256 component: "ingest-retention controller",
257 }
258 })?,
259 None => None,
260 };
261 #[cfg(not(any(test, feature = "test-seams")))]
262 let forced_late_cancellation: Option<u64> = None;
263 if forced_late_cancellation.is_some() || check_cancelled(cancel).is_err() {
264 self.abort_seal(vfs, id, forced_late_cancellation, current, absorbed_through)?;
265 return Err(StoreError::SealCancelled);
266 }
267 let mut segments = manifest.segments;
268 segments.push(meta);
269 let epochs = self.epoch_registry(&manifest.epochs);
270 commit_manifest(
271 vfs,
272 &self.directory,
273 &Manifest {
274 generation,
275 log_seq: absorbed_through,
276 segments,
277 epochs,
278 epoch_alias: manifest.epoch_alias,
279 schema: manifest.schema,
280 },
281 self.durability_policy,
282 )
283 .map_err(StoreError::Manifest)?;
284 let remapped = crate::lifecycle::PublishedSnapshot::load_on_vfs(
285 &self.directory,
286 &self.accounting,
287 vfs,
288 )?;
289 let mut published = self
290 .snapshot
291 .write()
292 .map_err(|_| StoreError::Synchronization {
293 component: "published snapshot",
294 })?;
295 let previous = published.replace(Arc::new(remapped));
296 drop(published);
297 drop(previous);
298 *active = Some(ActiveState::empty(generation));
299 writer.retire_visible_through(LogSeq::new(absorbed_through))?;
300 writer.truncate_absorbed(
303 vfs,
304 &self.directory,
305 self.durability_policy,
306 LogSeq::new(absorbed_through),
307 )?;
308 drop(active);
309 drop(wal);
310 drop(writer_lock);
311 drop(state);
312 Ok(generation)
313 }
314}
315
316fn build_seal_payloads(
317 segment: &super::ActiveSegment,
318) -> Result<SealPayloadBuffers<'_>, StoreError> {
319 let postings = if segment.has_text() {
320 let sealed = SealedSegment::seal(segment.lexical())
321 .map_err(SealedSegmentError::from)
322 .map_err(crate::segment::SegmentError::from)
323 .map_err(StoreError::Segment)?;
324 Some(
325 sealed
326 .encode_region()
327 .map_err(crate::segment::SegmentError::from)
328 .map_err(StoreError::Segment)?,
329 )
330 } else {
331 None
332 };
333 let metadata = (!segment.metadata_bytes().is_empty()).then_some(SegmentStoredMetadata {
334 end_offsets: segment.metadata_end_offsets(),
335 bytes: segment.metadata_bytes(),
336 });
337 let text = segment.has_text().then_some(SegmentStoredText {
338 present: segment.text_present(),
339 end_offsets: segment.text_end_offsets(),
340 bytes: segment.text_bytes(),
341 });
342 Ok(SealPayloadBuffers {
343 postings,
344 metadata,
345 text,
346 })
347}
348
349pub(super) fn load_current_manifest(
350 vfs: &dyn Vfs,
351 directory: &Path,
352 durable_end: u64,
353 generation: u64,
354 schema: &Schema,
355) -> Result<Manifest, StoreError> {
356 let path = directory.join(MANIFEST_FILE);
357 match vfs.open(&path) {
358 Ok(_) => load_manifest(vfs, &path, durable_end).map_err(StoreError::Manifest),
359 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Manifest {
360 generation,
361 log_seq: 0,
362 segments: Vec::new(),
363 epochs: Vec::new(),
364 epoch_alias: None,
365 schema: schema.clone(),
366 }),
367 Err(source) => Err(StoreError::Io { path, source }),
368 }
369}
370
371fn active_columns(
372 schema: &Schema,
373 active: &super::ActiveSegment,
374 cancel: Option<&CancelToken>,
375) -> Result<ColumnStore, StoreError> {
376 let mut builder = ColumnStoreBuilder::new(schema.clone());
377 for (row, timestamp) in active.timestamps().iter().copied().enumerate() {
378 if row.is_multiple_of(64) {
379 check_cancelled(cancel)?;
380 }
381 let values = active.column_values(row)?;
382 builder
383 .push_row(timestamp, &super::column_inputs(&values))
384 .map_err(|error| {
385 StoreError::Segment(crate::segment::SegmentError::Columns(error.to_string()))
386 })?;
387 }
388 builder.finish().map_err(|error| {
389 StoreError::Segment(crate::segment::SegmentError::Columns(error.to_string()))
390 })
391}
392
393fn clustering_key_range(
394 timestamps: &[i64],
395 alive: &crate::meta::AliveSet,
396) -> Result<ClusteringKeyRange, StoreError> {
397 let mut bounds: Option<(i64, i64)> = None;
398 for row in alive.iter_alive() {
399 let index = usize::try_from(row).map_err(|_| StoreError::ActiveRowOverflow)?;
400 let timestamp = timestamps
401 .get(index)
402 .copied()
403 .ok_or(StoreError::ActiveRowOverflow)?;
404 bounds = Some(match bounds {
405 None => (timestamp, timestamp),
406 Some((minimum, maximum)) => (minimum.min(timestamp), maximum.max(timestamp)),
407 });
408 }
409 Ok(match bounds {
410 Some((min_ts, max_ts)) => ClusteringKeyRange::Bounded { min_ts, max_ts },
411 None => ClusteringKeyRange::Empty,
414 })
415}
416
417fn check_cancelled(cancel: Option<&CancelToken>) -> Result<(), StoreError> {
418 if cancel.is_some_and(CancelToken::is_cancelled) {
419 Err(StoreError::SealCancelled)
420 } else {
421 Ok(())
422 }
423}
424
425fn seal_id(generation: u64, absorbed_through: u64) -> SegmentId {
426 let mut bytes = [0_u8; 16];
427 if let Some(prefix) = bytes.get_mut(..8) {
428 prefix.copy_from_slice(&generation.to_be_bytes());
429 }
430 if let Some(suffix) = bytes.get_mut(8..) {
431 suffix.copy_from_slice(&absorbed_through.to_be_bytes());
432 }
433 SegmentId::from_bytes(bytes)
434}
435
436fn cleanup_uncommitted_segment(
437 vfs: &dyn Vfs,
438 directory: &Path,
439 id: SegmentId,
440 policy: DurabilityPolicy,
441) -> Result<(), StoreError> {
442 let final_path = directory.join(id.file_name());
443 let temporary_path = directory.join(format!(".{}.tmp", id.file_name()));
444 let mut deleted = false;
445 for path in [temporary_path, final_path] {
446 match vfs.open(&path) {
447 Ok(_) => {
448 vfs.delete(&path)
449 .map_err(|source| StoreError::Io { path, source })?;
450 deleted = true;
451 }
452 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
453 Err(source) => return Err(StoreError::Io { path, source }),
454 }
455 }
456 if deleted && let SyncRequirement::Sync(kind) = policy.directory_sync() {
457 vfs.sync(directory, kind).map_err(|source| StoreError::Io {
458 path: directory.to_path_buf(),
459 source,
460 })?;
461 }
462 Ok(())
463}
464
465#[cfg(test)]
466#[allow(clippy::expect_used, clippy::panic)]
467mod wal_truncation_crash_tests {
468 use std::path::Path;
469 use std::sync::Arc;
470
471 use crate::ingest::StoreWal;
472 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
473 use crate::lifecycle::stats::Accounting;
474 use crate::vfs::crash::{CrashVfs, MemoryVfs};
475 use crate::wal::{LogSeq, WalReader, encode_wal_image};
476
477 #[test]
482 fn every_crash_state_of_the_wal_truncation_resumes_after_the_boundary() {
483 let directory = Path::new("/wal-truncation");
484 let wal = directory.join("wal.ze");
485 let records = (0..3_u8)
486 .map(|index| (99, vec![index; 8]))
487 .collect::<Vec<_>>();
488 let memory = MemoryVfs::new();
489 memory
490 .insert(
491 wal.clone(),
492 encode_wal_image(LogSeq::new(1), &records).expect("old WAL"),
493 )
494 .expect("seed old WAL");
495 let crash = CrashVfs::new(memory).expect("crash recorder");
496 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::Ordered)
497 .expect("ordered policy");
498 let accounting = Arc::new(Accounting::new(u64::MAX, u64::MAX));
499 let recovered = WalReader::open(&crash, &wal)
500 .expect("open old WAL")
501 .into_clean()
502 .expect("clean old WAL");
503 let mut writer = StoreWal::resume(&crash, &wal, recovered, policy, 3, &accounting)
504 .expect("resume old WAL");
505
506 writer
507 .truncate_absorbed(&crash, directory, policy, LogSeq::new(3))
508 .expect("truncate");
509
510 let states = crash.crash_states().expect("crash states");
511 assert!(!states.was_capped());
512 assert!(states.len() > 10, "only {} crash states", states.len());
513 for state in states.iter() {
514 let recovered = WalReader::open(state.vfs(), &wal)
515 .expect("read WAL")
516 .into_clean()
517 .unwrap_or_else(|error| panic!("{:?}: WAL unreadable: {error}", state.kind()));
518 let sequences = recovered
519 .records()
520 .iter()
521 .map(|record| record.seq.get())
522 .collect::<Vec<_>>();
523 assert!(
524 sequences == [1, 2, 3] || sequences.is_empty(),
525 "{:?}: WAL holds {sequences:?}",
526 state.kind()
527 );
528 let mut resumed =
529 StoreWal::resume(state.vfs(), &wal, recovered, policy, 3, &accounting)
530 .unwrap_or_else(|error| panic!("{:?}: resume: {error}", state.kind()));
531 assert_eq!(
532 resumed.commit(99, b"next").expect("commit after crash"),
533 LogSeq::new(4),
534 "{:?}",
535 state.kind()
536 );
537 }
538 }
539}
540
541#[cfg(test)]
542#[path = "seal_id_tests.rs"]
543mod seal_id_tests;