use std::path::Path;
use tracing::debug;
use super::{ReclaimError, ReclaimStats, Result};
use crate::data::executor::sparse_vector_checkpoint::{
read_sparse_vector_manifest_at, sparse_vector_checkpoint_prefix, sparse_vector_ckpt_gen_dir,
};
pub fn reclaim_sparse_vector_checkpoints(
data_dir: &Path,
database_id: u64,
tenant_id: u64,
collection: &str,
) -> Result<ReclaimStats> {
let root = data_dir.join("sparse-vector-ckpt");
let cores = match std::fs::read_dir(&root) {
Ok(entries) => entries,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
return Ok(ReclaimStats::default());
}
Err(source) => {
return Err(ReclaimError::Io {
operation: "read sparse-vector checkpoint root",
path: root,
source,
});
}
};
let prefix = sparse_vector_checkpoint_prefix(database_id, tenant_id, collection);
let mut stats = ReclaimStats::default();
for core_entry in cores {
let core_entry = core_entry.map_err(|source| ReclaimError::Io {
operation: "read sparse-vector core entry",
path: root.clone(),
source,
})?;
let core_dir = core_entry.path();
if !core_dir.is_dir() {
continue;
}
let Some(core_id) = core_dir
.file_name()
.and_then(|s| s.to_str())
.and_then(|n| n.strip_prefix("core-"))
.and_then(|n| n.parse::<usize>().ok())
else {
continue;
};
let manifest = match read_sparse_vector_manifest_at(&core_dir, core_id) {
Ok(Some(m)) => m,
Ok(None) => continue,
Err(error) => {
return Err(ReclaimError::SparseManifest {
path: core_dir.join("MANIFEST"),
detail: error.to_string(),
});
}
};
let gen_dir = sparse_vector_ckpt_gen_dir(&core_dir, manifest.generation);
reclaim_generation(&gen_dir, &prefix, &mut stats)?;
}
Ok(stats)
}
fn reclaim_generation(gen_dir: &Path, prefix: &str, stats: &mut ReclaimStats) -> Result<()> {
let entries = std::fs::read_dir(gen_dir).map_err(|source| ReclaimError::Io {
operation: "read sparse-vector live generation",
path: gen_dir.to_path_buf(),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| ReclaimError::Io {
operation: "read sparse-vector generation entry",
path: gen_dir.to_path_buf(),
source,
})?;
let path = entry.path();
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
if !name.starts_with(prefix) {
continue;
}
if !(name.ends_with(".ckpt") || name.ends_with(".ckpt.tmp")) {
continue;
}
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
match std::fs::remove_file(&path) {
Ok(()) => {
stats.files_unlinked = stats.files_unlinked.saturating_add(1);
stats.bytes_freed = stats.bytes_freed.saturating_add(size);
debug!(path = %path.display(), size, "sparse-vector reclaim: unlinked");
}
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => {
return Err(ReclaimError::Io {
operation: "unlink sparse-vector checkpoint",
path,
source,
});
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::executor::sparse_vector_checkpoint::sparse_vector_ckpt_dir;
use tempfile::TempDir;
fn publish(data_dir: &Path, core_id: usize, generation: u64, files: &[&str]) {
let ckpt_dir = sparse_vector_ckpt_dir(data_dir, core_id);
let gen_dir = sparse_vector_ckpt_gen_dir(&ckpt_dir, generation);
std::fs::create_dir_all(&gen_dir).expect("mkdir");
for f in files {
std::fs::write(gen_dir.join(f), b"x").expect("write");
}
let manifest =
crate::data::executor::sparse_vector_checkpoint::test_manifest_bytes(generation);
std::fs::write(ckpt_dir.join("MANIFEST"), manifest).expect("manifest");
}
#[test]
fn unlinks_the_collections_files_across_cores() {
let tmp = TempDir::new().expect("tempdir");
publish(
tmp.path(),
0,
3,
&[
"0_1_docs_title.ckpt",
"0_1_docs_body.ckpt",
"0_1_posts_title.ckpt",
],
);
publish(
tmp.path(),
1,
0,
&[
"0_1_docs_title.ckpt",
"0_2_docs_title.ckpt",
"1_1_docs_title.ckpt",
],
);
let stats = reclaim_sparse_vector_checkpoints(tmp.path(), 0, 1, "docs").unwrap();
assert_eq!(
stats.files_unlinked, 3,
"both of core 0's files and core 1's one file must be unlinked"
);
let gen0 = sparse_vector_ckpt_gen_dir(&sparse_vector_ckpt_dir(tmp.path(), 0), 3);
assert!(!gen0.join("0_1_docs_title.ckpt").exists());
assert!(!gen0.join("0_1_docs_body.ckpt").exists());
assert!(gen0.join("0_1_posts_title.ckpt").exists());
let gen1 = sparse_vector_ckpt_gen_dir(&sparse_vector_ckpt_dir(tmp.path(), 1), 0);
assert!(!gen1.join("0_1_docs_title.ckpt").exists());
assert!(gen1.join("0_2_docs_title.ckpt").exists());
assert!(gen1.join("1_1_docs_title.ckpt").exists());
}
#[test]
fn does_not_unlink_a_longer_collection_name() {
let tmp = TempDir::new().expect("tempdir");
publish(tmp.path(), 0, 0, &["0_1_docs%5Farchive_title.ckpt"]);
let stats = reclaim_sparse_vector_checkpoints(tmp.path(), 0, 1, "docs").unwrap();
assert_eq!(stats.files_unlinked, 0);
let gen_dir = sparse_vector_ckpt_gen_dir(&sparse_vector_ckpt_dir(tmp.path(), 0), 0);
assert!(gen_dir.join("0_1_docs%5Farchive_title.ckpt").exists());
}
#[test]
fn corrupt_manifest_is_returned_to_lifecycle_barrier() {
let tmp = TempDir::new().expect("tempdir");
let core_dir = sparse_vector_ckpt_dir(tmp.path(), 0);
std::fs::create_dir_all(&core_dir).expect("mkdir");
std::fs::write(core_dir.join("MANIFEST"), b"not-a-manifest").expect("manifest");
let error = reclaim_sparse_vector_checkpoints(tmp.path(), 0, 1, "docs").unwrap_err();
assert!(error.to_string().contains("manifest"));
}
#[test]
fn ignores_superseded_generations() {
let tmp = TempDir::new().expect("tempdir");
publish(tmp.path(), 0, 1, &["0_1_docs_title.ckpt"]);
let stale = sparse_vector_ckpt_gen_dir(&sparse_vector_ckpt_dir(tmp.path(), 0), 0);
std::fs::create_dir_all(&stale).expect("mkdir");
std::fs::write(stale.join("0_1_docs_title.ckpt"), b"old").expect("write");
let stats = reclaim_sparse_vector_checkpoints(tmp.path(), 0, 1, "docs").unwrap();
assert_eq!(stats.files_unlinked, 1, "only the live generation's file");
assert!(stale.join("0_1_docs_title.ckpt").exists());
}
#[test]
fn absent_root_is_a_noop() {
let tmp = TempDir::new().expect("tempdir");
let stats = reclaim_sparse_vector_checkpoints(tmp.path(), 0, 1, "docs").unwrap();
assert_eq!(stats.files_unlinked, 0);
}
}