Reclaim KV checkpoints the index cannot reach
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,8 @@ impl App {
|
||||
// Before the counters are taken, so they measure what is
|
||||
// actually reachable.
|
||||
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::engine::sweep_transient_cache(&transient_cache_path());
|
||||
let context_limit = preferences.context_tokens.max(0) as u32;
|
||||
// Reopen on the project we left, with a fresh draft chat.
|
||||
let last_project = preferences
|
||||
|
||||
@@ -26,6 +26,8 @@ use validation::{validate_dspark, validate_main};
|
||||
|
||||
pub(crate) use validation::validate_model_artifact;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use kvstore::sweep_unreachable as sweep_transient_cache;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) use metal::configure_sources as configure_metal_sources;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -68,6 +69,8 @@ impl KvStore {
|
||||
directory: directory.to_owned(),
|
||||
budget_bytes,
|
||||
};
|
||||
// Reclaim first, so the budget is measured against the whole directory.
|
||||
sweep_unreachable(directory);
|
||||
// A lowered budget takes effect at once, like ds4's eviction on open.
|
||||
store.evict(None);
|
||||
Ok(store)
|
||||
@@ -174,6 +177,41 @@ impl KvStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes what the index cannot reach: a checkpoint whose index is missing or
|
||||
/// unreadable can never be found, and an index whose checkpoint is gone is
|
||||
/// dead weight. Without this, an interrupted store or a checkpoint written
|
||||
/// before the index existed occupies disc that eviction cannot even see, and
|
||||
/// the directory grows past the budget unnoticed.
|
||||
pub(crate) fn sweep_unreachable(directory: &Path) {
|
||||
let Ok(files) = fs::read_dir(directory) else {
|
||||
return;
|
||||
};
|
||||
let reachable = files
|
||||
.filter_map(Result::ok)
|
||||
.map(|file| file.path())
|
||||
.filter(|path| path.extension().is_some_and(|value| value == "meta"))
|
||||
.filter_map(|path| read_entry(&path).ok())
|
||||
.filter(|entry| entry.checkpoint.is_file())
|
||||
.map(|entry| entry.checkpoint)
|
||||
.collect::<HashSet<_>>();
|
||||
let Ok(files) = fs::read_dir(directory) else {
|
||||
return;
|
||||
};
|
||||
for file in files.flatten() {
|
||||
let path = file.path();
|
||||
let dead = match path.extension().and_then(|value| value.to_str()) {
|
||||
Some("bin") => !reachable.contains(&path),
|
||||
Some("meta") => !reachable.contains(&path.with_extension("bin")),
|
||||
// Staging files from an interrupted index write.
|
||||
Some("tmp") => true,
|
||||
_ => false,
|
||||
};
|
||||
if dead {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn eviction_score(entry: &Entry, now: u64) -> f64 {
|
||||
let age = now.saturating_sub(entry.last_used.max(entry.created_at)) as f64;
|
||||
let hits = f64::from(entry.hits) * f64::exp2(-age / HIT_HALF_LIFE_SECONDS);
|
||||
@@ -364,6 +402,29 @@ mod tests {
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_reclaims_checkpoints_the_index_cannot_reach() {
|
||||
let directory = temporary_directory("unreachable");
|
||||
let store = KvStore::open(&directory, GIB).unwrap();
|
||||
let indexed = add(&store, b"indexed", 100, 32);
|
||||
let stray = directory.join("deadbeef.bin");
|
||||
let widow = directory.join("widow.meta");
|
||||
let staging = directory.join("indexed.meta.tmp");
|
||||
fs::write(&stray, vec![0; 4_096]).unwrap();
|
||||
fs::write(&widow, vec![0; 64]).unwrap();
|
||||
fs::write(&staging, vec![0; 64]).unwrap();
|
||||
|
||||
KvStore::open(&directory, GIB).unwrap();
|
||||
|
||||
assert!(indexed.checkpoint.is_file());
|
||||
assert!(index_path(&indexed.checkpoint).is_file());
|
||||
assert!(!stray.exists());
|
||||
assert!(!widow.exists());
|
||||
assert!(!staging.exists());
|
||||
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn budget_evicts_the_least_valuable_unprotected_entry() {
|
||||
let directory = temporary_directory("evict");
|
||||
|
||||
Reference in New Issue
Block a user