use std::collections::{BTreeSet, HashMap};
use std::hash::Hash;
use std::time::Instant;
#[derive(Debug)]
struct TimedValue<V> {
value: V,
updated_at: Instant,
}
#[derive(Debug)]
pub(crate) struct RecencyMap<K, V> {
entries: HashMap<K, TimedValue<V>>,
order: BTreeSet<(Instant, K)>,
capacity: usize,
}
impl<K, V> RecencyMap<K, V>
where
K: Clone + Eq + Hash + Ord,
{
pub(crate) fn new(capacity: usize) -> Self {
assert!(capacity > 0, "RecencyMap capacity must be non-zero");
Self {
entries: HashMap::with_capacity(capacity),
order: BTreeSet::new(),
capacity,
}
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[cfg(test)]
pub(crate) fn contains_key(&self, key: &K) -> bool {
self.entries.contains_key(key)
}
pub(crate) fn insert(&mut self, key: K, value: V, updated_at: Instant) -> Option<(K, V)> {
if let Some(previous) = self.entries.remove(&key) {
let removed = self.order.remove(&(previous.updated_at, key.clone()));
debug_assert!(removed, "entry must have exactly one recency index record");
}
let evicted = if self.entries.len() >= self.capacity {
self.order.pop_first().map(|(_, oldest_key)| {
let oldest = self
.entries
.remove(&oldest_key)
.expect("recency index key must identify a map entry");
(oldest_key, oldest.value)
})
} else {
None
};
let inserted = self.order.insert((updated_at, key.clone()));
debug_assert!(inserted, "new entry must have a unique recency index key");
self.entries.insert(key, TimedValue { value, updated_at });
debug_assert_eq!(self.entries.len(), self.order.len());
evicted
}
pub(crate) fn update<R>(
&mut self,
key: &K,
update: impl FnOnce(&mut V) -> (R, Instant),
) -> Option<R> {
let (result, previous_at, updated_at) = {
let entry = self.entries.get_mut(key)?;
let previous_at = entry.updated_at;
let (result, updated_at) = update(&mut entry.value);
entry.updated_at = updated_at;
(result, previous_at, updated_at)
};
if previous_at != updated_at {
let removed = self.order.remove(&(previous_at, key.clone()));
debug_assert!(removed, "entry must have exactly one recency index record");
let inserted = self.order.insert((updated_at, key.clone()));
debug_assert!(inserted, "updated entry must have a unique index record");
debug_assert_eq!(self.entries.len(), self.order.len());
}
Some(result)
}
#[cfg(test)]
pub(crate) fn index_len(&self) -> usize {
self.order.len()
}
#[cfg(test)]
pub(crate) fn get(&self, key: &K) -> Option<&V> {
self.entries.get(key).map(|entry| &entry.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[test]
fn evicts_oldest_with_key_as_deterministic_tie_breaker() {
let now = Instant::now();
let mut map = RecencyMap::new(3);
map.insert(3, "three", now);
map.insert(1, "one", now);
map.insert(2, "two", now + Duration::from_nanos(1));
let evicted = map.insert(4, "four", now + Duration::from_nanos(2));
assert_eq!(evicted, Some((1, "one")));
assert!(!map.contains_key(&1));
assert!(map.contains_key(&3));
assert_eq!(map.len(), 3);
assert_eq!(map.index_len(), 3);
}
#[test]
#[should_panic(expected = "RecencyMap capacity must be non-zero")]
fn zero_capacity_is_rejected() {
let _ = RecencyMap::<u8, u8>::new(0);
}
#[test]
fn capacity_one_replaces_the_only_entry() {
let now = Instant::now();
let mut map = RecencyMap::new(1);
assert_eq!(map.insert(1, "one", now), None);
assert_eq!(
map.insert(2, "two", now + Duration::from_nanos(1)),
Some((1, "one"))
);
assert_eq!(map.len(), 1);
assert_eq!(map.index_len(), 1);
assert!(!map.contains_key(&1));
assert_eq!(map.get(&2), Some(&"two"));
}
#[test]
fn unchanged_authenticated_update_time_does_not_refresh_eviction_order() {
let now = Instant::now();
let mut map = RecencyMap::new(2);
map.insert(1, "one", now);
map.insert(2, "two", now + Duration::from_nanos(1));
map.update(&1, |value| (*value, now));
let evicted = map.insert(3, "three", now + Duration::from_nanos(2));
assert_eq!(evicted, Some((1, "one")));
assert!(map.contains_key(&2));
assert!(map.contains_key(&3));
}
#[test]
fn repeated_refresh_and_replacement_do_not_leave_stale_index_entries() {
let now = Instant::now();
let mut map = RecencyMap::new(2);
map.insert(1, "one", now);
for offset in 1..=1_000 {
map.update(&1, |value| (*value, now + Duration::from_nanos(offset)));
}
map.insert(1, "replacement", now + Duration::from_secs(1));
assert_eq!(map.len(), 1);
assert_eq!(map.index_len(), 1);
assert_eq!(map.get(&1), Some(&"replacement"));
}
#[test]
fn capacity_is_never_exceeded_under_churn() {
let now = Instant::now();
let mut map = RecencyMap::new(8);
for key in 0..10_000 {
map.insert(key, key, now + Duration::from_nanos(key));
assert!(map.len() <= 8);
assert_eq!(map.len(), map.index_len());
}
}
proptest! {
#[test]
fn indexed_map_matches_simple_recency_model(
capacity in 1_usize..16,
keys in prop::collection::vec(0_u8..32, 0..2_000),
) {
let start = Instant::now();
let mut map = RecencyMap::new(capacity);
let mut model = Vec::<(u8, u64)>::new();
for (tick, key) in keys.into_iter().enumerate() {
let tick = tick as u64 + 1;
let updated_at = start + Duration::from_nanos(tick);
if map.contains_key(&key) {
map.update(&key, |value| (*value, updated_at));
model.retain(|(candidate, _)| *candidate != key);
} else {
map.insert(key, key, updated_at);
if model.len() >= capacity {
let oldest = model
.iter()
.enumerate()
.min_by_key(|(_, (candidate, timestamp))| (*timestamp, *candidate))
.map(|(index, _)| index)
.unwrap();
model.remove(oldest);
}
}
model.push((key, tick));
prop_assert_eq!(map.len(), model.len());
prop_assert_eq!(map.index_len(), model.len());
for candidate in 0_u8..32 {
prop_assert_eq!(
map.contains_key(&candidate),
model.iter().any(|(key, _)| *key == candidate)
);
}
}
}
}
#[test]
fn mutex_serialized_concurrent_churn_keeps_index_bounded_and_exact() {
let capacity = 64;
let map = Arc::new(Mutex::new(RecencyMap::new(capacity)));
let start = Instant::now();
let workers = (0_u64..8)
.map(|worker| {
let map = Arc::clone(&map);
thread::spawn(move || {
for offset in 0_u64..2_000 {
let key = worker * 2_000 + offset;
let updated_at = start + Duration::from_nanos(key + 1);
map.lock().unwrap().insert(key, key, updated_at);
}
})
})
.collect::<Vec<_>>();
for worker in workers {
worker.join().unwrap();
}
let map = map.lock().unwrap();
assert_eq!(map.len(), capacity);
assert_eq!(map.index_len(), capacity);
}
}