use super::*;
pub(super) fn expect_kv_handle(v: Option<&Value>) -> Result<u64, String> {
match v {
Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
Some(other) => Err(format!("expected Kv handle (Int), got {other:?}")),
None => Err("missing Kv argument".into()),
}
}
pub(super) fn kv_registry() -> &'static Mutex<KvRegistry> {
static REGISTRY: OnceLock<Mutex<KvRegistry>> = OnceLock::new();
REGISTRY.get_or_init(|| Mutex::new(KvRegistry::with_capacity(MAX_KV_HANDLES)))
}
pub(super) const MAX_KV_HANDLES: usize = 256;
pub(crate) struct KvRegistry {
pub(super) entries: indexmap::IndexMap<u64, sled::Db>,
pub(super) cap: usize,
}
impl KvRegistry {
pub(crate) fn with_capacity(cap: usize) -> Self {
Self { entries: indexmap::IndexMap::new(), cap }
}
pub(crate) fn insert(&mut self, handle: u64, db: sled::Db) {
if self.entries.len() >= self.cap {
self.entries.shift_remove_index(0);
}
self.entries.insert(handle, db);
}
pub(crate) fn touch_get(&mut self, handle: u64) -> Option<&sled::Db> {
let idx = self.entries.get_index_of(&handle)?;
self.entries.move_index(idx, self.entries.len() - 1);
self.entries.get(&handle)
}
pub(crate) fn remove(&mut self, handle: u64) {
self.entries.shift_remove(&handle);
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize { self.entries.len() }
}
pub(super) fn next_kv_handle() -> u64 {
static COUNTER: AtomicU64 = AtomicU64::new(1);
COUNTER.fetch_add(1, Ordering::SeqCst)
}
#[cfg(test)]
mod kv_registry_tests {
use super::KvRegistry;
fn fresh_db(tag: &str) -> sled::Db {
let dir = std::env::temp_dir().join(format!(
"lex-kv-reg-{}-{}-{}",
std::process::id(),
tag,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
sled::open(&dir).expect("sled open")
}
#[test]
fn insert_and_get_round_trip() {
let mut r = KvRegistry::with_capacity(4);
r.insert(1, fresh_db("a"));
assert!(r.touch_get(1).is_some());
assert!(r.touch_get(2).is_none());
}
#[test]
fn cap_evicts_lru_on_overflow() {
let mut r = KvRegistry::with_capacity(2);
r.insert(1, fresh_db("c1"));
r.insert(2, fresh_db("c2"));
let _ = r.touch_get(1);
r.insert(3, fresh_db("c3"));
assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
assert!(r.touch_get(3).is_some(), "3 just inserted, should survive");
assert_eq!(r.len(), 2);
}
#[test]
fn cap_with_no_touches_evicts_in_insertion_order() {
let mut r = KvRegistry::with_capacity(2);
r.insert(10, fresh_db("f1"));
r.insert(20, fresh_db("f2"));
r.insert(30, fresh_db("f3"));
assert!(r.touch_get(10).is_none());
assert!(r.touch_get(20).is_some());
assert!(r.touch_get(30).is_some());
}
#[test]
fn remove_drops_entry() {
let mut r = KvRegistry::with_capacity(4);
r.insert(1, fresh_db("r1"));
r.remove(1);
assert!(r.touch_get(1).is_none());
assert_eq!(r.len(), 0);
}
#[test]
fn remove_unknown_handle_is_noop() {
let mut r = KvRegistry::with_capacity(4);
r.insert(1, fresh_db("u1"));
r.remove(999);
assert!(r.touch_get(1).is_some());
}
#[test]
fn many_inserts_stay_bounded_at_cap() {
let cap = 8;
let mut r = KvRegistry::with_capacity(cap);
for i in 0..(cap as u64 * 3) {
r.insert(i, fresh_db(&format!("b{i}")));
assert!(r.len() <= cap);
}
assert_eq!(r.len(), cap);
}
}