use std::sync::Arc;
use std::time::{Duration, Instant};
use dashmap::DashMap;
use pubky_common::crypto::PublicKey;
use crate::shared::user_quota::UserQuota;
const CACHE_TTL: Duration = Duration::from_secs(120);
const NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(30);
const MAX_ENTRIES: usize = 100_000;
const CLEANUP_INTERVAL_SECS: u64 = 60;
#[derive(Debug, Clone)]
pub(super) struct CachedEntry {
pub config: Option<UserQuota>,
cached_at: Instant,
ttl: Duration,
}
impl CachedEntry {
pub fn found(config: UserQuota) -> Self {
Self {
config: Some(config),
cached_at: Instant::now(),
ttl: CACHE_TTL,
}
}
pub fn not_found() -> Self {
Self {
config: None,
cached_at: Instant::now(),
ttl: NEGATIVE_CACHE_TTL,
}
}
fn is_expired(&self) -> bool {
self.cached_at.elapsed() > self.ttl
}
}
#[derive(Debug, Clone)]
pub(super) struct QuotaCache {
entries: Arc<DashMap<PublicKey, CachedEntry>>,
}
impl QuotaCache {
pub fn new() -> Self {
let entries: Arc<DashMap<PublicKey, CachedEntry>> = Arc::new(DashMap::new());
let weak = Arc::downgrade(&entries);
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(CLEANUP_INTERVAL_SECS));
interval.tick().await; loop {
interval.tick().await;
let Some(map) = weak.upgrade() else {
break;
};
map.retain(|_, entry| !entry.is_expired());
}
});
Self { entries }
}
pub fn get(&self, pubkey: &PublicKey) -> Option<Option<UserQuota>> {
self.entries
.get(pubkey)
.filter(|entry| !entry.is_expired())
.map(|entry| entry.config.clone())
}
pub fn insert(&self, pubkey: PublicKey, entry: CachedEntry) {
self.entries.insert(pubkey, entry);
}
pub fn remove(&self, pubkey: &PublicKey) {
self.entries.remove(pubkey);
}
pub fn make_room(&self) {
if self.entries.len() < MAX_ENTRIES {
return;
}
self.entries.retain(|_, entry| !entry.is_expired());
if self.entries.len() >= MAX_ENTRIES {
let to_evict = MAX_ENTRIES / 10;
let keys: Vec<_> = self
.entries
.iter()
.take(to_evict.max(1))
.map(|entry| entry.key().clone())
.collect();
for key in keys {
self.entries.remove(&key);
}
}
}
}