#![forbid(unsafe_code)]
use std::{
borrow::Borrow,
hash::Hash,
sync::atomic::{AtomicUsize, Ordering},
time::Instant,
};
use dur::Duration;
use nix::errno::Errno;
use crate::hash::SydRandomState;
const MINIMUM_VACUUM_SIZE: usize = 8;
#[derive(Debug)]
struct ExpiryValue<V> {
value: V,
inserted: Instant,
ttl: Duration,
}
impl<V> ExpiryValue<V> {
fn not_expired(&self) -> bool {
self.inserted.elapsed() < self.ttl.into()
}
}
pub struct ExpiringMap<K, V>
where
K: Eq + Hash,
{
inner: scc::HashMap<K, ExpiryValue<V>, SydRandomState>,
last_size: AtomicUsize,
}
impl<K: Eq + Hash, V> Default for ExpiringMap<K, V> {
fn default() -> Self {
Self::new()
}
}
impl<K: Eq + Hash, V> ExpiringMap<K, V> {
pub fn new() -> Self {
Self {
inner: scc::HashMap::with_hasher(SydRandomState::new()),
last_size: AtomicUsize::new(MINIMUM_VACUUM_SIZE),
}
}
pub fn vacuum(&self) {
self.inner.retain_sync(|_, expiry| expiry.not_expired());
let len = self.inner.len();
if len > MINIMUM_VACUUM_SIZE {
self.last_size.store(len, Ordering::Relaxed);
} else {
self.last_size.store(MINIMUM_VACUUM_SIZE, Ordering::Relaxed);
}
}
pub fn vacuum_if_needed(&self) {
let last = self.last_size.load(Ordering::Relaxed);
let len = self.inner.len();
if last.saturating_mul(3).saturating_div(2) < len {
self.vacuum();
}
}
pub fn try_insert(&self, key: K, value: V, ttl: Duration) -> Result<Option<V>, Errno> {
self.vacuum_if_needed();
let _reserve = self.inner.reserve(1).ok_or(Errno::ENOMEM)?;
let entry = ExpiryValue {
value,
inserted: Instant::now(),
ttl,
};
match self.inner.entry_sync(key) {
scc::hash_map::Entry::Occupied(mut occupied) => {
let old = std::mem::replace(occupied.get_mut(), entry);
Ok(if old.not_expired() {
Some(old.value)
} else {
None
})
}
scc::hash_map::Entry::Vacant(vacant) => {
vacant.insert_entry(entry);
Ok(None)
}
}
}
pub fn update<Q, R, F>(&self, key: &Q, f: F) -> Option<R>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
F: FnOnce(&mut V) -> R,
{
self.inner
.update_sync(key, |_, v| {
if v.not_expired() {
Some(f(&mut v.value))
} else {
None
}
})
.flatten()
}
pub fn try_upsert<R, F>(
&self,
key: K,
update_fn: F,
value: V,
ttl: Duration,
) -> Result<Option<R>, Errno>
where
F: FnOnce(&mut V) -> R,
{
self.vacuum_if_needed();
let _reserve = self.inner.reserve(1).ok_or(Errno::ENOMEM)?;
match self.inner.entry_sync(key) {
scc::hash_map::Entry::Occupied(mut occupied) => {
let ev = occupied.get_mut();
if ev.not_expired() {
Ok(Some(update_fn(&mut ev.value)))
} else {
*ev = ExpiryValue {
value,
inserted: Instant::now(),
ttl,
};
Ok(None)
}
}
scc::hash_map::Entry::Vacant(vacant) => {
vacant.insert_entry(ExpiryValue {
value,
inserted: Instant::now(),
ttl,
});
Ok(None)
}
}
}
pub fn contains_key<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.inner
.read_sync(key, |_, v| v.not_expired())
.unwrap_or(false)
}
pub fn get<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
V: Copy,
{
self.inner
.read_sync(
key,
|_, v| {
if v.not_expired() {
Some(v.value)
} else {
None
}
},
)
.flatten()
}
pub fn remove<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.inner
.remove_sync(key)
.map(|(_, v)| v.not_expired())
.unwrap_or(false)
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn len(&self) -> usize {
self.inner.len()
}
}
#[cfg(test)]
mod tests {
use std::{sync::Arc, thread};
use dur::Duration;
use super::*;
#[test]
fn test_expirymap_1() {
let map: ExpiringMap<String, u8> = ExpiringMap::new();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn test_expirymap_2() {
let map = ExpiringMap::new();
assert!(map
.try_insert("key", "value", Duration::from_secs(5))
.unwrap()
.is_none());
assert_eq!(map.get(&"key"), Some("value"));
}
#[test]
fn test_expirymap_3() {
let map = ExpiringMap::new();
map.try_insert("key", "value", Duration::from_millis(50))
.unwrap();
assert_eq!(map.get(&"key"), Some("value"));
thread::sleep(Duration::from_millis(75).into());
assert!(map.get(&"key").is_none());
}
#[test]
fn test_expirymap_4() {
let map = ExpiringMap::new();
map.try_insert("key", 42u8, Duration::from_secs(5)).unwrap();
assert!(map.contains_key(&"key"));
assert!(!map.contains_key(&"other"));
}
#[test]
fn test_expirymap_5() {
let map = ExpiringMap::new();
map.try_insert("key", 42u8, Duration::from_millis(50))
.unwrap();
thread::sleep(Duration::from_millis(75).into());
assert!(!map.contains_key(&"key"));
}
#[test]
fn test_expirymap_6() {
let map = ExpiringMap::new();
map.try_insert("key", 42u8, Duration::from_secs(5)).unwrap();
assert!(map.remove(&"key"));
assert!(!map.contains_key(&"key"));
}
#[test]
fn test_expirymap_7() {
let map: ExpiringMap<&str, u8> = ExpiringMap::new();
assert!(!map.remove(&"key"));
}
#[test]
fn test_expirymap_8() {
let map = ExpiringMap::new();
map.try_insert("key", 42u8, Duration::from_millis(50))
.unwrap();
thread::sleep(Duration::from_millis(75).into());
assert!(!map.remove(&"key"));
}
#[test]
fn test_expirymap_9() {
let map = ExpiringMap::new();
assert!(map
.try_insert("key", "x", Duration::from_secs(5))
.unwrap()
.is_none());
assert_eq!(
map.try_insert("key", "y", Duration::from_secs(5)).unwrap(),
Some("x")
);
assert_eq!(map.get(&"key"), Some("y"));
}
#[test]
fn test_expirymap_10() {
let map = ExpiringMap::new();
map.try_insert("key", "x", Duration::from_secs(0)).unwrap();
assert!(map
.try_insert("key", "y", Duration::from_secs(5))
.unwrap()
.is_none());
assert_eq!(map.get(&"key"), Some("y"));
}
#[test]
fn test_expirymap_11() {
let map = ExpiringMap::new();
map.try_insert("key", 1u8, Duration::from_secs(5)).unwrap();
let result = map.update(&"key", |v| {
*v = v.saturating_add(1);
*v
});
assert_eq!(result, Some(2));
assert_eq!(map.get(&"key"), Some(2));
}
#[test]
fn test_expirymap_12() {
let map = ExpiringMap::new();
map.try_insert("key", 1u8, Duration::from_millis(50))
.unwrap();
thread::sleep(Duration::from_millis(75).into());
let result = map.update(&"key", |v| {
*v = v.saturating_add(1);
*v
});
assert!(result.is_none());
}
#[test]
fn test_expirymap_13() {
let map: ExpiringMap<&str, u8> = ExpiringMap::new();
let result = map.update(&"key", |v| {
*v = v.saturating_add(1);
*v
});
assert!(result.is_none());
}
#[test]
fn test_expirymap_14() {
let map = ExpiringMap::new();
map.try_insert("key", (), Duration::from_secs(50)).unwrap();
map.vacuum();
assert!(map.contains_key(&"key"));
}
#[test]
fn test_expirymap_15() {
let map = ExpiringMap::new();
map.try_insert("key", (), Duration::from_millis(50))
.unwrap();
thread::sleep(Duration::from_millis(75).into());
map.vacuum();
assert!(map.is_empty());
}
#[test]
fn test_expirymap_16() {
let map = ExpiringMap::new();
for i in 0..13 {
map.try_insert(i, (), Duration::from_millis(10)).unwrap();
}
thread::sleep(Duration::from_millis(50).into());
map.try_insert(100, (), Duration::from_secs(5)).unwrap();
assert!(map.contains_key(&100));
assert!(!map.contains_key(&0));
}
#[test]
fn test_expirymap_17() {
let map: ExpiringMap<&str, u8> = ExpiringMap::new();
assert!(map.is_empty());
map.try_insert("key", 1, Duration::from_secs(5)).unwrap();
assert!(!map.is_empty());
}
#[test]
fn test_expirymap_18() {
let map = ExpiringMap::new();
map.try_insert("key", 1u8, Duration::from_secs(5)).unwrap();
map.remove(&"key");
assert!(map.is_empty());
}
#[test]
fn test_expirymap_19() {
let map: ExpiringMap<String, usize> = ExpiringMap::new();
map.try_insert(String::from("x"), 1, Duration::from_secs(5))
.unwrap();
assert_eq!(map.get("x"), Some(1));
assert_eq!(map.get(&String::from("x")), Some(1));
assert!(map.contains_key("x"));
assert!(map.contains_key(&String::from("x")));
}
#[test]
fn test_expirymap_20() {
let map = Arc::new(ExpiringMap::new());
let handles: Vec<_> = (0..4)
.map(|t| {
let map = Arc::clone(&map);
thread::spawn(move || {
for i in 0..100 {
let key = t * 100 + i;
map.try_insert(key, key, Duration::from_secs(5)).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
for i in 0..400 {
assert!(map.contains_key(&i), "missing key {i}");
}
}
}