use crate::AppState;
use anyhow::Context;
use async_trait::async_trait;
use chrono::{DateTime, TimeZone, Utc};
use dashmap::DashMap;
use std::collections::HashMap;
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 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()
}
fn make_fresh(&self) -> Self {
Self {
last_checked_at: AtomicU64::from(Self::elapsed()),
content: Arc::clone(&self.content),
}
}
}
pub struct FileCache<T: AsyncFromStrWithState> {
cache: Arc<DashMap<PathBuf, Cached<T>>>,
static_files: HashMap<PathBuf, Cached<T>>,
}
impl<T: AsyncFromStrWithState> FileCache<T> {
pub fn new() -> Self {
Self {
cache: Arc::default(),
static_files: HashMap::new(),
}
}
pub fn add_static(&mut self, path: PathBuf, contents: T) {
log::trace!("Adding static file {path:?} to the cache.");
self.static_files.insert(path, Cached::new(contents));
}
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::trace!("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;
let parsed = match file_contents {
Ok(contents) => {
let value = T::from_str_with_state(app_state, &contents).await?;
Ok(Cached::new(value))
}
Err(e) => {
if let Some(static_file) = self.static_files.get(path) {
log::trace!("File {path:?} not found, loading it from static files instead.");
let cached: Cached<T> = static_file.make_fresh();
Ok(cached)
} else {
Err(e).with_context(|| format!("Couldn't load {path:?} into cache"))
}
}
};
match parsed {
Ok(value) => {
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>;
}