use std::hash::{Hash, Hasher};
use std::time::{Duration, Instant};
use dashmap::DashMap;
#[derive(Clone, Debug)]
pub struct CacheConfig {
pub ttl: Duration,
pub capacity: usize,
}
impl CacheConfig {
pub fn new(ttl: Duration, capacity: usize) -> Self {
Self { ttl, capacity }
}
pub fn default_authz() -> Self {
Self::new(Duration::from_millis(500), 1_000)
}
}
#[derive(Clone)]
struct CachedEntry<R> {
value: R,
expires_at: Instant,
}
#[derive(Clone, PartialEq, Eq)]
struct CacheKey(Vec<u8>);
impl Hash for CacheKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
pub struct ResponseCache<R> {
map: DashMap<CacheKey, CachedEntry<R>>,
config: CacheConfig,
}
impl<R: Clone + Send + 'static> ResponseCache<R> {
pub fn new(config: CacheConfig) -> Self {
Self {
map: DashMap::new(),
config,
}
}
pub fn get(&self, key: &[u8]) -> Option<R> {
let k = CacheKey(key.to_vec());
if let Some(entry) = self.map.get(&k)
&& entry.expires_at > Instant::now()
{
return Some(entry.value.clone());
}
self.map.remove(&k);
None
}
pub fn insert(&self, key: &[u8], value: R) {
if self.map.len() >= self.config.capacity {
return;
}
let k = CacheKey(key.to_vec());
self.map.insert(
k,
CachedEntry {
value,
expires_at: Instant::now() + self.config.ttl,
},
);
}
#[cfg(test)]
pub fn clear(&self) {
self.map.clear();
}
}
impl<R: Clone + Send + 'static> Clone for ResponseCache<R> {
fn clone(&self) -> Self {
Self {
map: self.map.clone(),
config: self.config.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn hit_within_ttl() {
let cache: ResponseCache<String> =
ResponseCache::new(CacheConfig::new(Duration::from_secs(10), 100));
cache.insert(b"key", "value".into());
assert_eq!(cache.get(b"key").as_deref(), Some("value"));
}
#[test]
fn miss_after_expiry() {
let cache: ResponseCache<String> =
ResponseCache::new(CacheConfig::new(Duration::from_millis(1), 100));
cache.insert(b"key", "value".into());
std::thread::sleep(Duration::from_millis(5));
assert_eq!(cache.get(b"key"), None);
}
#[test]
fn error_is_not_cached() {
let cache: ResponseCache<String> =
ResponseCache::new(CacheConfig::new(Duration::from_secs(10), 100));
assert_eq!(cache.get(b"key"), None);
}
#[test]
fn capacity_cap_drops_new_inserts() {
let cache: ResponseCache<u32> =
ResponseCache::new(CacheConfig::new(Duration::from_secs(10), 2));
cache.insert(b"a", 1);
cache.insert(b"b", 2);
cache.insert(b"c", 3); assert!(cache.get(b"a").is_some());
assert!(cache.get(b"b").is_some());
assert_eq!(cache.get(b"c"), None);
}
}