use crate::AppState;
use anyhow::Context;
use async_trait::async_trait;
use chrono::{DateTime, TimeZone, Utc};
use dashmap::DashMap;
use std::path::PathBuf;
use std::sync::atomic::{
AtomicU64,
Ordering::{Acquire, Release},
};
use std::sync::Arc;
use std::time::SystemTime;
const MAX_STALE_CACHE_MS: u64 = 100;
#[derive(Default)]
struct Cached<T> {
last_checked_at: AtomicU64,
content: Arc<T>,
}
impl<T> Cached<T> {
fn new(content: T) -> Self {
let s = Self {
last_checked_at: AtomicU64::new(0),
content: Arc::new(content),
};
s.update_check_time();
s
}
fn new_static(content: T) -> Self {
let this = Self::new(content);
this.last_checked_at.store(u64::MAX, Release);
this
}
fn last_check_time(&self) -> DateTime<Utc> {
self.last_checked_at
.load(Acquire)
.saturating_mul(MAX_STALE_CACHE_MS)
.try_into()
.ok()
.and_then(|millis| Utc.timestamp_millis_opt(millis).single())
.expect("file timestamp out of bound")
}
fn update_check_time(&self) {
self.last_checked_at.store(Self::elapsed(), Release);
}
fn elapsed() -> u64 {
let timestamp_millis = (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH))
.expect("invalid duration")
.as_millis();
let elapsed_intervals = timestamp_millis / u128::from(MAX_STALE_CACHE_MS);
u64::try_from(elapsed_intervals).expect("invalid date")
}
fn needs_check(&self) -> bool {
self.last_checked_at
.load(Acquire)
.saturating_add(MAX_STALE_CACHE_MS)
< Self::elapsed()
}
}
pub struct FileCache<T: AsyncFromStrWithState> {
cache: Arc<DashMap<PathBuf, Cached<T>>>,
}
impl<T: AsyncFromStrWithState> FileCache<T> {
pub fn new() -> Self {
Self {
cache: Arc::default(),
}
}
pub fn add_static(&mut self, path: PathBuf, contents: T) {
log::trace!("Adding static file {path:?} to the cache.");
let cached = Cached::new_static(contents);
self.cache.insert(path, cached);
}
pub async fn get(&self, app_state: &AppState, path: &PathBuf) -> anyhow::Result<Arc<T>> {
if let Some(cached) = self.cache.get(path) {
if !cached.needs_check() {
log::trace!("Cache answer without filesystem lookup for {:?}", path);
return Ok(Arc::clone(&cached.content));
}
match app_state
.file_system
.modified_since(app_state, path, cached.last_check_time())
.await
{
Ok(false) => {
log::trace!("Cache answer with filesystem metadata read for {:?}", path);
cached.update_check_time();
return Ok(Arc::clone(&cached.content));
}
Ok(true) => log::trace!("{path:?} was changed, updating cache..."),
Err(e) => log::warn!("Cannot read metadata of {path:?}, re-loading it: {e:#}"),
}
}
log::trace!("Loading and parsing {:?}", path);
let file_contents = app_state
.file_system
.read_to_string(app_state, path)
.await
.with_context(|| format!("Couldn't load {path:?} into cache"));
let parsed = match file_contents {
Ok(contents) => Ok(T::from_str_with_state(app_state, &contents).await?),
Err(e) => Err(e),
};
match parsed {
Ok(item) => {
let value = Cached::new(item);
let new_val = Arc::clone(&value.content);
self.cache.insert(path.clone(), value);
log::trace!("{:?} loaded in cache", path);
Ok(new_val)
}
Err(e) => {
log::trace!(
"Evicting {path:?} from the cache because the following error occurred: {e}"
);
self.cache.remove(path);
Err(e)
}
}
}
}
#[async_trait(? Send)]
pub trait AsyncFromStrWithState: Sized {
async fn from_str_with_state(app_state: &AppState, source: &str) -> anyhow::Result<Self>;
}