use super::{Error, Expired, RedisCache, TtlLruStore};
use chrono::Utc;
use serde::{Serialize, de::DeserializeOwned};
use std::num::NonZeroUsize;
use std::time::Duration;
type Result<T> = std::result::Result<T, Error>;
fn get_ttl_by_unit(unit: Duration) -> Duration {
let now = Utc::now();
let seconds = unit.as_secs() - (now.timestamp() as u64 % unit.as_secs());
if seconds < unit.as_secs() / 10 {
return Duration::from_secs(unit.as_secs() + seconds);
}
Duration::from_secs(seconds)
}
fn now_utc() -> i64 {
Utc::now().timestamp()
}
#[derive(Clone)]
struct ExpiredCache<T> {
data: T,
expired_at: i64,
}
impl<T> Expired for ExpiredCache<T> {
fn is_expired(&self) -> bool {
now_utc() >= 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(cache: RedisCache, size: NonZeroUsize, ttl: Duration) -> Self {
TwoLevelStore {
lru: TtlLruStore::new(size),
ttl,
redis: cache,
}
}
async fn fill_lru(&self, key: &str, value: T, ttl: Duration) {
if ttl.is_zero() {
return;
}
let expired_at = now_utc() + ttl.as_secs() as i64;
let data = ExpiredCache {
data: value,
expired_at,
};
self.lru.set(key, data).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(ref value) = result {
let ttl = get_ttl_by_unit(self.ttl);
if ttl <= self.ttl {
self.fill_lru(key, value.clone(), ttl).await;
}
}
Ok(result)
}
}