1use std::cell::RefCell;
7use std::collections::HashSet;
8use std::fs::{File, OpenOptions};
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU8, Ordering};
11use std::sync::Arc;
12
13use parking_lot::RwLock;
14
15use super::{
16 kernels::dot, vertex::SearchScratch, BuildParams, Graph, Metric, Residency, VGraph,
17 VSearchParams,
18};
19use crate::core::durable_file::{atomic_write, sync_directory, sync_mapped_file};
20use crate::core::error::HmsError;
21
22mod delta;
23mod format;
24use delta::Delta;
25use format::BaseRows;
26
27const MAX_DIM: usize = 1 << 20;
28const MAX_PAYLOAD: usize = 64 << 20;
29const MAX_OPS: usize = 65536;
30
31#[derive(Debug, thiserror::Error)]
33pub enum UpdateError {
34 #[error("unknown vector id {0}")]
35 UnknownId(u32),
36 #[error("vector id {0} already exists")]
37 AlreadyExists(u32),
38 #[error("id {0} occurs twice in one batch")]
39 DuplicateIdInBatch(u32),
40 #[error("expected dimension {expected}, got {actual}")]
41 DimensionMismatch { expected: usize, actual: usize },
42 #[error("invalid vector: {0}")]
43 InvalidVector(&'static str),
44 #[error("id u32::MAX is reserved")]
45 IdOutOfRange,
46 #[error("vector index allocation or batch limit exceeded")]
47 Capacity,
48 #[error("corrupt vector log at byte {offset}: {reason}")]
49 CorruptLog { offset: u64, reason: String },
50 #[error("corrupt vector base: {0}")]
51 CorruptBase(String),
52 #[error("unsupported vector format: {0}")]
53 UnsupportedFormat(&'static str),
54 #[error("another writer is active")]
55 WriterBusy,
56 #[error("index is already open by a writer")]
57 Locked,
58 #[error("write outcome uncertain; reopen the index before further writes")]
59 RecoveryRequired,
60 #[error(transparent)]
61 Io(#[from] std::io::Error),
62 #[error(transparent)]
63 Persistence(#[from] anyhow::Error),
64 #[error(transparent)]
65 Search(#[from] HmsError),
66}
67
68#[derive(Clone, Copy, Debug)]
70pub struct ConsolidationPolicy {
71 pub delta_ratio: f64,
72 pub tombstone_ratio: f64,
73 pub min_delta: usize,
74}
75impl Default for ConsolidationPolicy {
76 fn default() -> Self {
77 Self {
78 delta_ratio: 0.1,
79 tombstone_ratio: 0.2,
80 min_delta: 1024,
81 }
82 }
83}
84
85#[derive(Clone)]
86pub(super) struct Encoding {
87 pub(super) bits: u8,
88 pub(super) residual_bits: u8,
89 pub(super) align: bool,
90 pub(super) reorder: bool,
91 pub(super) id_bytes: u8,
92 pub(super) screen: bool,
93 pub(super) block_bits: u8,
94}
95impl Default for Encoding {
96 fn default() -> Self {
97 Self {
98 bits: 8,
99 residual_bits: 8,
100 align: true,
101 reorder: false,
102 id_bytes: 4,
103 screen: false,
104 block_bits: 0,
105 }
106 }
107}
108#[derive(Clone)]
109struct Manifest {
110 identity: [u8; 16],
111 dim: usize,
112 sequence: u64,
113 build: BuildParams,
114 encoding: Encoding,
115}
116impl Manifest {
117 fn vertex_path(&self, dir: &Path) -> PathBuf {
118 dir.join(format!("base-{}.vertex", self.sequence))
119 }
120 fn rows_path(&self, dir: &Path) -> PathBuf {
121 dir.join(format!("rows-{}.bin", self.sequence))
122 }
123}
124
125#[derive(Clone)]
126enum Operation {
127 Insert(u32, Vec<f32>),
128 Delete(u32),
129 Replace(u32, Vec<f32>),
130}
131impl Operation {
132 fn id(&self) -> u32 {
133 match self {
134 Self::Insert(id, _) | Self::Delete(id) | Self::Replace(id, _) => *id,
135 }
136 }
137 fn tag(&self) -> u8 {
138 match self {
139 Self::Insert(..) => 1,
140 Self::Delete(..) => 2,
141 Self::Replace(..) => 3,
142 }
143 }
144 fn vector(&self) -> Option<&[f32]> {
145 match self {
146 Self::Insert(_, v) | Self::Replace(_, v) => Some(v),
147 Self::Delete(_) => None,
148 }
149 }
150}
151struct Base {
152 graph: Option<VGraph>,
153 rows: BaseRows,
154 dense_ids: bool,
155}
156impl Base {
157 fn open(dir: &Path, m: &Manifest) -> Result<Self, UpdateError> {
158 let rows = BaseRows::open(&m.rows_path(dir), m)?;
159 let graph = if rows.ids.is_empty() {
160 None
161 } else {
162 let graph = VGraph::open(&m.vertex_path(dir), Residency::Mapped)?;
163 if graph.len() != rows.ids.len()
164 || graph.dim() != m.dim
165 || graph.metric() != m.build.metric
166 {
167 return Err(UpdateError::CorruptBase(
168 "vertex/rows shape mismatch".into(),
169 ));
170 }
171 Some(graph)
172 };
173 let dense_ids = rows.ids.iter().enumerate().all(|(i, &id)| i == id as usize);
174 Ok(Self {
175 graph,
176 rows,
177 dense_ids,
178 })
179 }
180}
181#[derive(Clone)]
182struct State {
183 base: Arc<Base>,
184 tombstones: Vec<u64>,
185 deleted: usize,
186 delta: Delta,
187 pending: Vec<Operation>,
188 touched: HashSet<u32>,
189 pending_bytes: usize,
190 manifest: Manifest,
191 sequence: u64,
192 log_end: u64,
193}
194impl State {
195 fn new(base: Base, manifest: Manifest) -> Self {
196 Self {
197 tombstones: vec![0; base.rows.ids.len().div_ceil(64)],
198 base: Arc::new(base),
199 deleted: 0,
200 delta: Delta::default(),
201 pending: Vec::new(),
202 touched: HashSet::new(),
203 pending_bytes: 0,
204 sequence: manifest.sequence,
205 manifest,
206 log_end: format::LOG_HEADER,
207 }
208 }
209 fn dead(&self, slot: usize) -> bool {
210 self.tombstones[slot / 64] & (1 << (slot % 64)) != 0
211 }
212 fn base_slot(&self, id: u32) -> Option<usize> {
213 self.base
214 .rows
215 .ids
216 .binary_search(&id)
217 .ok()
218 .filter(|&s| !self.dead(s))
219 }
220 fn contains(&self, id: u32) -> bool {
221 self.delta.live.contains_key(&id) || self.base_slot(id).is_some()
222 }
223 fn len(&self) -> usize {
224 self.base.rows.ids.len() - self.deleted + self.delta.live.len()
225 }
226 fn validate_op(&self, op: &Operation) -> Result<(), UpdateError> {
227 if op.id() == u32::MAX {
228 return Err(UpdateError::IdOutOfRange);
229 }
230 if let Some(v) = op.vector() {
231 validate_vector(v, self.manifest.dim, self.manifest.build.metric)?;
232 }
233 match op {
234 Operation::Insert(id, _) if self.contains(*id) => Err(UpdateError::AlreadyExists(*id)),
235 Operation::Delete(id) | Operation::Replace(id, _) if !self.contains(*id) => {
236 Err(UpdateError::UnknownId(*id))
237 }
238 _ => Ok(()),
239 }
240 }
241 fn apply(&mut self, op: &Operation) {
242 let id = op.id();
243 if !matches!(op, Operation::Insert(..)) {
244 self.delta.live.remove(&id);
245 if let Some(slot) = self.base_slot(id) {
246 self.tombstones[slot / 64] |= 1 << (slot % 64);
247 self.deleted += 1;
248 }
249 }
250 if let Some(row) = op.vector() {
251 let mut row = row.to_vec();
252 super::prepare_rows(&mut row, self.manifest.dim, self.manifest.build.metric);
253 self.delta.insert(id, &row, &self.manifest.build);
254 }
255 }
256 fn replay(&mut self, ops: Vec<Operation>) -> Result<(), UpdateError> {
257 for op in &ops {
258 self.validate_op(op)?;
259 }
260 for op in &ops {
261 self.apply(op);
262 }
263 Ok(())
264 }
265}
266
267thread_local! { static SCRATCH: RefCell<Option<SearchScratch>> = const { RefCell::new(None) }; }
268
269pub struct UpdatableVGraph {
273 dir: PathBuf,
274 _lock: IndexLock,
275 writer: AtomicU8,
276 state: RwLock<State>,
277}
278struct Writer<'a>(&'a AtomicU8);
279impl Writer<'_> {
280 fn uncertain(&self) {
281 self.0.store(2, Ordering::Release);
282 }
283}
284impl Drop for Writer<'_> {
285 fn drop(&mut self) {
286 let _ = self
287 .0
288 .compare_exchange(1, 0, Ordering::Release, Ordering::Relaxed);
289 }
290}
291struct IndexLock(File);
292impl Drop for IndexLock {
293 fn drop(&mut self) {
294 if let Err(error) = fs2::FileExt::unlock(&self.0) {
296 tracing::warn!(%error, "failed to release vector index lock");
297 }
298 }
299}
300fn lock(dir: &Path) -> Result<IndexLock, UpdateError> {
301 let file = OpenOptions::new()
302 .create(true)
303 .truncate(false)
304 .read(true)
305 .write(true)
306 .open(dir.join(".lock"))?;
307 fs2::FileExt::try_lock_exclusive(&file).map_err(|e| {
308 if e.kind() == std::io::ErrorKind::WouldBlock
310 || e.raw_os_error() == fs2::lock_contended_error().raw_os_error()
311 {
312 UpdateError::Locked
313 } else {
314 UpdateError::Io(e)
315 }
316 })?;
317 Ok(IndexLock(file))
318}
319fn validate_vector(row: &[f32], dim: usize, metric: Metric) -> Result<(), UpdateError> {
320 if row.len() != dim {
321 return Err(UpdateError::DimensionMismatch {
322 expected: dim,
323 actual: row.len(),
324 });
325 }
326 if row.iter().any(|v| !v.is_finite()) {
327 return Err(UpdateError::InvalidVector("non-finite coordinate"));
328 }
329 if !dot(row, row).is_finite() {
330 return Err(UpdateError::InvalidVector("squared norm overflows f32"));
331 }
332 if metric == Metric::Angular && row.iter().all(|&v| v == 0.0) {
333 return Err(UpdateError::InvalidVector("zero angular vector"));
334 }
335 Ok(())
336}
337fn distance(q: &[f32], row: &[f32], metric: Metric) -> f32 {
338 match metric {
339 Metric::Angular => -dot(q, row),
340 Metric::Euclidean => dot(row, row) * 0.5 - dot(q, row),
341 }
342}
343
344fn widened_width(requested: usize, allocated: usize, live: usize) -> usize {
345 ((requested.min(allocated) as u128 * allocated as u128).div_ceil(live as u128))
346 .min(allocated as u128) as usize
347}
348
349impl UpdatableVGraph {
350 pub fn create(
352 path: &Path,
353 data: &[f32],
354 dim: usize,
355 params: &BuildParams,
356 ) -> Result<Self, UpdateError> {
357 Self::create_with_encoding(path, data, dim, params, Encoding::default())
358 }
359 fn create_with_encoding(
360 path: &Path,
361 data: &[f32],
362 dim: usize,
363 params: &BuildParams,
364 encoding: Encoding,
365 ) -> Result<Self, UpdateError> {
366 if dim == 0
367 || dim > MAX_DIM
368 || !data.len().is_multiple_of(dim)
369 || data.len() / dim >= u32::MAX as usize
370 {
371 return Err(UpdateError::Capacity);
372 }
373 let manifest = Manifest {
374 dim,
375 identity: rand::random(),
376 sequence: 0,
377 build: params.clone(),
378 encoding,
379 };
380 Manifest::decode(&manifest.encode())?;
381 for row in data.chunks_exact(dim) {
382 validate_vector(row, dim, params.metric)?;
383 }
384 std::fs::create_dir(path)?;
385 let file = lock(path)?;
386 let mut vectors = data.to_vec();
387 super::prepare_rows(&mut vectors, dim, params.metric);
388 let ids: Vec<u32> = (0..(data.len() / dim) as u32).collect();
389 write_base(path, &manifest, &ids, vectors)?;
390 format::rotate(&path.join("updates.log"), &manifest)?;
391 atomic_write(&path.join("CURRENT"), &manifest.encode())?;
392 sync_directory(
393 path.parent()
394 .filter(|p| !p.as_os_str().is_empty())
395 .unwrap_or_else(|| Path::new(".")),
396 )?;
397 let base = Base::open(path, &manifest)?;
398 Ok(Self {
399 dir: path.to_path_buf(),
400 _lock: file,
401 writer: AtomicU8::new(0),
402 state: RwLock::new(State::new(base, manifest)),
403 })
404 }
405 pub fn create_from_graph(
407 path: &Path,
408 graph: &Graph,
409 vertex: &VGraph,
410 ) -> Result<Self, UpdateError> {
411 if graph.n != vertex.len() || graph.dim != vertex.dim() || graph.metric != vertex.metric() {
412 return Err(UpdateError::CorruptBase(
413 "graph and vertex encoding differ".into(),
414 ));
415 }
416 let manifest = Manifest {
417 identity: rand::random(),
418 dim: graph.dim,
419 sequence: 0,
420 build: graph.build.clone(),
421 encoding: vertex.update_encoding(),
422 };
423 Manifest::decode(&manifest.encode())?;
424 for row in graph.vectors.chunks_exact(graph.dim) {
425 validate_vector(row, graph.dim, graph.metric)?;
426 }
427 std::fs::create_dir(path)?;
428 let file = lock(path)?;
429 vertex.save(&manifest.vertex_path(path))?;
430 let ids: Vec<_> = (0..graph.n as u32).collect();
431 BaseRows::write(&manifest.rows_path(path), &manifest, &ids, &graph.vectors)?;
432 format::rotate(&path.join("updates.log"), &manifest)?;
433 atomic_write(&path.join("CURRENT"), &manifest.encode())?;
434 sync_directory(
435 path.parent()
436 .filter(|p| !p.as_os_str().is_empty())
437 .unwrap_or_else(|| Path::new(".")),
438 )?;
439 let base = Base::open(path, &manifest)?;
440 Ok(Self {
441 dir: path.to_path_buf(),
442 _lock: file,
443 writer: AtomicU8::new(0),
444 state: RwLock::new(State::new(base, manifest)),
445 })
446 }
447 pub fn open(path: &Path) -> Result<Self, UpdateError> {
449 let file = lock(path)?;
450 let manifest = Manifest::read(&path.join("CURRENT"))?;
451 let base = Base::open(path, &manifest)?;
452 let mut state = State::new(base, manifest.clone());
453 let log = path.join("updates.log");
454 let (sequence, end) = format::replay(&log, &manifest, |ops| state.replay(ops))?;
455 let log_file = OpenOptions::new().write(true).open(&log)?;
456 if log_file.metadata()?.len() != end {
457 log_file.set_len(end)?;
458 log_file.sync_all()?;
459 sync_mapped_file(&log_file)?;
460 }
461 state.sequence = sequence;
462 state.log_end = end;
463 cleanup_generations(path, state.manifest.sequence)?;
464 Ok(Self {
465 dir: path.to_path_buf(),
466 _lock: file,
467 writer: AtomicU8::new(0),
468 state: RwLock::new(state),
469 })
470 }
471 fn writer(&self) -> Result<Writer<'_>, UpdateError> {
472 self.writer
473 .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
474 .map_err(|v| {
475 if v == 2 {
476 UpdateError::RecoveryRequired
477 } else {
478 UpdateError::WriterBusy
479 }
480 })?;
481 Ok(Writer(&self.writer))
482 }
483 pub fn len(&self) -> usize {
484 self.state.read().len()
485 }
486 pub fn is_empty(&self) -> bool {
487 self.len() == 0
488 }
489 pub fn dim(&self) -> usize {
490 self.state.read().manifest.dim
491 }
492 pub fn insert(&self, id: u32, vector: &[f32]) -> Result<(), UpdateError> {
494 self.stage(id, Some(vector), 1)
495 }
496 pub fn delete(&self, id: u32) -> Result<(), UpdateError> {
498 self.stage(id, None, 2)
499 }
500 pub fn replace(&self, id: u32, vector: &[f32]) -> Result<(), UpdateError> {
502 self.stage(id, Some(vector), 3)
503 }
504 fn stage(&self, id: u32, vector: Option<&[f32]>, tag: u8) -> Result<(), UpdateError> {
505 let _writer = self.writer()?;
506 let mut s = self.state.write();
507 if id == u32::MAX {
508 return Err(UpdateError::IdOutOfRange);
509 }
510 if s.touched.contains(&id) {
511 return Err(UpdateError::DuplicateIdInBatch(id));
512 }
513 if let Some(v) = vector {
514 validate_vector(v, s.manifest.dim, s.manifest.build.metric)?;
515 }
516 let size = 5 + vector.map_or(0, |v| v.len() * 4);
517 if s.pending.len() >= MAX_OPS
518 || s.pending_bytes + size > MAX_PAYLOAD
519 || s.delta.len() >= (u32::MAX - 1) as usize
520 {
521 return Err(UpdateError::Capacity);
522 }
523 let op = match tag {
524 1 => Operation::Insert(id, vector.expect("insert vector").to_vec()),
525 2 => Operation::Delete(id),
526 _ => Operation::Replace(id, vector.expect("replace vector").to_vec()),
527 };
528 s.validate_op(&op)?;
529 s.apply(&op);
530 s.touched.insert(id);
531 s.pending_bytes += size;
532 s.pending.push(op);
533 Ok(())
534 }
535 pub fn batch_capacity(&self) -> usize {
537 MAX_OPS.min(MAX_PAYLOAD / (5 + 4 * self.dim()))
538 }
539 pub fn insert_batch(&self, ids: &[u32], vectors: &[f32]) -> Result<(), UpdateError> {
542 let _writer = self.writer()?;
543 let mut s = self.state.write();
544 let dim = s.manifest.dim;
545 if vectors.len() != ids.len() * dim {
546 return Err(UpdateError::DimensionMismatch {
547 expected: ids.len() * dim,
548 actual: vectors.len(),
549 });
550 }
551 let size = (5 + 4 * dim) * ids.len();
552 if s.pending.len() + ids.len() > MAX_OPS
553 || s.pending_bytes + size > MAX_PAYLOAD
554 || s.delta.len() + ids.len() >= (u32::MAX - 1) as usize
555 {
556 return Err(UpdateError::Capacity);
557 }
558 let mut batch = HashSet::with_capacity(ids.len());
559 for (&id, row) in ids.iter().zip(vectors.chunks_exact(dim)) {
560 if id == u32::MAX {
561 return Err(UpdateError::IdOutOfRange);
562 }
563 if s.touched.contains(&id) || !batch.insert(id) {
564 return Err(UpdateError::DuplicateIdInBatch(id));
565 }
566 if s.contains(id) {
567 return Err(UpdateError::AlreadyExists(id));
568 }
569 validate_vector(row, dim, s.manifest.build.metric)?;
570 }
571 let mut rows = vectors.to_vec();
572 super::prepare_rows(&mut rows, dim, s.manifest.build.metric);
573 let build = s.manifest.build.clone();
574 s.delta.insert_batch(ids, &rows, &build);
575 for (&id, row) in ids.iter().zip(vectors.chunks_exact(dim)) {
576 s.pending.push(Operation::Insert(id, row.to_vec()));
577 s.touched.insert(id);
578 }
579 s.pending_bytes += size;
580 Ok(())
581 }
582 pub fn commit(&self) -> Result<(), UpdateError> {
584 let writer = self.writer()?;
585 self.commit_with(&writer)
586 }
587 fn commit_with(&self, writer: &Writer<'_>) -> Result<(), UpdateError> {
588 let (bytes, end, sequence) = {
589 let s = self.state.read();
590 if s.pending.is_empty() {
591 return Ok(());
592 }
593 let sequence = s.sequence.checked_add(1).ok_or(UpdateError::Capacity)?;
594 (
595 format::frame(&s.pending, sequence, s.manifest.dim)?,
596 s.log_end,
597 sequence,
598 )
599 };
600 if let Err(e) = format::append(&self.dir.join("updates.log"), end, &bytes) {
601 writer.uncertain();
602 return Err(e);
603 }
604 let mut s = self.state.write();
605 s.sequence = sequence;
606 s.log_end += bytes.len() as u64;
607 s.pending.clear();
608 s.touched.clear();
609 s.pending_bytes = 0;
610 Ok(())
611 }
612 pub fn search(
614 &self,
615 query: &[f32],
616 k: usize,
617 params: VSearchParams,
618 ) -> Result<Vec<u32>, UpdateError> {
619 let mut out = Vec::new();
620 self.search_into(query, k, params, &mut out)?;
621 Ok(out)
622 }
623 pub fn search_into(
625 &self,
626 query: &[f32],
627 k: usize,
628 params: VSearchParams,
629 out: &mut Vec<u32>,
630 ) -> Result<(), UpdateError> {
631 let s = self.state.read();
632 validate_vector(query, s.manifest.dim, s.manifest.build.metric)?;
633 let k = k.min(s.len());
634 out.clear();
635 if k == 0 {
636 return Ok(());
637 }
638 if s.delta.len() == 0 && s.deleted == 0 {
639 if let Some(graph) = &s.base.graph {
640 SCRATCH.with(|cache| {
641 graph.search_reusing(&mut cache.borrow_mut(), query, k, params, out)
642 })?;
643 if !s.base.dense_ids {
644 for slot in out {
645 *slot = s.base.rows.ids[*slot as usize];
646 }
647 }
648 }
649 return Ok(());
650 }
651 if let Some(graph) = s
652 .base
653 .graph
654 .as_ref()
655 .filter(|_| s.deleted < s.base.rows.ids.len())
656 {
657 let live = graph.len() - s.deleted;
658 let mut take = widened_width(params.ef.max(params.rerank).max(k), graph.len(), live);
659 loop {
660 let mut widened = params;
661 widened.ef = take;
662 widened.rerank = take;
663 widened.patience = 0;
664 SCRATCH.with(|cache| {
665 graph.search_reusing(&mut cache.borrow_mut(), query, take, widened, out)
666 })?;
667 if take == graph.len()
668 || out.iter().filter(|&&slot| !s.dead(slot as usize)).count() >= k.min(live)
669 {
670 break;
671 }
672 take = take.saturating_mul(2).min(graph.len());
673 }
674 }
675 let metric = s.manifest.build.metric;
676 let mut candidates =
677 Vec::with_capacity(out.len() + s.delta.live.len().min(params.ef.max(k)));
678 for &slot in out.iter() {
679 let slot = slot as usize;
680 if !s.dead(slot) {
681 candidates.push((
682 distance(query, s.base.rows.row(slot)?, metric),
683 s.base.rows.ids[slot],
684 ));
685 }
686 }
687 if !s.delta.live.is_empty() {
688 let mut ef = widened_width(
689 params.ef.max(params.rerank).max(k),
690 s.delta.len(),
691 s.delta.live.len(),
692 );
693 loop {
694 let found = s.delta.search(query, ef, metric);
695 if ef == s.delta.len() || found.len() >= k.min(s.delta.live.len()) {
696 candidates.extend(found);
697 break;
698 }
699 ef = ef.saturating_mul(2).min(s.delta.len());
700 }
701 }
702 candidates.sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
703 candidates.truncate(k);
704 out.clear();
705 out.extend(candidates.into_iter().map(|(_, id)| id));
706 Ok(())
707 }
708 pub fn needs_consolidation(&self, policy: ConsolidationPolicy) -> Result<bool, UpdateError> {
709 if !policy.delta_ratio.is_finite()
710 || !policy.tombstone_ratio.is_finite()
711 || !(0.0..=1.0).contains(&policy.delta_ratio)
712 || !(0.0..=1.0).contains(&policy.tombstone_ratio)
713 {
714 return Err(UpdateError::InvalidVector(
715 "consolidation ratios must be in 0..=1",
716 ));
717 }
718 let s = self.state.read();
719 let base = s.base.rows.ids.len().max(1) as f64;
720 Ok(
721 (s.delta.len() >= policy.min_delta
722 && s.delta.len() as f64 / base >= policy.delta_ratio)
723 || (s.deleted > 0 && s.deleted as f64 / base >= policy.tombstone_ratio),
724 )
725 }
726 pub fn consolidate(&self) -> Result<(), UpdateError> {
728 self.consolidate_inner(|_| Ok(()))
729 }
730 fn consolidate_inner(
731 &self,
732 mut checkpoint: impl FnMut(u8) -> Result<(), UpdateError>,
733 ) -> Result<(), UpdateError> {
734 let writer = self.writer()?;
735 self.commit_with(&writer)?;
736 let snapshot = self.state.read();
739 if snapshot.delta.len() == 0 && snapshot.deleted == 0 {
740 return Ok(());
741 }
742 let mut manifest = snapshot.manifest.clone();
743 manifest.sequence = snapshot.sequence;
744 let mut rows = Vec::new();
745 rows.try_reserve_exact(
746 snapshot
747 .len()
748 .checked_mul(manifest.dim)
749 .ok_or(UpdateError::Capacity)?,
750 )
751 .map_err(|_| UpdateError::Capacity)?;
752 let mut ids: Vec<_> = snapshot
753 .base
754 .rows
755 .ids
756 .iter()
757 .enumerate()
758 .filter(|(slot, _)| !snapshot.dead(*slot))
759 .map(|(_, id)| *id)
760 .chain(snapshot.delta.live.keys().copied())
761 .collect();
762 ids.sort_unstable();
763 for &id in &ids {
764 if let Some(&slot) = snapshot.delta.live.get(&id) {
765 rows.extend_from_slice(snapshot.delta.row(slot, manifest.dim));
766 } else {
767 rows.extend_from_slice(
768 snapshot
769 .base
770 .rows
771 .row(snapshot.base_slot(id).expect("live base id"))?,
772 );
773 }
774 }
775 drop(snapshot);
776 write_base(&self.dir, &manifest, &ids, rows)?;
777 checkpoint(1)?;
778 let base = Base::open(&self.dir, &manifest)?;
779 let publication = (|| {
780 atomic_write(&self.dir.join("CURRENT"), &manifest.encode())?;
781 checkpoint(2)?;
782 checkpoint(3)?;
783 format::rotate(&self.dir.join("updates.log"), &manifest)?;
784 Ok::<_, UpdateError>(())
785 })();
786 if let Err(e) = publication {
787 writer.uncertain();
788 return Err(e);
789 }
790 *self.state.write() = State::new(base, manifest.clone());
791 cleanup_generations(&self.dir, manifest.sequence)?;
792 Ok(())
793 }
794}
795fn cleanup_generations(dir: &Path, active: u64) -> Result<(), UpdateError> {
796 for entry in std::fs::read_dir(dir)? {
797 let entry = entry?;
798 let name = entry.file_name();
799 let Some(name) = name.to_str() else { continue };
800 let sequence = name
801 .strip_prefix("base-")
802 .and_then(|s| s.strip_suffix(".vertex"))
803 .or_else(|| {
804 name.strip_prefix("rows-")
805 .and_then(|s| s.strip_suffix(".bin"))
806 })
807 .and_then(|s| s.parse::<u64>().ok());
808 if sequence.is_some_and(|s| s != active) {
809 std::fs::remove_file(entry.path())?;
810 }
811 }
812 sync_directory(dir)?;
813 Ok(())
814}
815
816fn write_base(
819 dir: &Path,
820 manifest: &Manifest,
821 ids: &[u32],
822 rows: Vec<f32>,
823) -> Result<(), UpdateError> {
824 BaseRows::write(&manifest.rows_path(dir), manifest, ids, &rows)?;
825 if !ids.is_empty() {
826 let graph = Graph::build_owned(rows, manifest.dim, &manifest.build)?;
827 let e = &manifest.encoding;
828 let mut vertex = VGraph::from_graph_with(
829 &graph,
830 e.bits,
831 e.residual_bits,
832 e.align,
833 e.reorder,
834 e.id_bytes,
835 e.screen,
836 )?;
837 if e.block_bits != 0 {
838 vertex = vertex.with_blocks(&graph, e.block_bits)?;
839 }
840 vertex.save(&manifest.vertex_path(dir))?;
841 }
842 sync_directory(dir)?;
843 Ok(())
844}
845
846#[cfg(test)]
847mod tests;