use std::collections::HashMap;
use std::marker::PhantomData;
use std::ops::Range;
use std::sync::{Arc, Mutex, RwLock};
use sha2::{Digest, Sha256};
use crate::serve::kv_persist::block_store::{DiskBlockStore, WriteJob};
use crate::serve::kv_persist::format::{
compute_model_fingerprint, BlockHash, EnvelopeHeader, ModelFingerprint, ParentBlockHash,
BLOCK_TOKENS, CURRENT_FORMAT_VERSION,
};
use crate::serve::kv_persist::writer::AsyncWriterHandle;
use crate::serve::multi_model::{
KvSpiller, LoadedEngine, LoadedHandle, RestoreErrorKind, RestoreOutcome, SpillErrorKind,
SpillOutcome,
};
use crate::serve::quant_select::QuantType;
pub trait KvCacheSpill: Send + Sync {
fn block_alignment(&self) -> u32;
fn snapshot_block(&self, layer_rank: usize, range: Range<u32>) -> Option<Vec<u8>>;
fn restore_block(
&mut self,
layer_rank: usize,
range: Range<u32>,
payload: &[u8],
) -> Result<(), SpillErrorKind>;
fn model_fingerprint(
&self,
_repo: &str,
_quant: crate::serve::quant_select::QuantType,
) -> Option<crate::serve::kv_persist::format::ModelFingerprint> {
None
}
fn n_layers(&self) -> usize {
1
}
fn snapshot_prompt_cache(&self) -> Option<Vec<u8>> {
None
}
fn restore_prompt_cache(&mut self, _payload: &[u8]) -> Result<(), SpillErrorKind> {
Ok(())
}
}
type FamilyKey = (String, &'static str);
type FamilyHook = Arc<Mutex<dyn KvCacheSpill>>;
pub struct BlockPrefixCacheSpiller<E> {
store: Arc<DiskBlockStore>,
writer: Arc<AsyncWriterHandle>,
registrations: RwLock<HashMap<FamilyKey, FamilyHook>>,
registry: RwLock<Option<Arc<crate::serve::kv_persist::registry::KvPersistRegistry>>>,
_phantom: PhantomData<fn(E)>,
}
impl<E> BlockPrefixCacheSpiller<E> {
pub fn new(store: Arc<DiskBlockStore>, writer: Arc<AsyncWriterHandle>) -> Self {
Self {
store,
writer,
registrations: RwLock::new(HashMap::new()),
registry: RwLock::new(None),
_phantom: PhantomData,
}
}
pub fn set_registry(
&self,
registry: Arc<crate::serve::kv_persist::registry::KvPersistRegistry>,
) {
let mut g = self
.registry
.write()
.expect("BlockPrefixCacheSpiller::registry RwLock poisoned");
*g = Some(registry);
}
pub fn register_family(&self, repo: String, quant: QuantType, hook: FamilyHook) {
let mut g = self
.registrations
.write()
.expect("BlockPrefixCacheSpiller::registrations RwLock poisoned");
g.insert((repo, quant.as_str()), hook);
}
pub fn unregister_family(&self, repo: &str, quant: QuantType) -> bool {
let mut g = self
.registrations
.write()
.expect("BlockPrefixCacheSpiller::registrations RwLock poisoned");
let key = (repo.to_string(), quant.as_str());
g.remove(&key).is_some()
}
pub fn registered_count(&self) -> usize {
self.registrations
.read()
.expect("BlockPrefixCacheSpiller::registrations RwLock poisoned")
.len()
}
pub fn pending_writer_queue_depth(&self) -> usize {
self.writer.pending_jobs()
}
fn lookup_hook(&self, repo: &str, quant: QuantType) -> Option<FamilyHook> {
let g = self
.registrations
.read()
.expect("BlockPrefixCacheSpiller::registrations RwLock poisoned");
let key = (repo.to_string(), quant.as_str());
g.get(&key).cloned()
}
fn family_model_fp(&self, repo: &str, quant: QuantType) -> ModelFingerprint {
if let Some(hook_arc) = self.lookup_hook(repo, quant) {
if let Ok(g) = hook_arc.lock() {
if let Some(fp) = g.model_fingerprint(repo, quant) {
return fp;
}
}
}
compute_model_fingerprint(repo, quant.as_str(), "", "", "")
}
fn parse_quant(handle: &LoadedHandle) -> Option<QuantType> {
QuantType::from_canonical_str(&handle.quant).ok()
}
fn n_layers_for_family(hook: &FamilyHook) -> usize {
hook.lock().map(|g| g.n_layers()).unwrap_or(1)
}
const MAX_PER_LAYER_TOKENS: u32 = 32768;
}
#[cfg(test)]
impl<E> BlockPrefixCacheSpiller<E> {
pub fn test_only_inject_pending_spill(&self, n: usize) {
let test_fp = compute_model_fingerprint(
"_test/K2",
"test-quant",
"iter-16-infra",
"0000000000000000000000000000000000000000000000000000000000000000",
"<|test|>",
);
for seed in 0..n {
let body: Vec<u8> = (0..64u32)
.flat_map(|i| (i.wrapping_add(seed as u32).wrapping_mul(0x9E3779B1)).to_le_bytes())
.collect();
let bh: [u8; 32] = Sha256::digest(&body).into();
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: test_fp,
block_hash: BlockHash(bh),
parent_block_hash: ParentBlockHash(None),
payload_kind: "kv-spiller-test-K2".into(),
codec_version: 1,
n_tokens: BLOCK_TOKENS,
};
let job = WriteJob {
header,
body,
completion_tx: None,
};
let _ = self.writer.enqueue(job);
}
}
}
impl<E> KvSpiller<E> for BlockPrefixCacheSpiller<E>
where
E: Send + Sync + 'static,
{
fn pre_evict(&self, handle: &LoadedHandle, _engine: &Arc<LoadedEngine<E>>) -> SpillOutcome {
let Some(quant) = Self::parse_quant(handle) else {
tracing::warn!(
target: "hf2q::kv_persist::spiller",
quant = ?handle.quant,
repo_id = ?handle.repo_id,
"pre_evict: parse_quant=None — skipping spill"
);
return SpillOutcome::Skipped;
};
let bare_repo: &str = handle
.repo_id
.strip_suffix(&format!("@{}", handle.quant))
.unwrap_or(handle.repo_id.as_str());
let Some(hook_arc) = self.lookup_hook(bare_repo, quant) else {
return SpillOutcome::Skipped;
};
let model_fp = self.family_model_fp(bare_repo, quant);
let alignment = {
let g = match hook_arc.lock() {
Ok(g) => g,
Err(_) => return SpillOutcome::Error(SpillErrorKind::CodecErr),
};
g.block_alignment()
};
if alignment == 0 {
return SpillOutcome::Skipped;
}
let n_layers = Self::n_layers_for_family(&hook_arc);
let mut enqueued: u32 = 0;
let mut parent = ParentBlockHash(None);
for layer_rank in 0..n_layers {
let mut range_start: u32 = 0;
while range_start < Self::MAX_PER_LAYER_TOKENS {
let range_end = range_start
.saturating_add(alignment)
.min(Self::MAX_PER_LAYER_TOKENS);
let range = range_start..range_end;
let snapshot = {
let g = match hook_arc.lock() {
Ok(g) => g,
Err(_) => return SpillOutcome::Error(SpillErrorKind::CodecErr),
};
g.snapshot_block(layer_rank, range.clone())
};
let body = match snapshot {
Some(b) => b,
None => {
break;
}
};
let bh: [u8; 32] = Sha256::digest(&body).into();
let block_hash = BlockHash(bh);
let n_tokens = (range.end.saturating_sub(range.start)).min(BLOCK_TOKENS);
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: model_fp,
block_hash,
parent_block_hash: parent,
payload_kind: format!("kv-spiller-l{layer_rank}"),
codec_version: 1,
n_tokens,
};
let job = WriteJob {
header,
body,
completion_tx: None,
};
let enqueue_result = match self.writer.enqueue(job) {
Ok(()) => Ok(()),
Err(std::sync::mpsc::TrySendError::Full(job)) => {
self.writer.enqueue_blocking(job).map_err(|_| ())
}
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => Err(()),
};
match enqueue_result {
Ok(()) => {
enqueued = enqueued.saturating_add(1);
parent = ParentBlockHash(Some(block_hash));
range_start = range_end;
}
Err(()) => {
return SpillOutcome::Error(SpillErrorKind::IoErr);
}
}
}
}
tracing::debug!(
target: "hf2q::kv_persist::spiller",
enqueued,
"pre_evict: post-layer-loop, attempting snapshot_prompt_cache"
);
let prompt_cache_payload = {
let g = match hook_arc.lock() {
Ok(g) => g,
Err(_) => {
tracing::error!(
target: "hf2q::kv_persist::spiller",
"pre_evict: hook_arc.lock() poisoned at prompt-cache call"
);
return SpillOutcome::Error(SpillErrorKind::CodecErr);
}
};
g.snapshot_prompt_cache()
};
if let Some(body) = prompt_cache_payload {
if !body.is_empty() {
let bh: [u8; 32] = Sha256::digest(&body).into();
let block_hash = BlockHash(bh);
let header = EnvelopeHeader {
format_version: CURRENT_FORMAT_VERSION.0,
model_fingerprint: model_fp,
block_hash,
parent_block_hash: parent,
payload_kind:
crate::serve::kv_persist::prompt_cache_persist::PROMPT_CACHE_PAYLOAD_KIND
.to_string(),
codec_version: 1,
n_tokens: 0, };
let job = WriteJob {
header,
body,
completion_tx: None,
};
let pc_enqueue_result = match self.writer.enqueue(job) {
Ok(()) => Ok(()),
Err(std::sync::mpsc::TrySendError::Full(job)) => {
self.writer.enqueue_blocking(job).map_err(|_| ())
}
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => Err(()),
};
if pc_enqueue_result.is_ok() {
enqueued = enqueued.saturating_add(1);
tracing::debug!(
target: "hf2q::kv_persist::spiller",
"pre_evict: prompt-cache enqueue OK"
);
} else {
tracing::warn!(
target: "hf2q::kv_persist::spiller",
"pre_evict: prompt-cache enqueue FAILED (writer disconnected)"
);
}
}
}
if enqueued == 0 {
SpillOutcome::Skipped
} else {
SpillOutcome::EnqueuedBlocks(enqueued)
}
}
fn post_admit(
&self,
repo: &str,
quant: QuantType,
_engine: &Arc<LoadedEngine<E>>,
) -> RestoreOutcome {
tracing::debug!(
target: "hf2q::kv_persist::spiller",
repo = ?repo,
quant = quant.as_str(),
"post_admit ENTER"
);
let Some(hook_arc) = self.lookup_hook(repo, quant) else {
tracing::debug!(
target: "hf2q::kv_persist::spiller",
"post_admit: lookup_hook=None → Skipped"
);
return RestoreOutcome::Skipped;
};
let model_fp = self.family_model_fp(repo, quant);
let alignment = {
let g = match hook_arc.lock() {
Ok(g) => g,
Err(_) => return RestoreOutcome::Error(RestoreErrorKind::CodecErr),
};
g.block_alignment()
};
if alignment == 0 {
tracing::debug!(
target: "hf2q::kv_persist::spiller",
"post_admit: alignment=0 → Skipped"
);
return RestoreOutcome::Skipped;
}
let mut metas = self.store.index().iter_by_model(&model_fp);
tracing::debug!(
target: "hf2q::kv_persist::spiller",
n_metas = metas.len(),
"post_admit: found metas in index"
);
if metas.is_empty() {
return RestoreOutcome::Skipped;
}
metas.sort_by(|a, b| a.mtime.cmp(&b.mtime).then_with(|| a.hash.0.cmp(&b.hash.0)));
let mut restored: u32 = 0;
for meta in &metas {
if meta.payload_kind
!= crate::serve::kv_persist::prompt_cache_persist::PROMPT_CACHE_PAYLOAD_KIND
{
continue;
}
let body = match self.store.read_block(&meta.hash) {
Ok(b) => b,
Err(_) => continue, };
let pc_result = {
let mut g = match hook_arc.lock() {
Ok(g) => g,
Err(_) => return RestoreOutcome::Error(RestoreErrorKind::CodecErr),
};
g.restore_prompt_cache(&body)
};
if pc_result.is_ok() {
restored = restored.saturating_add(1);
}
}
for meta in metas {
if meta.payload_kind
== crate::serve::kv_persist::prompt_cache_persist::PROMPT_CACHE_PAYLOAD_KIND
{
continue; }
let body = match self.store.read_block(&meta.hash) {
Ok(b) => b,
Err(_e) => {
return RestoreOutcome::Error(RestoreErrorKind::ParityFail);
}
};
let layer_rank: usize = parse_layer_rank(&meta.payload_kind);
let range: Range<u32> = 0..meta.n_tokens;
let restore_result = {
let mut g = match hook_arc.lock() {
Ok(g) => g,
Err(_) => return RestoreOutcome::Error(RestoreErrorKind::CodecErr),
};
g.restore_block(layer_rank, range, &body)
};
match restore_result {
Ok(()) => restored = restored.saturating_add(1),
Err(SpillErrorKind::CodecErr) => {
tracing::warn!(
target: "hf2q::kv_persist::spiller",
layer = layer_rank,
restored,
"post_admit: restore_block CodecErr — bailing"
);
return RestoreOutcome::Error(RestoreErrorKind::CodecErr);
}
Err(SpillErrorKind::IoErr) => {
tracing::debug!(
target: "hf2q::kv_persist::spiller",
layer = layer_rank,
"post_admit: restore_block IoErr (skipped, continuing)"
);
continue;
}
Err(SpillErrorKind::ParityFail) => {
tracing::warn!(
target: "hf2q::kv_persist::spiller",
layer = layer_rank,
restored,
"post_admit: restore_block ParityFail — bailing"
);
return RestoreOutcome::Error(RestoreErrorKind::ParityFail);
}
}
}
tracing::debug!(
target: "hf2q::kv_persist::spiller",
restored,
"post_admit: loop done"
);
if restored == 0 {
RestoreOutcome::Skipped
} else {
RestoreOutcome::RestoredBlocks(restored)
}
}
fn drop_family(&self, repo: &str, quant: QuantType) {
let _ = self.unregister_family(repo, quant);
let registry = {
let g = self
.registry
.read()
.expect("BlockPrefixCacheSpiller::registry RwLock poisoned");
g.clone()
};
if let Some(r) = registry {
let _ = r.unregister(repo, quant);
}
}
}
fn parse_layer_rank(payload_kind: &str) -> usize {
let prefix = "kv-spiller-l";
if let Some(rest) = payload_kind.strip_prefix(prefix) {
rest.parse().unwrap_or(0)
} else {
0
}
}
#[derive(Debug, Default)]
pub struct StubGemma4Spill;
impl KvCacheSpill for StubGemma4Spill {
fn block_alignment(&self) -> u32 {
BLOCK_TOKENS
}
fn snapshot_block(&self, _layer_rank: usize, _range: Range<u32>) -> Option<Vec<u8>> {
None
}
fn restore_block(
&mut self,
_layer_rank: usize,
_range: Range<u32>,
_payload: &[u8],
) -> Result<(), SpillErrorKind> {
Err(SpillErrorKind::CodecErr)
}
}
impl crate::serve::kv_persist::EngineBindable for StubGemma4Spill {
fn bind_engine(&self, _engine_dyn: Arc<dyn std::any::Any + Send + Sync>) {
}
fn unbind_engine(&self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serve::kv_persist::format::{self, BLOCK_TOKENS, CURRENT_FORMAT_VERSION};
use crate::serve::multi_model::LoadedEngine;
use std::process;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex as StdMutex;
use std::thread;
use std::time::{Duration, SystemTime};
fn temp_dir(label: &str) -> std::path::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-spiller-{label}-{pid}-{nanos}-{n}"));
std::fs::create_dir_all(&dir).expect("temp_dir mkdir");
dir
}
fn fresh_substrate(
label: &str,
) -> (
Arc<DiskBlockStore>,
Arc<AsyncWriterHandle>,
std::path::PathBuf,
) {
let dir = temp_dir(label);
let store = Arc::new(DiskBlockStore::new(dir.clone(), 0).expect("DiskBlockStore::new"));
let writer = Arc::new(AsyncWriterHandle::spawn(Arc::clone(&store), 32));
(store, writer, dir)
}
#[derive(Debug)]
struct TestEngine;
fn fresh_engine(repo: &str, quant: QuantType) -> Arc<LoadedEngine<TestEngine>> {
Arc::new(LoadedEngine {
engine: TestEngine,
repo: repo.to_string(),
quant,
bytes_resident: 1 << 30,
loaded_at: SystemTime::now(),
})
}
fn fresh_handle(repo: &str, quant: QuantType) -> LoadedHandle {
LoadedHandle::new(repo, quant.as_str(), 1 << 30)
}
struct MockKvCacheSpill {
align: u32,
snapshot_returns: StdMutex<Vec<Option<Vec<u8>>>>,
restored: StdMutex<Vec<(usize, Range<u32>, Vec<u8>)>>,
restore_error: StdMutex<Option<SpillErrorKind>>,
}
impl MockKvCacheSpill {
fn new(align: u32, snapshots: Vec<Option<Vec<u8>>>) -> Arc<Mutex<Self>> {
Arc::new(Mutex::new(Self {
align,
snapshot_returns: StdMutex::new(snapshots),
restored: StdMutex::new(Vec::new()),
restore_error: StdMutex::new(None),
}))
}
fn force_restore_error(this: &Arc<Mutex<Self>>, kind: SpillErrorKind) {
let g = this.lock().expect("lock mock");
*g.restore_error.lock().unwrap() = Some(kind);
}
fn restored_calls(this: &Arc<Mutex<Self>>) -> Vec<(usize, Range<u32>, Vec<u8>)> {
let g = this.lock().expect("lock mock");
let snapshot = g.restored.lock().unwrap().clone();
snapshot
}
}
impl KvCacheSpill for MockKvCacheSpill {
fn block_alignment(&self) -> u32 {
self.align
}
fn snapshot_block(&self, _layer_rank: usize, _range: Range<u32>) -> Option<Vec<u8>> {
let mut q = self.snapshot_returns.lock().unwrap();
if q.is_empty() {
None
} else {
q.remove(0)
}
}
fn restore_block(
&mut self,
layer_rank: usize,
range: Range<u32>,
payload: &[u8],
) -> Result<(), SpillErrorKind> {
if let Some(kind) = *self.restore_error.lock().unwrap() {
return Err(kind);
}
self.restored
.lock()
.unwrap()
.push((layer_rank, range, payload.to_vec()));
Ok(())
}
}
fn wait_for_index_count(store: &Arc<DiskBlockStore>, expected: usize) {
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while std::time::Instant::now() < deadline {
if store.index().block_count() == expected {
return;
}
thread::sleep(Duration::from_millis(5));
}
panic!(
"writer did not drain to expected={expected}; got {} after 2s",
store.index().block_count()
);
}
#[test]
fn new_spiller_has_zero_registrations() {
let (store, writer, dir) = fresh_substrate("new0");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(store, writer);
assert_eq!(spiller.registered_count(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn register_then_unregister_family_round_trip() {
let (store, writer, dir) = fresh_substrate("reg");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(store, writer);
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook.clone());
assert_eq!(spiller.registered_count(), 1);
let hook2 = MockKvCacheSpill::new(BLOCK_TOKENS, vec![]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook2);
assert_eq!(spiller.registered_count(), 1);
spiller.register_family(
"acme/m1".into(),
QuantType::Q8_0,
MockKvCacheSpill::new(BLOCK_TOKENS, vec![]),
);
assert_eq!(spiller.registered_count(), 2);
assert!(spiller.unregister_family("acme/m1", QuantType::Q4_K_M));
assert_eq!(spiller.registered_count(), 1);
assert!(!spiller.unregister_family("acme/m1", QuantType::Q4_K_M));
assert!(spiller.unregister_family("acme/m1", QuantType::Q8_0));
assert_eq!(spiller.registered_count(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pre_evict_with_no_registered_family_returns_skipped() {
let (store, writer, dir) = fresh_substrate("pre0");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), writer);
let handle = fresh_handle("acme/m1", QuantType::Q4_K_M);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let outcome = spiller.pre_evict(&handle, &engine);
assert!(matches!(outcome, SpillOutcome::Skipped), "got {outcome:?}");
assert_eq!(store.index().block_count(), 0, "no writes occurred");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pre_evict_with_mock_hook_enqueues_blocks() {
let (store, writer, dir) = fresh_substrate("pre_enq");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body: Vec<u8> = (0..1024u32).map(|i| (i & 0xff) as u8).collect();
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body.clone())]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook);
let handle = fresh_handle("acme/m1", QuantType::Q4_K_M);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let outcome = spiller.pre_evict(&handle, &engine);
assert!(
matches!(outcome, SpillOutcome::EnqueuedBlocks(1)),
"got {outcome:?}"
);
wait_for_index_count(&store, 1);
let metas = store.index().snapshot_all();
assert_eq!(metas.len(), 1);
let body_back = store.read_block(&metas[0].hash).expect("read");
assert_eq!(body_back, body, "body bytes round-trip");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pre_evict_with_writer_full_returns_error_io_err() {
let (store, writer, dir) = fresh_substrate("pre_full");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body: Vec<u8> = vec![0xAA; 1024];
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body)]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook);
drop(writer);
let handle = fresh_handle("acme/m1", QuantType::Q4_K_M);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let outcome = spiller.pre_evict(&handle, &engine);
assert!(
matches!(outcome, SpillOutcome::EnqueuedBlocks(1)),
"writer healthy → EnqueuedBlocks(1); got {outcome:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pre_evict_chain_hash_links_blocks() {
let (store, writer, dir) = fresh_substrate("chain");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body_a: Vec<u8> = (0..512u32).flat_map(|i| i.to_le_bytes()).collect();
let hook_a = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body_a.clone())]);
spiller.register_family("acme/a1".into(), QuantType::Q4_K_M, hook_a);
let body_b: Vec<u8> = (0..512u32)
.flat_map(|i| i.wrapping_add(0xDEAD).to_le_bytes())
.collect();
let hook_b = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body_b.clone())]);
spiller.register_family("acme/b2".into(), QuantType::Q4_K_M, hook_b);
let h_a = fresh_handle("acme/a1", QuantType::Q4_K_M);
let e_a = fresh_engine("acme/a1", QuantType::Q4_K_M);
let _ = spiller.pre_evict(&h_a, &e_a);
let h_b = fresh_handle("acme/b2", QuantType::Q4_K_M);
let e_b = fresh_engine("acme/b2", QuantType::Q4_K_M);
let _ = spiller.pre_evict(&h_b, &e_b);
wait_for_index_count(&store, 2);
let metas = store.index().snapshot_all();
assert_eq!(metas.len(), 2);
for m in &metas {
assert_eq!(
m.parent,
ParentBlockHash(None),
"each pre_evict starts a fresh chain"
);
}
assert_ne!(
metas[0].hash, metas[1].hash,
"distinct bodies hash distinct"
);
for m in &metas {
let body_back = store.read_block(&m.hash).expect("read");
let computed_bh: [u8; 32] = Sha256::digest(&body_back).into();
assert_eq!(
BlockHash(computed_bh),
m.hash,
"body sha matches block_hash"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn post_admit_with_no_registered_family_returns_skipped() {
let (store, writer, dir) = fresh_substrate("post0");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(store, writer);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let outcome = spiller.post_admit("acme/m1", QuantType::Q4_K_M, &engine);
assert!(
matches!(outcome, RestoreOutcome::Skipped),
"got {outcome:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn post_admit_with_disk_blocks_calls_restore_for_each() {
let (store, writer, dir) = fresh_substrate("post_disk");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body: Vec<u8> = (0..1024u32).flat_map(|i| i.to_le_bytes()).collect();
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body.clone())]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook.clone());
let handle = fresh_handle("acme/m1", QuantType::Q4_K_M);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let _ = spiller.pre_evict(&handle, &engine);
wait_for_index_count(&store, 1);
let outcome = spiller.post_admit("acme/m1", QuantType::Q4_K_M, &engine);
assert!(
matches!(outcome, RestoreOutcome::RestoredBlocks(1)),
"got {outcome:?}"
);
let calls = MockKvCacheSpill::restored_calls(&hook);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].2, body, "restored bytes byte-exact");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn post_admit_with_zero_disk_blocks_returns_restored_blocks_zero() {
let (store, writer, dir) = fresh_substrate("post_zero");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(store, writer);
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let outcome = spiller.post_admit("acme/m1", QuantType::Q4_K_M, &engine);
assert!(
matches!(outcome, RestoreOutcome::Skipped),
"got {outcome:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn post_admit_with_corrupted_block_returns_error_parity_fail() {
let (store, writer, dir) = fresh_substrate("post_corrupt");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body: Vec<u8> = (0..1024u32).flat_map(|i| i.to_le_bytes()).collect();
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body.clone())]);
spiller.register_family("acme/m1".into(), QuantType::Q4_K_M, hook);
let handle = fresh_handle("acme/m1", QuantType::Q4_K_M);
let engine = fresh_engine("acme/m1", QuantType::Q4_K_M);
let _ = spiller.pre_evict(&handle, &engine);
wait_for_index_count(&store, 1);
let metas = store.index().snapshot_all();
let path = &metas[0].file_path;
let mut bytes = std::fs::read(path).expect("read file");
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
std::fs::write(path, &bytes).expect("mutate body");
let outcome = spiller.post_admit("acme/m1", QuantType::Q4_K_M, &engine);
assert!(
matches!(outcome, RestoreOutcome::Error(RestoreErrorKind::ParityFail)),
"expected ParityFail; got {outcome:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pre_evict_then_post_admit_round_trip_byte_exact() {
let (store, writer, dir) = fresh_substrate("rc1");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body: Vec<u8> = (0..4096u32)
.flat_map(|i| (i.wrapping_mul(0x9E3779B1)).to_le_bytes())
.collect();
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body.clone())]);
spiller.register_family("acme/rc1".into(), QuantType::Q4_K_M, hook.clone());
let handle = fresh_handle("acme/rc1", QuantType::Q4_K_M);
let engine = fresh_engine("acme/rc1", QuantType::Q4_K_M);
let spill = spiller.pre_evict(&handle, &engine);
assert!(
matches!(spill, SpillOutcome::EnqueuedBlocks(1)),
"spill: {spill:?}"
);
wait_for_index_count(&store, 1);
let metas = store.index().snapshot_all();
assert_eq!(metas.len(), 1);
let (header_back, body_back) =
format::read_envelope_body(&metas[0].file_path).expect("envelope round-trips");
assert_eq!(header_back.format_version, CURRENT_FORMAT_VERSION.0);
assert_eq!(body_back, body, "envelope body byte-exact pre-restore");
let restore = spiller.post_admit("acme/rc1", QuantType::Q4_K_M, &engine);
assert!(
matches!(restore, RestoreOutcome::RestoredBlocks(1)),
"restore: {restore:?}"
);
let calls = MockKvCacheSpill::restored_calls(&hook);
assert_eq!(calls.len(), 1, "exactly one restore call");
let (layer_rank, range, restored_bytes) = &calls[0];
assert_eq!(*layer_rank, 0, "single-layer stub");
assert_eq!(*range, 0u32..BLOCK_TOKENS, "full-block range");
assert_eq!(
restored_bytes, &body,
"R-C1: snapshot_block bytes ≡ restore_block bytes byte-for-byte"
);
eprintln!(
"[R-C1] PASS — {} bytes round-tripped byte-exact via spill→disk→restore",
body.len()
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn noop_kv_spiller_default_path_byte_identical_to_pre_iter212() {
use crate::serve::multi_model::NoopKvSpiller;
let noop: NoopKvSpiller = NoopKvSpiller;
let handle = fresh_handle("acme/any", QuantType::Q4_K_M);
let engine = fresh_engine("acme/any", QuantType::Q4_K_M);
let s: SpillOutcome =
<NoopKvSpiller as KvSpiller<TestEngine>>::pre_evict(&noop, &handle, &engine);
assert!(matches!(s, SpillOutcome::Skipped));
let r: RestoreOutcome = <NoopKvSpiller as KvSpiller<TestEngine>>::post_admit(
&noop,
"acme/any",
QuantType::Q4_K_M,
&engine,
);
assert!(matches!(r, RestoreOutcome::Skipped));
}
#[test]
fn post_admit_maps_hook_codec_err_to_restore_codec_err() {
let (store, writer, dir) = fresh_substrate("post_codec_err");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), Arc::clone(&writer));
let body: Vec<u8> = vec![0x42; 256];
let hook = MockKvCacheSpill::new(BLOCK_TOKENS, vec![Some(body)]);
spiller.register_family("acme/codec".into(), QuantType::Q4_K_M, hook.clone());
let handle = fresh_handle("acme/codec", QuantType::Q4_K_M);
let engine = fresh_engine("acme/codec", QuantType::Q4_K_M);
let _ = spiller.pre_evict(&handle, &engine);
wait_for_index_count(&store, 1);
MockKvCacheSpill::force_restore_error(&hook, SpillErrorKind::CodecErr);
let outcome = spiller.post_admit("acme/codec", QuantType::Q4_K_M, &engine);
assert!(
matches!(outcome, RestoreOutcome::Error(RestoreErrorKind::CodecErr)),
"expected CodecErr; got {outcome:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn engine_bindable_stub_spill_noop_does_not_panic() {
use crate::serve::kv_persist::EngineBindable;
let stub = StubGemma4Spill;
let unit: Arc<dyn std::any::Any + Send + Sync> = Arc::new(());
EngineBindable::bind_engine(&stub, unit);
let s: Arc<dyn std::any::Any + Send + Sync> = Arc::new(String::from("hello stub"));
EngineBindable::bind_engine(&stub, s);
struct Bogus(u32);
let b: Arc<dyn std::any::Any + Send + Sync> = Arc::new(Bogus(42));
EngineBindable::bind_engine(&stub, b);
EngineBindable::unbind_engine(&stub);
EngineBindable::unbind_engine(&stub);
}
#[test]
fn stub_gemma4_spill_returns_skipped_on_pre_evict() {
let (store, writer, dir) = fresh_substrate("stub_gemma");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), writer);
let stub: Arc<Mutex<dyn KvCacheSpill>> = Arc::new(Mutex::new(StubGemma4Spill));
spiller.register_family("google/gemma-4".into(), QuantType::Q4_K_M, stub);
let handle = fresh_handle("google/gemma-4", QuantType::Q4_K_M);
let engine = fresh_engine("google/gemma-4", QuantType::Q4_K_M);
let outcome = spiller.pre_evict(&handle, &engine);
assert!(
matches!(outcome, SpillOutcome::Skipped),
"stub should Skip; got {outcome:?}"
);
assert_eq!(store.index().block_count(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn pending_writer_queue_depth_starts_at_zero() {
let (store, writer, dir) = fresh_substrate("k2v3_zero");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), writer);
assert_eq!(
spiller.pending_writer_queue_depth(),
0,
"fresh spiller must report zero pending writer jobs"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_only_inject_pending_spill_increases_depth() {
let (store, writer, dir) = fresh_substrate("k2v3_inject");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), writer);
assert_eq!(spiller.pending_writer_queue_depth(), 0);
spiller.test_only_inject_pending_spill(50);
let d = spiller.pending_writer_queue_depth();
assert!(d <= 50, "depth {d} should be <= 50");
let deadline = std::time::Instant::now() + Duration::from_secs(2);
loop {
let depth_now = spiller.pending_writer_queue_depth();
let blocks_now = store.index().block_count();
if depth_now == 0 && blocks_now > 0 {
assert!(blocks_now > 0, "at least one synthetic job landed");
assert!(
blocks_now <= 50,
"blocks_now={blocks_now} bounded by inject count 50"
);
break;
}
if std::time::Instant::now() > deadline {
panic!(
"writer did not drain inject queue within 2s; depth={depth_now} blocks={blocks_now}"
);
}
thread::sleep(Duration::from_millis(5));
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_only_inject_pending_spill_is_cfg_test_only() {
let (store, writer, dir) = fresh_substrate("k2v3_cfg");
let spiller: BlockPrefixCacheSpiller<TestEngine> =
BlockPrefixCacheSpiller::new(Arc::clone(&store), writer);
spiller.test_only_inject_pending_spill(0); assert_eq!(spiller.pending_writer_queue_depth(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
}