use super::{Error, Expired, RedisCache, TtlLruStore};
use serde::{Serialize, de::DeserializeOwned};
use std::num::NonZeroUsize;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
type Result<T> = std::result::Result<T, Error>;
#[inline]
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[inline]
fn get_ttl_by_unit(unit: Duration) -> Duration {
let secs = unit.as_secs();
if secs == 0 {
return Duration::ZERO;
}
let remaining = secs - (now_secs() % secs);
if remaining < secs / 10 {
return Duration::from_secs(secs + remaining);
}
Duration::from_secs(remaining)
}
#[derive(Clone)]
struct ExpiredCache<T> {
data: T,
expired_at: u64,
}
impl<T> Expired for ExpiredCache<T> {
fn is_expired(&self) -> bool {
now_secs() >= self.expired_at
}
}
pub struct TwoLevelStore<T> {
lru: TtlLruStore<ExpiredCache<T>>,
ttl: Duration,
redis: RedisCache,
}
impl<T: Clone + Serialize + DeserializeOwned> TwoLevelStore<T> {
pub fn new(redis: RedisCache, size: NonZeroUsize, ttl: Duration) -> Self {
Self {
lru: TtlLruStore::new(size),
ttl,
redis,
}
}
async fn fill_lru(&self, key: &str, value: T, ttl: Duration) {
if ttl.is_zero() {
return;
}
self.lru
.set(
key,
ExpiredCache {
data: value,
expired_at: now_secs() + ttl.as_secs(),
},
)
.await;
}
pub async fn set(&self, key: &str, value: T) -> Result<()> {
let ttl = get_ttl_by_unit(self.ttl);
self.redis.set_struct(key, &value, Some(ttl)).await?;
self.fill_lru(key, value, ttl).await;
Ok(())
}
pub async fn get(&self, key: &str) -> Result<Option<T>> {
if let Some(value) = self.lru.get(key).await {
return Ok(Some(value.data));
}
let result: Option<T> = self.redis.get_struct(key).await?;
if let Some(value) = &result {
let ttl = get_ttl_by_unit(self.ttl);
if ttl <= self.ttl {
self.fill_lru(key, value.clone(), ttl).await;
}
}
Ok(result)
}
pub async fn del(&self, key: &str) -> Result<()> {
self.lru.del(key).await;
self.redis.del(key).await
}
pub async fn purge_expired(&self) {
self.lru.purge_expired().await;
}
}