use std::fs::{self, File};
use std::io::{self, ErrorKind};
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::{Arc, RwLock};
use crate::serve::kv_persist::format::{self, BlockHash, EnvelopeHeader, ModelFingerprint};
use crate::serve::kv_persist::index::{BlockIndex, BlockMeta};
use crate::serve::kv_persist::metrics::KvCacheMetricsSink;
pub const MAX_BLOCK_BYTES: u64 = 256 * 1024 * 1024;
pub type CompletionTx = std::sync::mpsc::SyncSender<io::Result<()>>;
#[derive(Debug)]
pub struct WriteJob {
pub header: EnvelopeHeader,
pub body: Vec<u8>,
pub completion_tx: Option<CompletionTx>,
}
pub struct DiskBlockStore {
cache_root: PathBuf,
index: BlockIndex,
budget_bytes: AtomicU64,
max_block_bytes_override: AtomicU64,
kv_counters: RwLock<Option<Arc<dyn KvCacheMetricsSink>>>,
}
impl DiskBlockStore {
pub fn new(cache_root: PathBuf, budget_bytes: u64) -> io::Result<Self> {
Self::new_with_index(cache_root, BlockIndex::new(), budget_bytes)
}
pub fn new_with_index(
cache_root: PathBuf,
index: BlockIndex,
budget_bytes: u64,
) -> io::Result<Self> {
if !cache_root.exists() {
fs::create_dir_all(&cache_root)?;
}
fs::create_dir_all(cache_root.join("locks"))?;
fs::create_dir_all(cache_root.join("models"))?;
Ok(Self {
cache_root,
index,
budget_bytes: AtomicU64::new(budget_bytes),
max_block_bytes_override: AtomicU64::new(0),
kv_counters: RwLock::new(None),
})
}
pub fn set_kv_counters(&self, counters: Arc<dyn KvCacheMetricsSink>) {
if let Ok(mut guard) = self.kv_counters.write() {
*guard = Some(counters);
}
}
pub fn max_block_bytes(&self) -> u64 {
let override_val = self.max_block_bytes_override.load(AtomicOrdering::Relaxed);
if override_val == 0 {
MAX_BLOCK_BYTES
} else {
override_val
}
}
#[doc(hidden)]
pub fn set_max_block_bytes_override(&self, override_bytes: u64) {
self.max_block_bytes_override
.store(override_bytes, AtomicOrdering::Relaxed);
}
pub fn cache_root(&self) -> &Path {
&self.cache_root
}
pub fn budget_bytes(&self) -> u64 {
self.budget_bytes.load(AtomicOrdering::Relaxed)
}
pub fn set_budget_bytes(&self, new_budget: u64) {
self.budget_bytes.store(new_budget, AtomicOrdering::Relaxed);
}
pub fn index(&self) -> &BlockIndex {
&self.index
}
pub fn block_path(&self, model_fp: &ModelFingerprint, hash: &BlockHash) -> PathBuf {
let hex = hash.to_string();
let fanout = &hex[..1];
self.cache_root
.join("models")
.join(model_fp.short_hex())
.join("kv")
.join(fanout)
.join(format!("{hex}.safetensors"))
}
pub fn quarantine_dir(&self, model_fp: &ModelFingerprint) -> PathBuf {
self.cache_root
.join("models")
.join(model_fp.short_hex())
.join("kv-quarantine")
}
fn lock_path(&self, model_fp: &ModelFingerprint, hash: &BlockHash) -> PathBuf {
let hex = hash.to_string();
let prefix = &hex[..2];
self.cache_root
.join("locks")
.join(format!("{}__{}.lock", model_fp.short_hex(), prefix))
}
pub fn write_block_sync(&self, header: &EnvelopeHeader, body: &[u8]) -> io::Result<PathBuf> {
let body_len = body.len() as u64;
let ceiling = self.max_block_bytes();
if body_len > ceiling {
return Err(io::Error::new(
ErrorKind::InvalidInput,
format!("block body {body_len} bytes exceeds MAX_BLOCK_BYTES {ceiling}"),
));
}
let path = self.block_path(&header.model_fingerprint, &header.block_hash);
let _lock =
AdvisoryLock::acquire(&self.lock_path(&header.model_fingerprint, &header.block_hash))?;
let total_bytes = format::write_envelope(&path, header, body)?;
let metadata = fs::metadata(&path)?;
let mtime = metadata.modified().unwrap_or(std::time::UNIX_EPOCH);
let bytes_on_disk = metadata.len();
debug_assert_eq!(
bytes_on_disk, total_bytes,
"stat size matches writer return"
);
let meta = BlockMeta {
hash: header.block_hash,
parent: header.parent_block_hash,
model_fp: header.model_fingerprint,
payload_kind: header.payload_kind.clone(),
codec_version: header.codec_version,
n_tokens: header.n_tokens,
file_path: path.clone(),
mtime,
bytes_on_disk,
};
self.index.insert(meta);
Ok(path)
}
pub fn read_block(&self, hash: &BlockHash) -> io::Result<Vec<u8>> {
let meta = self.index.lookup(hash).ok_or_else(|| {
io::Error::new(ErrorKind::NotFound, format!("block {hash} not in index"))
})?;
let (_, body) = format::read_envelope_body(&meta.file_path)?;
Ok(body)
}
pub fn read_block_with_header(
&self,
hash: &BlockHash,
) -> io::Result<(EnvelopeHeader, Vec<u8>)> {
let meta = self.index.lookup(hash).ok_or_else(|| {
io::Error::new(ErrorKind::NotFound, format!("block {hash} not in index"))
})?;
format::read_envelope_body(&meta.file_path)
}
pub fn remove_block(&self, hash: &BlockHash) -> io::Result<u64> {
let Some(meta) = self.index.remove(hash) else {
return Ok(0);
};
match fs::remove_file(&meta.file_path) {
Ok(()) => {}
Err(e) if e.kind() == ErrorKind::NotFound => {
}
Err(e) => return Err(e),
}
Ok(meta.bytes_on_disk)
}
pub fn evict_lru_until_under_budget<F>(&self, is_block_pinned: F) -> io::Result<u64>
where
F: Fn(&BlockHash) -> bool,
{
let budget = self.budget_bytes();
if budget == 0 {
return Ok(0);
}
let total = self.index.total_bytes_on_disk();
if total <= budget {
return Ok(0);
}
let mut entries: Vec<BlockMeta> = self.index.snapshot_all();
entries.sort_by(|a, b| {
a.mtime
.cmp(&b.mtime)
.then_with(|| b.bytes_on_disk.cmp(&a.bytes_on_disk))
.then_with(|| a.hash.0.cmp(&b.hash.0))
});
let mut freed = 0u64;
let counters_snapshot: Option<Arc<dyn KvCacheMetricsSink>> = self
.kv_counters
.read()
.ok()
.and_then(|g| g.as_ref().map(Arc::clone));
for meta in entries {
if self.index.total_bytes_on_disk() <= budget {
break;
}
if is_block_pinned(&meta.hash) {
continue;
}
if self.index.lookup(&meta.hash).is_none() {
continue;
}
let bytes = self.remove_block(&meta.hash)?;
freed = freed.saturating_add(bytes);
if let Some(c) = counters_snapshot.as_ref() {
c.record_eviction_budget_overflow();
}
}
Ok(freed)
}
}
pub fn shared(store: DiskBlockStore) -> Arc<DiskBlockStore> {
Arc::new(store)
}
struct AdvisoryLock {
_file: File,
}
impl AdvisoryLock {
fn acquire(path: &Path) -> io::Result<Self> {
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let file = File::options()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)?;
let fd = file.as_raw_fd();
let ret = unsafe { libc::flock(fd, libc::LOCK_EX) };
if ret != 0 {
return Err(io::Error::last_os_error());
}
Ok(Self { _file: file })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serve::kv_persist::format::{
compute_model_fingerprint, BlockHash, EnvelopeHeader, ParentBlockHash, BLOCK_TOKENS,
CURRENT_FORMAT_VERSION,
};
use sha2::{Digest, Sha256};
use std::process;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, SystemTime};
fn temp_dir(label: &str) -> PathBuf {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = process::id();
let nanos = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!("hf2q-kv-store-{label}-{pid}-{nanos}-{n}"));
fs::create_dir_all(&dir).expect("temp_dir mkdir");
dir
}
fn fixture_fp(seed: &str) -> ModelFingerprint {
compute_model_fingerprint(
seed,
"Q4_0",
"hf2q-test-1.0.0",
"deadbeefcafebabe1122334455667788",
"<|im_start|>...<|im_end|>",
)
}
#[derive(Debug, Default)]
struct TestMetricsSink {
quarantines: [AtomicU64; 4],
evictions: [AtomicU64; 1],
}
impl TestMetricsSink {
fn new() -> Self {
Self::default()
}
fn snapshot_evictions(&self) -> [u64; 1] {
[self.evictions[0].load(AtomicOrdering::Relaxed)]
}
}
impl crate::serve::kv_persist::metrics::KvCacheMetricsSink for TestMetricsSink {
fn record_quarantine(&self, reason: crate::serve::kv_persist::metrics::KvQuarantineReason) {
self.quarantines[reason.index()].fetch_add(1, AtomicOrdering::Relaxed);
}
fn record_eviction_budget_overflow(&self) {
self.evictions[0].fetch_add(1, AtomicOrdering::Relaxed);
}
}
fn make_block(
fp: ModelFingerprint,
parent: ParentBlockHash,
seed: u32,
) -> (Vec<u8>, EnvelopeHeader) {
let body: Vec<u8> = (0..512u32)
.flat_map(|i| (i.wrapping_add(seed)).to_le_bytes())
.collect();
let mut h = Sha256::new();
h.update(&body);
let bh: [u8; 32] = h.finalize().into();
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: fp,
block_hash: BlockHash(bh),
parent_block_hash: parent,
payload_kind: "kv-dense-bf16".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
(body, header)
}
#[test]
fn write_block_sync_round_trip_via_format() {
let dir = temp_dir("rt");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("rt");
let (body, header) = make_block(fp, ParentBlockHash(None), 0xAA);
let path = store.write_block_sync(&header, &body).expect("write");
let expected = store.block_path(&fp, &header.block_hash);
assert_eq!(path, expected);
assert!(path.exists(), "file at expected path");
let (header_back, body_back) =
format::read_envelope_body(&path).expect("read_envelope_body");
assert_eq!(header_back, header, "header round-trips");
assert_eq!(body_back, body, "body bytes round-trip byte-for-byte");
let meta = store.index().lookup(&header.block_hash).expect("indexed");
assert_eq!(meta.bytes_on_disk, fs::metadata(&path).unwrap().len());
assert_eq!(meta.file_path, path);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn read_block_returns_bytes_after_write() {
let dir = temp_dir("read");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("read");
let (body, header) = make_block(fp, ParentBlockHash(None), 0xBB);
store.write_block_sync(&header, &body).expect("write");
let body_back = store.read_block(&header.block_hash).expect("read");
assert_eq!(body_back, body, "read_block returns identical bytes");
let unknown = BlockHash([0xFF; 32]);
let err = store.read_block(&unknown).expect_err("unknown");
assert_eq!(err.kind(), ErrorKind::NotFound);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn remove_block_decrements_index_and_deletes_file() {
let dir = temp_dir("rm");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("rm");
let (body_a, header_a) = make_block(fp, ParentBlockHash(None), 1);
let (body_b, header_b) = make_block(fp, ParentBlockHash(None), 2);
let path_a = store.write_block_sync(&header_a, &body_a).expect("a");
let path_b = store.write_block_sync(&header_b, &body_b).expect("b");
assert_eq!(store.index().block_count(), 2);
let bytes_a = fs::metadata(&path_a).unwrap().len();
let freed = store.remove_block(&header_a.block_hash).expect("rm a");
assert_eq!(freed, bytes_a, "freed bytes match on-disk size");
assert!(!path_a.exists(), "file deleted");
assert!(path_b.exists(), "other file untouched");
assert_eq!(store.index().block_count(), 1);
assert!(store.index().lookup(&header_a.block_hash).is_none());
let freed_again = store.remove_block(&header_a.block_hash).expect("rm a 2");
assert_eq!(freed_again, 0);
let bytes_b_recorded = store
.index()
.lookup(&header_b.block_hash)
.expect("b indexed pre-nuke")
.bytes_on_disk;
fs::remove_file(&path_b).expect("nuke b");
let freed_b = store.remove_block(&header_b.block_hash).expect("rm b");
assert_eq!(
freed_b, bytes_b_recorded,
"freed bytes come from the index even after the file vanished"
);
assert!(freed_b > 0);
assert_eq!(store.index().block_count(), 0);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn evict_lru_until_under_budget_evicts_oldest_first() {
let dir = temp_dir("lru");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("lru");
let mut hashes: Vec<BlockHash> = Vec::new();
let mut sizes: Vec<u64> = Vec::new();
for s in 0u32..5 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
let path = store.write_block_sync(&header, &body).expect("write");
hashes.push(header.block_hash);
sizes.push(fs::metadata(&path).unwrap().len());
thread::sleep(Duration::from_millis(20));
}
assert_eq!(store.index().block_count(), 5);
let budget = sizes[3] + sizes[4];
store.set_budget_bytes(budget);
let freed = store
.evict_lru_until_under_budget(|_| false)
.expect("evict");
let expected_freed: u64 = sizes[..3].iter().sum();
assert_eq!(freed, expected_freed, "freed bytes match oldest 3");
assert_eq!(store.index().block_count(), 2, "2 survivors");
for h in &hashes[..3] {
assert!(store.index().lookup(h).is_none(), "oldest evicted");
let p = store.block_path(&fp, h);
assert!(!p.exists(), "evicted file removed from disk");
}
for h in &hashes[3..] {
assert!(store.index().lookup(h).is_some(), "newest survived");
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn evict_lru_skips_in_use_blocks() {
let dir = temp_dir("pin");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("pin");
let mut hashes: Vec<BlockHash> = Vec::new();
let mut sizes: Vec<u64> = Vec::new();
for s in 0u32..5 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
let path = store.write_block_sync(&header, &body).expect("write");
hashes.push(header.block_hash);
sizes.push(fs::metadata(&path).unwrap().len());
thread::sleep(Duration::from_millis(20));
}
let budget = sizes[1] + sizes[2] + sizes[3] + sizes[4];
store.set_budget_bytes(budget);
let pinned_hashes = vec![hashes[0], hashes[1]];
let pinned_for_closure = pinned_hashes.clone();
let freed = store
.evict_lru_until_under_budget(move |h| pinned_for_closure.contains(h))
.expect("evict");
assert!(
store.index().lookup(&hashes[2]).is_none(),
"block 2 evicted"
);
assert!(
store.index().lookup(&hashes[0]).is_some(),
"pinned 0 survived"
);
assert!(
store.index().lookup(&hashes[1]).is_some(),
"pinned 1 survived"
);
assert_eq!(freed, sizes[2], "freed bytes = block 2 size");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn block_path_uses_hex_fanout_per_d5() {
let dir = temp_dir("path");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("path");
let hex_target_first_char = '7';
let mut bh = BlockHash([0u8; 32]);
bh.0[0] = 0x7B; let p = store.block_path(&fp, &bh);
let p_str = p.to_string_lossy().to_string();
assert!(p_str.contains(&format!("models/{}", fp.short_hex())));
assert!(
p_str.contains(&format!("/kv/{hex_target_first_char}/")),
"hex-fanout dir = first hex char '{hex_target_first_char}': {p_str}"
);
assert!(p_str.ends_with(&format!("{}.safetensors", bh)));
let q = store.quarantine_dir(&fp);
let q_str = q.to_string_lossy().to_string();
assert!(q_str.ends_with(&format!("models/{}/kv-quarantine", fp.short_hex())));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn advisory_lock_serializes_concurrent_writes() {
let dir = temp_dir("lock");
let store = Arc::new(DiskBlockStore::new(dir.clone(), 0).expect("new"));
let fp = fixture_fp("lock");
fn forced_prefix_header(fp: ModelFingerprint, suffix: u32) -> (Vec<u8>, EnvelopeHeader) {
let body: Vec<u8> = (0..1024u32)
.flat_map(|i| (i.wrapping_add(suffix)).to_le_bytes())
.collect();
let mut h = Sha256::new();
h.update(&body);
h.update(b"-collide-prefix-");
h.update(suffix.to_le_bytes());
let mut bh: [u8; 32] = h.finalize().into();
bh[0] = 0xAB;
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: fp,
block_hash: BlockHash(bh),
parent_block_hash: ParentBlockHash(None),
payload_kind: "kv-dense-bf16".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
(body, header)
}
let log: Arc<Mutex<Vec<(u32, SystemTime, SystemTime)>>> = Arc::new(Mutex::new(Vec::new()));
let store_a = Arc::clone(&store);
let log_a = Arc::clone(&log);
let h_a = thread::spawn(move || {
let (body, header) = forced_prefix_header(fp, 1);
thread::sleep(Duration::from_millis(20));
let t0 = SystemTime::now();
let _ = store_a.write_block_sync(&header, &body).expect("write a");
let t1 = SystemTime::now();
log_a.lock().unwrap().push((1, t0, t1));
});
let store_b = Arc::clone(&store);
let log_b = Arc::clone(&log);
let h_b = thread::spawn(move || {
let (body, header) = forced_prefix_header(fp, 2);
let t0 = SystemTime::now();
let _ = store_b.write_block_sync(&header, &body).expect("write b");
let t1 = SystemTime::now();
log_b.lock().unwrap().push((2, t0, t1));
});
h_a.join().expect("a join");
h_b.join().expect("b join");
assert_eq!(store.index().block_count(), 2);
let example_hash = {
let mut bh = [0u8; 32];
bh[0] = 0xAB;
BlockHash(bh)
};
let lp = store.lock_path(&fp, &example_hash);
assert!(lp.exists(), "lock file present at {}", lp.display());
let lp_name = lp.file_name().unwrap().to_string_lossy().to_string();
assert!(lp_name.contains("__ab.lock"), "lock file name: {lp_name}");
let entries = log.lock().unwrap();
assert_eq!(entries.len(), 2, "both threads logged");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn oversized_block_refusal_returns_error() {
let dir = temp_dir("oversize");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("oversize");
store.set_max_block_bytes_override(1024);
let body: Vec<u8> = vec![0u8; 1025];
let mut h = Sha256::new();
h.update(&body);
let bh: [u8; 32] = h.finalize().into();
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: fp,
block_hash: BlockHash(bh),
parent_block_hash: ParentBlockHash(None),
payload_kind: "kv-oversize-test".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
let err = store.write_block_sync(&header, &body).expect_err("err");
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert!(
err.to_string().contains("exceeds MAX_BLOCK_BYTES"),
"error mentions MAX_BLOCK_BYTES: {err}"
);
assert_eq!(store.index().block_count(), 0);
let p = store.block_path(&fp, &header.block_hash);
assert!(!p.exists());
store.set_max_block_bytes_override(1024);
let body_at_ceiling: Vec<u8> = vec![0u8; 1024];
let mut h2 = Sha256::new();
h2.update(&body_at_ceiling);
let bh2: [u8; 32] = h2.finalize().into();
let header_at_ceiling = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: fp,
block_hash: BlockHash(bh2),
parent_block_hash: ParentBlockHash(None),
payload_kind: "kv-oversize-boundary".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
store
.write_block_sync(&header_at_ceiling, &body_at_ceiling)
.expect("at-ceiling accepted");
assert_eq!(store.index().block_count(), 1);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn budget_zero_disables_eviction() {
let dir = temp_dir("uncapped");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("uncapped");
for s in 0u32..3 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
store.write_block_sync(&header, &body).expect("write");
}
assert_eq!(store.index().block_count(), 3);
let freed = store
.evict_lru_until_under_budget(|_| false)
.expect("evict");
assert_eq!(freed, 0, "uncapped → no eviction");
assert_eq!(store.index().block_count(), 3);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn new_with_index_preserves_recovery_state() {
let dir = temp_dir("withidx");
{
let store_a = DiskBlockStore::new(dir.clone(), 0).expect("new a");
let fp = fixture_fp("withidx");
for s in 0u32..3 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
store_a.write_block_sync(&header, &body).expect("write");
}
}
let idx = BlockIndex::rebuild_from_disk(&dir).expect("rebuild");
assert_eq!(idx.block_count(), 3);
let store_b = DiskBlockStore::new_with_index(dir.clone(), idx, 0).expect("new b");
assert_eq!(store_b.index().block_count(), 3);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn set_budget_bytes_zero_means_unlimited() {
let dir = temp_dir("budget-zero");
let store = DiskBlockStore::new(dir.clone(), 1 << 20).expect("new");
assert_eq!(store.budget_bytes(), 1 << 20);
store.set_budget_bytes(0);
assert_eq!(store.budget_bytes(), 0, "0 = unlimited sentinel");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn set_budget_bytes_nonzero_persists_through_lookup() {
let dir = temp_dir("budget-nonzero");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
assert_eq!(store.budget_bytes(), 0);
const ONE_GIB: u64 = 1 << 30;
store.set_budget_bytes(ONE_GIB);
assert_eq!(store.budget_bytes(), ONE_GIB);
const FOUR_GIB: u64 = 4u64 << 30;
store.set_budget_bytes(FOUR_GIB);
assert_eq!(store.budget_bytes(), FOUR_GIB);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn cache_evictions_total_bumps_per_evicted_block() {
let dir = temp_dir("rf7-evict");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let concrete = Arc::new(TestMetricsSink::new());
let as_trait: Arc<dyn KvCacheMetricsSink> =
Arc::clone(&concrete) as Arc<dyn KvCacheMetricsSink>;
store.set_kv_counters(as_trait);
let fp = fixture_fp("rf7-evict");
let mut hashes: Vec<BlockHash> = Vec::new();
let mut sizes: Vec<u64> = Vec::new();
for s in 0u32..5 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
let path = store.write_block_sync(&header, &body).expect("write");
hashes.push(header.block_hash);
sizes.push(fs::metadata(&path).unwrap().len());
thread::sleep(Duration::from_millis(20));
}
assert_eq!(concrete.snapshot_evictions(), [0u64]);
let budget = sizes[3] + sizes[4];
store.set_budget_bytes(budget);
let _freed = store
.evict_lru_until_under_budget(|_| false)
.expect("evict");
assert_eq!(store.index().block_count(), 2);
assert_eq!(
concrete.snapshot_evictions(),
[3u64],
"3 blocks evicted ⇒ +3 on budget_overflow"
);
let dir2 = temp_dir("rf7-evict-pin");
let store2 = DiskBlockStore::new(dir2.clone(), 0).expect("new2");
let concrete2 = Arc::new(TestMetricsSink::new());
let as_trait2: Arc<dyn KvCacheMetricsSink> =
Arc::clone(&concrete2) as Arc<dyn KvCacheMetricsSink>;
store2.set_kv_counters(as_trait2);
let fp2 = fixture_fp("rf7-evict-pin");
let mut h2: Vec<BlockHash> = Vec::new();
let mut s2: Vec<u64> = Vec::new();
for s in 0u32..3 {
let (body, header) = make_block(fp2, ParentBlockHash(None), s);
let path = store2.write_block_sync(&header, &body).expect("w2");
h2.push(header.block_hash);
s2.push(fs::metadata(&path).unwrap().len());
thread::sleep(Duration::from_millis(20));
}
let budget2 = s2[2];
store2.set_budget_bytes(budget2);
let pinned = vec![h2[1], h2[2]];
let pinned_clone = pinned.clone();
let _ = store2
.evict_lru_until_under_budget(move |h| pinned_clone.contains(h))
.expect("evict2");
assert_eq!(
concrete2.snapshot_evictions(),
[1u64],
"1 block evicted under pin pressure ⇒ +1 on budget_overflow"
);
let _ = fs::remove_dir_all(&dir);
let _ = fs::remove_dir_all(&dir2);
}
#[test]
fn kv_persist_cache_bytes_on_disk_gauge_returns_sum_of_block_sizes() {
let dir = temp_dir("rf7-bytes");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("rf7-bytes");
assert_eq!(store.index().total_bytes_on_disk(), 0);
let mut total_fs_bytes: u64 = 0;
for s in 0u32..4 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
let path = store.write_block_sync(&header, &body).expect("write");
total_fs_bytes += fs::metadata(&path).unwrap().len();
}
assert_eq!(
store.index().total_bytes_on_disk(),
total_fs_bytes,
"gauge sources from index == fs::metadata reality"
);
let to_remove = store.index().snapshot_all()[0].clone();
let removed_bytes = to_remove.bytes_on_disk;
let pre = store.index().total_bytes_on_disk();
store.remove_block(&to_remove.hash).expect("rm");
let post = store.index().total_bytes_on_disk();
assert_eq!(
pre - post,
removed_bytes,
"gauge tracks remove() in lockstep"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn kv_persist_cache_blocks_total_gauge_matches_block_index_len() {
let dir = temp_dir("rf7-blocks");
let store = DiskBlockStore::new(dir.clone(), 0).expect("new");
let fp = fixture_fp("rf7-blocks");
assert_eq!(store.index().block_count(), 0);
let mut hashes: Vec<BlockHash> = Vec::new();
for s in 0u32..7 {
let (body, header) = make_block(fp, ParentBlockHash(None), s);
store.write_block_sync(&header, &body).expect("write");
hashes.push(header.block_hash);
assert_eq!(
store.index().block_count(),
hashes.len(),
"gauge tracks insert in lockstep"
);
}
for h in hashes.drain(..3) {
store.remove_block(&h).expect("rm");
}
assert_eq!(store.index().block_count(), 4, "7 - 3 = 4");
let _ = fs::remove_dir_all(&dir);
}
}