use std::{
borrow::Borrow,
collections::{HashMap, VecDeque},
hash::Hash,
};
pub(crate) trait WeakEntry {
fn is_closed(&self) -> bool;
fn same_channel(&self, other: &Self) -> bool;
}
const GC_PROBE: usize = 2;
pub(crate) struct WeakCache<K, V> {
map: HashMap<K, V>,
ring: VecDeque<(K, V)>,
}
impl<K, V> Default for WeakCache<K, V> {
fn default() -> Self {
Self {
map: HashMap::new(),
ring: VecDeque::new(),
}
}
}
impl<K, V> WeakCache<K, V>
where
K: Clone + Eq + Hash,
V: WeakEntry + Clone,
{
pub fn get<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
match self.map.get(key) {
Some(v) if !v.is_closed() => Some(v.clone()),
Some(_) => {
self.map.remove(key);
None
}
None => None,
}
}
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
if let Some(existing) = self.map.get(&key)
&& !existing.is_closed()
{
return Some(existing.clone());
}
self.map.insert(key.clone(), value.clone());
self.gc();
self.ring.push_back((key, value));
None
}
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.ring.retain(|(k, _)| k.borrow() != key);
self.map.remove(key)
}
pub fn contains_key<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.map.contains_key(key)
}
pub fn iter(&self) -> impl Iterator<Item = &V> {
self.map.values()
}
fn gc(&mut self) {
for _ in 0..self.ring.len().min(GC_PROBE) {
let Some((key, entry)) = self.ring.pop_front() else {
break;
};
enum Probe {
Keep,
Reclaim,
Drop,
}
let probe = match self.map.get(&key) {
Some(cur) if !cur.same_channel(&entry) => Probe::Drop,
Some(cur) if cur.is_closed() => Probe::Reclaim,
Some(_) => Probe::Keep,
None => Probe::Drop,
};
match probe {
Probe::Keep => self.ring.push_back((key, entry)),
Probe::Reclaim => {
self.map.remove(&key);
}
Probe::Drop => {}
}
}
}
}
#[cfg(test)]
impl<K, V> WeakCache<K, V> {
pub fn len(&self) -> usize {
self.map.len()
}
pub fn ring_len(&self) -> usize {
self.ring.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
#[derive(Clone)]
struct Fake(Arc<AtomicBool>);
impl Fake {
fn open() -> Self {
Self(Arc::new(AtomicBool::new(false)))
}
fn close(&self) {
self.0.store(true, Ordering::SeqCst);
}
}
impl WeakEntry for Fake {
fn is_closed(&self) -> bool {
self.0.load(Ordering::SeqCst)
}
fn same_channel(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[test]
fn get_drops_closed() {
let mut cache = WeakCache::default();
let entry = Fake::open();
cache.insert("a", entry.clone());
assert!(cache.get("a").is_some());
entry.close();
assert!(cache.get("a").is_none(), "a closed entry must not resolve");
assert_eq!(cache.len(), 0, "the closed entry is dropped on lookup");
}
#[test]
fn remove_prunes_ring() {
let mut cache = WeakCache::default();
cache.insert("a", Fake::open());
cache.insert("b", Fake::open());
assert_eq!(cache.ring_len(), 2);
assert!(cache.remove("a").is_some());
assert_eq!(cache.len(), 1);
assert_eq!(cache.ring_len(), 1, "remove must prune the ring, not just the map");
assert!(cache.remove("a").is_none(), "already removed");
}
#[test]
fn distinct_one_shot_paths_stay_bounded() {
let mut cache = WeakCache::default();
for i in 0..1000 {
let entry = Fake::open();
cache.insert(i, entry.clone());
entry.close();
}
assert!(cache.len() <= GC_PROBE + 1, "map grew unbounded: {}", cache.len());
assert!(
cache.ring_len() <= GC_PROBE + 1,
"ring grew unbounded: {}",
cache.ring_len()
);
}
#[test]
fn same_key_churn_does_not_leak() {
let mut cache = WeakCache::default();
for _ in 0..1000 {
let entry = Fake::open();
cache.insert("hot", entry.clone());
entry.close();
}
assert_eq!(cache.len(), 1, "one key must map to at most one entry");
assert!(
cache.ring_len() <= 2,
"superseded twins must not accumulate: {}",
cache.ring_len()
);
}
#[test]
fn insert_dedups_live_replaces_closed() {
let mut cache = WeakCache::default();
let first = Fake::open();
assert!(
cache.insert("a", first.clone()).is_none(),
"first insert takes the slot"
);
let second = Fake::open();
let existing = cache.insert("a", second.clone()).expect("live entry returned");
assert!(existing.same_channel(&first), "the existing live entry wins");
assert!(cache.get("a").expect("still present").same_channel(&first));
first.close();
let third = Fake::open();
assert!(cache.insert("a", third.clone()).is_none(), "closed entry is replaced");
assert!(cache.get("a").expect("present").same_channel(&third));
}
}