use std::sync::Arc;
use crate::persistent_artrie::eviction::EvictionConfig;
use crate::persistent_artrie::overlay_fault::evict_overlay_nodes;
use crate::persistent_artrie::PersistentARTrie;
use crate::persistent_artrie_core::durability::DurabilityPolicy;
use crate::persistent_artrie_core::overlay::evict::OverlayEvictable;
use crate::MappedDictionary;
fn scratch(prefix: &str) -> tempfile::TempDir {
std::fs::create_dir_all("target/test-tmp").ok();
tempfile::Builder::new()
.prefix(prefix)
.tempdir_in("target/test-tmp")
.expect("scratch tempdir under target/test-tmp")
}
fn is_cold(path: &[u8]) -> bool {
path.first() == Some(&b'c')
}
fn evict_cold_overlay<V, S>(trie: &PersistentARTrie<V, S>, budget_bytes: usize) -> usize
where
V: crate::value::DictionaryValue,
S: crate::persistent_artrie::block_storage::BlockStorage,
{
let coordinator = match trie
.eviction_coordinator
.lock()
.expect("eviction_coordinator mutex poisoned")
.as_ref()
{
Some(c) => Arc::clone(c),
None => return 0,
};
coordinator
.force_eviction_bytes(budget_bytes, |cands| {
let filtered: Vec<_> = cands.into_iter().filter(|(_, p, _)| is_cold(p)).collect();
evict_overlay_nodes(trie, filtered, 4)
})
.0
}
#[test]
fn byte_evict_then_reload_returns_exact_values() {
let dir = scratch("byte-oe3-evict-reload");
let path = dir.path().join("oe3.part");
let cold: Vec<(String, u64)> = (0..40)
.map(|i| (format!("cold-{i:04}"), 1000 + i as u64))
.collect();
let live: Vec<(String, u64)> = (0..20)
.map(|i| (format!("warm-{i:04}"), 5000 + i as u64))
.collect();
{
let mut trie = PersistentARTrie::<u64>::create(&path).expect("create");
trie.set_durability_policy(DurabilityPolicy::Immediate);
trie.install_overlay();
trie.bench_enable_eviction(EvictionConfig::without_memory_monitor())
.expect("bench_enable_eviction");
for (t, v) in cold.iter().chain(live.iter()) {
trie.try_increment_cas_durable(t.as_bytes(), *v)
.expect("durable increment");
}
trie.bench_immutable_checkpoint_with_eviction()
.expect("checkpoint with eviction");
assert!(
trie.evictable_node_count().unwrap_or(0) > 0,
"byte registry must be published (evictable_node_count > 0) — registration gap"
);
let trie = Arc::new(trie);
let mut evicted = 0usize;
for _ in 0..16 {
evicted += evict_cold_overlay(&*trie, 1 << 20);
}
assert!(
evicted > 0,
"OE3-twin: no cold byte nodes evicted (driver no-op / registration gap)"
);
drop(trie);
}
let reopened = PersistentARTrie::<u64>::open(&path).expect("reopen");
for (t, v) in cold.iter().chain(live.iter()) {
assert_eq!(
MappedDictionary::get_value(&reopened, t),
Some(*v),
"byte term {t:?} value wrong after evict+reload (expected {v})"
);
}
}
#[test]
fn byte_overwrite_since_checkpoint_is_not_evicted_to_stale_image() {
use crate::persistent_artrie_core::overlay::evict::OverlayEvictOutcome;
let dir = scratch("byte-oe5-overwrite-guard");
let path = dir.path().join("oe5.part");
let mut trie = PersistentARTrie::<u64>::create(&path).expect("create");
trie.set_durability_policy(DurabilityPolicy::Immediate);
trie.install_overlay();
trie.bench_enable_eviction(EvictionConfig::without_memory_monitor())
.expect("bench_enable_eviction");
trie.try_increment_cas_durable(b"cold-stable", 10)
.expect("inc stable");
trie.try_increment_cas_durable(b"cold-rewritten", 20)
.expect("inc rewritten");
trie.bench_immutable_checkpoint_with_eviction()
.expect("checkpoint with eviction");
let coordinator = trie
.eviction_coordinator
.lock()
.expect("eviction_coordinator mutex poisoned")
.as_ref()
.map(Arc::clone)
.expect("coordinator present");
let captured: std::cell::RefCell<
std::collections::HashMap<Vec<u8>, crate::persistent_artrie::swizzled_ptr::SwizzledPtr>,
> = std::cell::RefCell::new(std::collections::HashMap::new());
coordinator.force_eviction_bytes(1 << 20, |cands| {
for (_, p, ptr) in cands {
captured.borrow_mut().insert(p, ptr);
}
(0, 0)
});
let caps = captured.into_inner();
let stable_ptr = caps
.get(b"cold-stable".as_slice())
.expect("cold-stable leaf registered")
.clone();
let rewritten_ptr = caps
.get(b"cold-rewritten".as_slice())
.expect("cold-rewritten leaf registered")
.clone();
trie.try_increment_cas_durable(b"cold-rewritten", 5)
.expect("overwrite");
assert_eq!(
trie.get_lockfree(b"cold-rewritten"),
Some(25),
"overwrite stuck (20+5)"
);
assert_eq!(
trie.evict_overlay_node_at_path(b"cold-rewritten", rewritten_ptr),
OverlayEvictOutcome::NotEvictable,
"1c guard: a node overwritten since checkpoint must NOT be evicted to its stale image"
);
assert_eq!(
trie.get_lockfree(b"cold-rewritten"),
Some(25),
"the NEW value survives (not lost to a stale-image eviction)"
);
assert_eq!(
trie.evict_overlay_node_at_path(b"cold-stable", stable_ptr),
OverlayEvictOutcome::Evicted,
"an un-overwritten registered node still evicts (guard does not over-reject)"
);
assert_eq!(
trie.get_lockfree(b"cold-stable"),
Some(10),
"the evicted node faults back to its exact durable value"
);
}
#[test]
fn byte_evict_faultin_evict_thrash_terminates() {
let dir = scratch("byte-oe8-thrash");
let path = dir.path().join("oe8.part");
let cold: Vec<(String, u64)> = (0..24)
.map(|i| (format!("cold-{i:03}"), 700 + i as u64))
.collect();
let mut trie = PersistentARTrie::<u64>::create(&path).expect("create");
trie.set_durability_policy(DurabilityPolicy::Immediate);
trie.install_overlay();
trie.bench_enable_eviction(EvictionConfig::without_memory_monitor())
.expect("bench_enable_eviction");
for (t, v) in &cold {
trie.try_increment_cas_durable(t.as_bytes(), *v)
.expect("durable increment");
}
trie.bench_immutable_checkpoint_with_eviction()
.expect("checkpoint with eviction");
let trie = Arc::new(trie);
let mut total_evicted = 0usize;
for round in 0..8 {
let mut evicted = 0usize;
for _ in 0..8 {
evicted += evict_cold_overlay(&*trie, 1 << 20);
}
total_evicted += evicted;
for (t, v) in &cold {
assert_eq!(
trie.get_lockfree(t.as_bytes()),
Some(*v),
"OE8-twin: round {round} term {t:?} wrong value after evict/faultin thrash"
);
}
}
assert!(
total_evicted > 0,
"OE8-twin: thrash never evicted anything (vacuous — re-faulted nodes must become \
re-evictable for the thrash to be meaningful)"
);
}
#[test]
fn oe9_byte_iter_prefix_faults_evicted_subtree_no_under_report() {
let dir = scratch("oe9-byte-prefix-fault");
let path = dir.path().join("oe9b.artb");
let mut owned = PersistentARTrie::<u64>::create(&path).expect("create");
owned.set_durability_policy(DurabilityPolicy::Immediate);
owned.install_overlay();
owned
.bench_enable_eviction(EvictionConfig::without_memory_monitor())
.expect("bench_enable_eviction");
let under_ab: [(&[u8], u64); 4] = [(b"abcd", 1), (b"abce", 2), (b"abcfg", 3), (b"abxy", 4)];
for (t, v) in under_ab.iter() {
owned.try_increment_cas_durable(t, *v).expect("inc");
}
owned.try_increment_cas_durable(b"az", 99).expect("sibling");
owned
.bench_immutable_checkpoint_with_eviction()
.expect("checkpoint with eviction");
let coordinator = owned
.eviction_coordinator
.lock()
.expect("eviction_coordinator mutex poisoned")
.as_ref()
.map(std::sync::Arc::clone)
.expect("coordinator present");
let evicted = coordinator
.force_eviction_bytes(1 << 20, |cands| {
let filtered: Vec<_> = cands
.into_iter()
.filter(|(_, p, _)| p.starts_with(b"abc"))
.collect();
evict_overlay_nodes(&owned, filtered, 4)
})
.0;
assert!(
evicted > 0,
"OE9 byte: expected to evict the 'abc' subtree (0 = driver no-op)"
);
let mut got: Vec<Vec<u8>> = owned
.iter_prefix(b"ab")
.expect("prefix 'ab' present")
.collect();
got.sort();
assert_eq!(
got,
vec![
b"abcd".to_vec(),
b"abce".to_vec(),
b"abcfg".to_vec(),
b"abxy".to_vec()
],
"byte iter_prefix MUST fault the evicted subtree (no under-report)"
);
assert!(
!got.iter().any(|t| t.as_slice() == b"az"),
"prefix scoping: 'az' is outside 'ab' and must be excluded"
);
let mut gv: Vec<(Vec<u8>, u64)> = owned
.iter_prefix_with_values(b"ab")
.expect("prefix present")
.collect();
gv.sort();
assert_eq!(
gv,
vec![
(b"abcd".to_vec(), 1),
(b"abce".to_vec(), 2),
(b"abcfg".to_vec(), 3),
(b"abxy".to_vec(), 4)
],
"byte iter_prefix_with_values MUST fault evicted finals with exact counters"
);
}
#[test]
fn phase7_byte_resident_budget_checkpoint_tail_evicts_to_budget() {
fn run(budget: Option<usize>) -> (usize, usize, bool) {
let dir = scratch("phase7-byte-budget");
let path = dir.path().join("p7b.artb");
let mut owned = PersistentARTrie::<u64>::create(&path).expect("create");
owned.set_durability_policy(DurabilityPolicy::Immediate);
owned.install_overlay();
let config = EvictionConfig {
resident_budget_bytes: budget,
..EvictionConfig::without_memory_monitor()
};
owned
.bench_enable_eviction(config)
.expect("bench_enable_eviction");
let terms: Vec<String> = (0..40).map(|i| format!("ngram-{i:03}")).collect();
for (i, t) in terms.iter().enumerate() {
owned
.try_increment_cas_durable(t.as_bytes(), (i + 1) as u64)
.expect("inc");
}
owned
.bench_immutable_checkpoint_with_eviction()
.expect("ckpt1");
let count1 = owned.evictable_node_count().unwrap_or(0);
owned
.bench_immutable_checkpoint_with_eviction()
.expect("ckpt2");
let count2 = owned.evictable_node_count().unwrap_or(0);
let all_present = terms
.iter()
.enumerate()
.all(|(i, t)| MappedDictionary::get_value(&owned, t.as_str()) == Some((i + 1) as u64));
(count1, count2, all_present)
}
let (t1, t2, t_lossless) = run(Some(2000));
assert!(t1 > 0, "byte checkpoint #1 must register the full overlay");
assert!(
t2 < t1,
"byte budget tail must evict cold nodes ({t1} → {t2})"
);
assert!(t_lossless, "byte budget eviction must be LOSSLESS");
let (c1, c2, c_lossless) = run(None);
assert_eq!(c1, c2, "byte: no budget → no tail eviction");
assert!(c_lossless, "byte control: all terms present");
assert!(
t2 < c2,
"byte budgeted retains fewer than control ({t2} < {c2})"
);
}