use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
use crate::SizeGauge;
struct Timestamped<V> {
value: V,
last_seen_secs: u64,
seq: u64,
}
pub struct BoundedMap<K, V> {
inner: HashMap<K, Timestamped<V>>,
order: VecDeque<(K, u64)>,
capacity: usize,
idle_ttl_secs: u64,
sweep_interval: usize,
inserts_since_sweep: usize,
next_seq: u64,
gauge: SizeGauge,
}
impl<K: Eq + Hash + Clone, V> BoundedMap<K, V> {
pub fn new(capacity: usize, idle_ttl_secs: u64, gauge: SizeGauge) -> Self {
Self {
inner: HashMap::new(),
order: VecDeque::new(),
capacity,
idle_ttl_secs,
sweep_interval: 256,
inserts_since_sweep: 0,
next_seq: 0,
gauge,
}
}
pub fn insert(&mut self, key: K, value: V, now_secs: u64) -> Option<(K, V)> {
if self.capacity == 0 {
return Some((key, value));
}
self.inserts_since_sweep = self.inserts_since_sweep.saturating_add(1);
if self.inserts_since_sweep >= self.sweep_interval {
self.sweep_idle(now_secs);
self.inserts_since_sweep = 0;
}
let mut evicted = None;
if !self.inner.contains_key(&key) && self.inner.len() >= self.capacity {
evicted = self.evict_oldest();
}
let seq = self.next_seq;
self.next_seq = self.next_seq.saturating_add(1);
self.inner.insert(
key.clone(),
Timestamped {
value,
last_seen_secs: now_secs,
seq,
},
);
self.order.push_back((key, seq));
if self.order.len() > self.capacity.saturating_mul(2) {
self.compact_order();
}
self.gauge.set(self.inner.len());
evicted
}
pub fn get(&mut self, key: &K, now_secs: u64) -> Option<&V> {
match self.inner.get_mut(key) {
Some(entry) => {
entry.last_seen_secs = now_secs;
Some(&entry.value)
}
None => None,
}
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn capacity(&self) -> usize {
self.capacity
}
fn is_live_position(&self, key: &K, seq: u64) -> bool {
matches!(self.inner.get(key), Some(entry) if entry.seq == seq)
}
fn compact_order(&mut self) {
let mut live: Vec<(K, u64)> = self
.inner
.iter()
.map(|(k, entry)| (k.clone(), entry.seq))
.collect();
live.sort_by_key(|(_, seq)| *seq);
self.order = live.into_iter().collect();
}
fn sweep_idle(&mut self, now_secs: u64) {
if self.idle_ttl_secs == 0 {
return;
}
let floor = now_secs.saturating_sub(self.idle_ttl_secs);
self.inner.retain(|_, entry| entry.last_seen_secs > floor);
let inner = &self.inner;
self.order
.retain(|(k, seq)| matches!(inner.get(k), Some(entry) if entry.seq == *seq));
self.gauge.set(self.inner.len());
}
fn evict_oldest(&mut self) -> Option<(K, V)> {
while let Some((candidate, seq)) = self.order.pop_front() {
if self.is_live_position(&candidate, seq) {
if let Some(entry) = self.inner.remove(&candidate) {
self.gauge.set(self.inner.len());
return Some((candidate, entry.value));
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use crate::{BoundedMap, SizeGauge};
#[test]
fn insert_evicts_oldest_at_cap_and_returns_it() {
let gauge = SizeGauge::new();
let mut map: BoundedMap<u32, u32> = BoundedMap::new(2, 0, gauge.clone());
assert_eq!(map.insert(1, 10, 0), None);
assert_eq!(map.insert(2, 20, 0), None);
assert_eq!(map.len(), 2);
assert_eq!(map.insert(3, 30, 0), Some((1, 10)));
assert_eq!(map.len(), 2);
assert_eq!(gauge.get(), 2);
assert_eq!(map.get(&1, 0), None);
assert_eq!(map.get(&3, 0), Some(&30));
}
#[test]
fn reinsert_moves_key_to_newest_so_refreshed_key_survives_eviction() {
let gauge = SizeGauge::new();
let mut map: BoundedMap<u32, u32> = BoundedMap::new(2, 0, gauge.clone());
map.insert(1, 10, 0); map.insert(2, 20, 0);
map.insert(1, 11, 0);
assert_eq!(
map.insert(3, 30, 0),
Some((2, 20)),
"the genuinely-oldest key must be evicted, not the refreshed one"
);
assert_eq!(map.get(&1, 0), Some(&11), "refreshed key must survive");
assert_eq!(map.get(&2, 0), None, "true-oldest key must be gone");
assert_eq!(map.get(&3, 0), Some(&30));
assert_eq!(map.len(), 2);
assert_eq!(gauge.get(), 2);
}
#[test]
fn many_reinserts_do_not_leak_order_or_breach_capacity() {
let gauge = SizeGauge::new();
let mut map: BoundedMap<u32, u32> = BoundedMap::new(4, 0, gauge.clone());
for round in 0..1000u32 {
let key = round % 4; let _ = map.insert(key, round, 0);
assert!(map.len() <= 4, "capacity breached: {}", map.len());
assert_eq!(map.len(), gauge.get(), "gauge desynced from len");
}
for key in 0..4u32 {
assert!(map.get(&key, 0).is_some(), "live key {key} lost");
}
}
#[test]
fn sweep_idle_drops_expired_keeps_fresh() {
let gauge = SizeGauge::new();
let mut map: BoundedMap<u32, u32> = BoundedMap::new(4096, 10, gauge.clone());
map.insert(1, 10, 100); map.insert(2, 20, 100);
assert_eq!(map.get(&2, 105), Some(&20));
for k in 1000..1256u32 {
map.insert(k, k, 115);
}
assert_eq!(map.get(&1, 115), None);
let gauge2 = SizeGauge::new();
let mut map2: BoundedMap<u32, u32> = BoundedMap::new(4096, 10, gauge2);
map2.insert(1, 10, 100);
assert_eq!(map2.get(&1, 200), Some(&10)); for k in 1000..1256u32 {
map2.insert(k, k, 205); }
assert_eq!(map2.get(&1, 205), Some(&10));
}
#[test]
fn zero_capacity_disables_and_hands_pair_back() {
let gauge = SizeGauge::new();
let mut map: BoundedMap<u32, u32> = BoundedMap::new(0, 0, gauge.clone());
assert_eq!(map.insert(1, 10, 0), Some((1, 10)));
assert_eq!(map.len(), 0);
assert_eq!(gauge.get(), 0);
}
}
#[cfg(test)]
mod prop {
use crate::{BoundedMap, SizeGauge};
use proptest::prelude::*;
#[derive(Debug, Clone)]
enum Op {
Insert(u8, u8, u64),
Get(u8, u64),
}
fn op_strategy() -> impl Strategy<Value = Op> {
prop_oneof![
(any::<u8>(), any::<u8>(), 0u64..1000).prop_map(|(k, v, t)| Op::Insert(k, v, t)),
(any::<u8>(), 0u64..1000).prop_map(|(k, t)| Op::Get(k, t)),
]
}
proptest! {
#[test]
fn gauge_tracks_len_and_len_never_exceeds_capacity(
cap in 1usize..64,
ttl in 0u64..50,
ops in prop::collection::vec(op_strategy(), 0..500),
) {
let gauge = SizeGauge::new();
let mut map: BoundedMap<u8, u8> = BoundedMap::new(cap, ttl, gauge.clone());
for op in ops {
match op {
Op::Insert(k, v, t) => { let _ = map.insert(k, v, t); }
Op::Get(k, t) => { let _ = map.get(&k, t); }
}
prop_assert!(map.len() <= cap, "len {} exceeded cap {}", map.len(), cap);
prop_assert_eq!(map.len(), gauge.get(), "gauge desynced from len");
}
}
}
}