use crate::TenantContext;
use async_trait::async_trait;
use dashmap::DashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use uuid::Uuid;
#[derive(Debug, Clone)]
struct CacheEntry {
context: TenantContext,
inserted_at: Instant,
}
#[async_trait]
pub trait TenantCache: Send + Sync + std::fmt::Debug {
async fn get(&self, tenant_id: &Uuid) -> Option<TenantContext>;
async fn set(&self, tenant_id: Uuid, context: TenantContext);
async fn invalidate(&self, tenant_id: &Uuid);
async fn clear(&self);
}
#[derive(Debug, Clone)]
pub struct MemoryTenantCache {
cache: Arc<DashMap<Uuid, CacheEntry>>,
ttl: Duration,
max_size: usize,
}
impl MemoryTenantCache {
pub fn new(ttl: Duration, max_size: usize) -> Self {
Self {
cache: Arc::new(DashMap::new()),
ttl,
max_size,
}
}
fn is_expired(&self, entry: &CacheEntry) -> bool {
entry.inserted_at.elapsed() > self.ttl
}
fn evict_expired(&self) {
self.cache.retain(|_, entry| !self.is_expired(entry));
}
fn evict_if_full(&self) {
if self.cache.len() >= self.max_size {
if let Some(oldest) = self
.cache
.iter()
.min_by_key(|entry| entry.value().inserted_at)
.map(|entry| *entry.key())
{
self.cache.remove(&oldest);
}
}
}
}
impl Default for MemoryTenantCache {
fn default() -> Self {
Self::new(Duration::from_secs(15 * 60), 10_000)
}
}
#[async_trait]
impl TenantCache for MemoryTenantCache {
async fn get(&self, tenant_id: &Uuid) -> Option<TenantContext> {
if rand::random::<u8>() < 3 {
self.evict_expired();
}
self.cache.get(tenant_id).and_then(|entry| {
if self.is_expired(&entry) {
drop(entry);
self.cache.remove(tenant_id);
None
} else {
Some(entry.context.clone())
}
})
}
async fn set(&self, tenant_id: Uuid, context: TenantContext) {
self.evict_if_full();
let entry = CacheEntry {
context,
inserted_at: Instant::now(),
};
self.cache.insert(tenant_id, entry);
}
async fn invalidate(&self, tenant_id: &Uuid) {
self.cache.remove(tenant_id);
}
async fn clear(&self) {
self.cache.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_cache_set_and_get() {
let cache = MemoryTenantCache::default();
let tenant_id = Uuid::new_v4();
let context = TenantContext::new(tenant_id, "test".to_string(), "key".to_string());
cache.set(tenant_id, context.clone()).await;
let retrieved = cache.get(&tenant_id).await;
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().tenant_id, tenant_id);
}
#[tokio::test]
async fn test_cache_ttl_expiration() {
let cache = MemoryTenantCache::new(Duration::from_millis(100), 1000);
let tenant_id = Uuid::new_v4();
let context = TenantContext::new(tenant_id, "test".to_string(), "key".to_string());
cache.set(tenant_id, context).await;
assert!(cache.get(&tenant_id).await.is_some());
tokio::time::sleep(Duration::from_millis(150)).await;
assert!(cache.get(&tenant_id).await.is_none());
}
#[tokio::test]
async fn test_cache_invalidation() {
let cache = MemoryTenantCache::default();
let tenant_id = Uuid::new_v4();
let context = TenantContext::new(tenant_id, "test".to_string(), "key".to_string());
cache.set(tenant_id, context).await;
assert!(cache.get(&tenant_id).await.is_some());
cache.invalidate(&tenant_id).await;
assert!(cache.get(&tenant_id).await.is_none());
}
#[tokio::test]
async fn test_cache_max_size() {
let cache = MemoryTenantCache::new(Duration::from_secs(60), 3);
for i in 0..5 {
let tenant_id = Uuid::new_v4();
let context = TenantContext::new(tenant_id, format!("tenant-{i}"), format!("key-{i}"));
cache.set(tenant_id, context).await;
}
assert!(cache.cache.len() <= 3);
}
}