use crossbeam::atomic::AtomicCell;
use crossbeam_skiplist::{map::Entry, SkipMap};
use derive_more::IsVariant;
use parking_lot::Mutex;
use std::sync::Arc;
use std::time::Instant;
use tokio::time::Duration;
#[derive(Debug, IsVariant, Clone)]
pub enum ExpiryValue<V> {
Constant(Arc<Mutex<V>>),
Expirable {
value: Arc<Mutex<V>>,
expires_at: Arc<AtomicCell<Instant>>,
},
}
impl<V> ExpiryValue<V> {
pub fn is_expired(&self) -> bool {
match self {
Self::Constant(_) => false,
Self::Expirable {
value: _,
expires_at,
} => Instant::now() >= expires_at.load(),
}
}
pub fn get(&self) -> Arc<Mutex<V>> {
match self {
Self::Constant(inner) => inner.clone(),
Self::Expirable {
value,
expires_at: _,
} => value.clone(),
}
}
}
#[derive(Debug)]
pub struct TimedMap<K: Ord + 'static, V> {
inner: SkipMap<K, ExpiryValue<V>>,
disable_expiration: AtomicCell<bool>,
}
impl<K: Ord + 'static + Send, V> Default for TimedMap<K, V> {
fn default() -> Self {
Self {
inner: SkipMap::default(),
disable_expiration: AtomicCell::default(),
}
}
}
impl<K: Ord, V> TimedMap<K, V> {
pub fn toggle_expiration(&self) {
let previous_state = self.disable_expiration.load();
let _ = self
.disable_expiration
.compare_exchange(previous_state, !previous_state);
}
}
impl<K: Ord + Clone + Send + 'static + Sync, V: Send + 'static + Sync> TimedMap<K, V> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert_constant(&self, key: K, val: V) {
let entry = ExpiryValue::Constant(Arc::new(val.into()));
self.inner.insert(key, entry);
}
pub fn insert_expirable(&self, key: K, val: V, timeout: Duration) {
if self.disable_expiration.load() {
return self.insert_constant(key, val);
}
let expires_at = Instant::now() + timeout;
let entry = ExpiryValue::Expirable {
value: Arc::new(Mutex::new(val)),
expires_at: Arc::new(AtomicCell::new(expires_at)),
};
self.inner.insert(key, entry);
}
pub fn get(&self, key: &K) -> Option<Arc<Mutex<V>>> {
let found = self.inner.get(key)?;
if found.value().is_expired() {
self.inner.remove(key);
return None;
}
Some(found.value().get())
}
pub fn iter(&self) -> impl Iterator<Item = Entry<'_, K, ExpiryValue<V>>> {
self.inner.iter().filter_map(|entry| {
if entry.value().is_expired() {
entry.remove();
return None;
}
Some(entry)
})
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn contains_key(&self, key: &K) -> bool {
let exists = self.inner.contains_key(key);
if exists {
return self.get(key).is_some();
}
false
}
pub fn len_expired(&self) -> usize {
self.inner
.iter()
.filter(|entry| entry.value().is_expirable())
.count()
}
pub fn remove(&self, key: &K) {
let _ = self.inner.remove(key);
}
pub fn update_expiration_status(&self, key: &K, duration: Duration) -> Option<Instant> {
let found = self.inner.get(key)?;
let existing = found.value();
let next_instant = Instant::now() + duration;
match existing {
ExpiryValue::Constant(_) => None,
ExpiryValue::Expirable {
value: _,
expires_at,
} => {
expires_at.swap(next_instant);
Some(next_instant)
}
}
}
pub fn expires_entries(&self) -> bool {
!self.disable_expiration.load()
}
pub fn clear(&self) {
self.inner.clear();
}
pub fn purge_expired(&self) {
if !self.expires_entries() {
return;
}
self.inner
.iter()
.filter(|entry| entry.value().is_expired())
.for_each(|entry| {
entry.remove();
});
}
}
#[cfg(test)]
mod tests {
use super::TimedMap;
use std::sync::Arc;
use tokio::time::{sleep, Duration};
#[tokio::test(flavor = "multi_thread")]
async fn test_purge_removes_expired_async() {
let map: Arc<TimedMap<u64, u64>> = Arc::new(TimedMap::new());
map.insert_expirable(1, 100, Duration::from_millis(50));
sleep(Duration::from_millis(80)).await;
map.purge_expired();
assert!(!map.inner.contains_key(&1));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_concurrent_inserts_and_purge_async() {
let map = Arc::new(TimedMap::new());
let mut handles = Vec::new();
for i in 0..50u64 {
let m = Arc::clone(&map);
handles.push(tokio::spawn(async move {
for j in 0..10u64 {
let k = i * 100 + j;
m.insert_expirable(k, k, Duration::from_millis(30));
}
}));
}
for h in handles {
let _ = h.await;
}
sleep(Duration::from_millis(60)).await;
map.purge_expired();
assert_eq!(map.len_expired(), 0);
}
}