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-support"))]
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-support"))]
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-support"))]
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-support"))]
192 let int8_storage = self.test_seal_storage(current, dims)?;
193 #[cfg(any(test, feature = "test-support"))]
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-support")))]
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-support"))]
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-support")))]
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 drop(active);
301 drop(wal);
302 drop(writer_lock);
303 drop(state);
304 Ok(generation)
305 }
306}
307
308fn build_seal_payloads(
309 segment: &super::ActiveSegment,
310) -> Result<SealPayloadBuffers<'_>, StoreError> {
311 let postings = if segment.has_text() {
312 let sealed = SealedSegment::seal(segment.lexical())
313 .map_err(SealedSegmentError::from)
314 .map_err(crate::segment::SegmentError::from)
315 .map_err(StoreError::Segment)?;
316 Some(
317 sealed
318 .encode_region()
319 .map_err(crate::segment::SegmentError::from)
320 .map_err(StoreError::Segment)?,
321 )
322 } else {
323 None
324 };
325 let metadata = (!segment.metadata_bytes().is_empty()).then_some(SegmentStoredMetadata {
326 end_offsets: segment.metadata_end_offsets(),
327 bytes: segment.metadata_bytes(),
328 });
329 let text = segment.has_text().then_some(SegmentStoredText {
330 present: segment.text_present(),
331 end_offsets: segment.text_end_offsets(),
332 bytes: segment.text_bytes(),
333 });
334 Ok(SealPayloadBuffers {
335 postings,
336 metadata,
337 text,
338 })
339}
340
341fn load_current_manifest(
342 vfs: &dyn Vfs,
343 directory: &Path,
344 durable_end: u64,
345 generation: u64,
346 schema: &Schema,
347) -> Result<Manifest, StoreError> {
348 let path = directory.join(MANIFEST_FILE);
349 match vfs.open(&path) {
350 Ok(_) => load_manifest(vfs, &path, durable_end).map_err(StoreError::Manifest),
351 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Manifest {
352 generation,
353 log_seq: 0,
354 segments: Vec::new(),
355 epochs: Vec::new(),
356 epoch_alias: None,
357 schema: schema.clone(),
358 }),
359 Err(source) => Err(StoreError::Io { path, source }),
360 }
361}
362
363fn active_columns(
364 schema: &Schema,
365 active: &super::ActiveSegment,
366 cancel: Option<&CancelToken>,
367) -> Result<ColumnStore, StoreError> {
368 let mut builder = ColumnStoreBuilder::new(schema.clone());
369 for (row, timestamp) in active.timestamps().iter().copied().enumerate() {
370 if row.is_multiple_of(64) {
371 check_cancelled(cancel)?;
372 }
373 let values = active.column_values(row)?;
374 builder
375 .push_row(timestamp, &super::column_inputs(&values))
376 .map_err(|error| {
377 StoreError::Segment(crate::segment::SegmentError::Columns(error.to_string()))
378 })?;
379 }
380 builder.finish().map_err(|error| {
381 StoreError::Segment(crate::segment::SegmentError::Columns(error.to_string()))
382 })
383}
384
385fn clustering_key_range(
386 timestamps: &[i64],
387 alive: &crate::meta::AliveSet,
388) -> Result<ClusteringKeyRange, StoreError> {
389 let mut bounds: Option<(i64, i64)> = None;
390 for row in alive.iter_alive() {
391 let index = usize::try_from(row).map_err(|_| StoreError::ActiveRowOverflow)?;
392 let timestamp = timestamps
393 .get(index)
394 .copied()
395 .ok_or(StoreError::ActiveRowOverflow)?;
396 bounds = Some(match bounds {
397 None => (timestamp, timestamp),
398 Some((minimum, maximum)) => (minimum.min(timestamp), maximum.max(timestamp)),
399 });
400 }
401 Ok(match bounds {
402 Some((min_ts, max_ts)) => ClusteringKeyRange::Bounded { min_ts, max_ts },
403 None => ClusteringKeyRange::Empty,
406 })
407}
408
409fn check_cancelled(cancel: Option<&CancelToken>) -> Result<(), StoreError> {
410 if cancel.is_some_and(CancelToken::is_cancelled) {
411 Err(StoreError::SealCancelled)
412 } else {
413 Ok(())
414 }
415}
416
417fn seal_id(generation: u64, absorbed_through: u64) -> SegmentId {
418 let mut bytes = [0_u8; 16];
419 if let Some(prefix) = bytes.get_mut(..8) {
420 prefix.copy_from_slice(&generation.to_be_bytes());
421 }
422 if let Some(suffix) = bytes.get_mut(8..) {
423 suffix.copy_from_slice(&absorbed_through.to_be_bytes());
424 }
425 SegmentId::from_bytes(bytes)
426}
427
428fn cleanup_uncommitted_segment(
429 vfs: &dyn Vfs,
430 directory: &Path,
431 id: SegmentId,
432 policy: DurabilityPolicy,
433) -> Result<(), StoreError> {
434 let final_path = directory.join(id.file_name());
435 let temporary_path = directory.join(format!(".{}.tmp", id.file_name()));
436 let mut deleted = false;
437 for path in [temporary_path, final_path] {
438 match vfs.open(&path) {
439 Ok(_) => {
440 vfs.delete(&path)
441 .map_err(|source| StoreError::Io { path, source })?;
442 deleted = true;
443 }
444 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
445 Err(source) => return Err(StoreError::Io { path, source }),
446 }
447 }
448 if deleted && let SyncRequirement::Sync(kind) = policy.directory_sync() {
449 vfs.sync(directory, kind).map_err(|source| StoreError::Io {
450 path: directory.to_path_buf(),
451 source,
452 })?;
453 }
454 Ok(())
455}