use crate::AppState;
use anyhow::Context;
use std::path::PathBuf;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::atomic::{
AtomicU64,
Ordering::{Acquire, Release},
};
use std::sync::{Arc, RwLock};
use std::time::{Duration, 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) -> SystemTime {
let millis = self
.last_checked_at
.load(Acquire)
.saturating_mul(MAX_STALE_CACHE_MS);
SystemTime::UNIX_EPOCH + Duration::from_millis(millis)
}
fn update_check_time(&self) {
let elapsed = u64::try_from(Self::elapsed()).expect("too far in the future");
self.last_checked_at.store(elapsed, Release);
}
fn elapsed() -> u128 {
(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH))
.unwrap()
.as_millis()
/ u128::from(MAX_STALE_CACHE_MS)
}
fn needs_check(&self) -> bool {
self.last_check_time() + Duration::from_millis(MAX_STALE_CACHE_MS) <= SystemTime::now()
}
}
pub struct FileCache<T: AsyncFromStrWithState> {
cache: Arc<RwLock<HashMap<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);
let mut cache = self.cache.write().expect("cache");
cache.insert(path, cached);
}
pub async fn get(&self, app_state: &AppState, path: &PathBuf) -> anyhow::Result<Arc<T>> {
{
let read_lock = self.cache.read().expect("lock");
if let Some(cached) = read_lock.get(path) {
if !cached.needs_check() {
log::trace!("Cache answer without filesystem lookup for {:?}", path);
return Ok(Arc::clone(&cached.content));
}
if let Ok(modified) = std::fs::metadata(path).and_then(|m| m.modified()) {
if modified <= cached.last_check_time() {
log::trace!("Cache answer with filesystem metadata read for {:?}", path);
cached.update_check_time();
return Ok(Arc::clone(&cached.content));
}
}
}
}
log::trace!("Loading and parsing {:?}", path);
let file_contents = std::fs::read_to_string(path)
.with_context(|| format!("Reading {path:?} to load it in cache"));
let parsed = match file_contents {
Ok(contents) => Ok(T::from_str_with_state(app_state, &contents).await?),
Err(e) => Err(e),
};
let mut write_lock = self.cache.write().expect("write lock");
match parsed {
Ok(item) => {
let value = Cached::new(item);
let new_val = Arc::clone(&value.content);
write_lock.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}"
);
write_lock.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>;
}