use std::num::NonZeroUsize;
use std::sync::Arc;
use lru::LruCache;
use crate::prelude::*;
const PROFILE_ME_CACHE_TTL_SECS: i64 = 30;
const DEFAULT_CAPACITY: NonZeroUsize = match NonZeroUsize::new(256) {
Some(n) => n,
None => NonZeroUsize::MIN,
};
#[derive(Debug, Clone)]
struct ProfileMeEntry {
etag: Box<str>,
valid_until: Timestamp,
}
type ProfileMeCacheInner = LruCache<TnId, ProfileMeEntry>;
#[derive(Debug)]
pub struct ProfileMeCache {
entries: Arc<parking_lot::Mutex<ProfileMeCacheInner>>,
}
impl ProfileMeCache {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
pub fn with_capacity(capacity: NonZeroUsize) -> Self {
Self { entries: Arc::new(parking_lot::Mutex::new(LruCache::new(capacity))) }
}
pub fn get(&self, tn_id: TnId) -> Option<Box<str>> {
let mut cache = self.entries.lock();
let now = Timestamp::now();
cache.get(&tn_id).filter(|e| e.valid_until.0 > now.0).map(|e| e.etag.clone())
}
pub fn insert(&self, tn_id: TnId, etag: Box<str>) {
let valid_until = Timestamp::from_now(PROFILE_ME_CACHE_TTL_SECS);
let mut cache = self.entries.lock();
cache.put(tn_id, ProfileMeEntry { etag, valid_until });
}
pub fn touch(&self, tn_id: TnId) {
let mut cache = self.entries.lock();
if let Some(entry) = cache.get_mut(&tn_id) {
entry.valid_until = Timestamp::from_now(PROFILE_ME_CACHE_TTL_SECS);
}
}
pub fn invalidate(&self, tn_id: TnId) {
let mut cache = self.entries.lock();
cache.pop(&tn_id);
}
}
impl Default for ProfileMeCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn touch_extends_valid_until() {
let cache = ProfileMeCache::new();
let tn_id = TnId(1);
cache.insert(tn_id, "etag".into());
{
let mut inner = cache.entries.lock();
if let Some(e) = inner.get_mut(&tn_id) {
e.valid_until = Timestamp(0);
}
}
cache.touch(tn_id);
let now = Timestamp::now().0;
let inner = cache.entries.lock();
let entry = inner.peek(&tn_id);
assert!(entry.is_some(), "entry should still be present after touch");
assert!(
entry.is_some_and(|e| e.valid_until.0 > now),
"touch should extend valid_until past now"
);
assert!(entry.is_some_and(|e| &*e.etag == "etag"), "touch must not change the ETag");
}
#[test]
fn touch_is_noop_on_missing_key() {
let cache = ProfileMeCache::new();
cache.touch(TnId(42));
assert!(cache.get(TnId(42)).is_none());
}
}