use std::{
borrow::Borrow,
fmt::Debug,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use lfu_cache::LfuCache;
pub use crate::{
error::Error,
traits::{AsyncFileRepr, Key},
};
mod error;
#[cfg(test)]
mod test;
mod traits;
#[cfg(not(feature = "utf8-paths"))]
type Path = std::path::Path;
#[cfg(not(feature = "utf8-paths"))]
type PathBuf = std::path::PathBuf;
#[cfg(feature = "utf8-paths")]
type Path = camino::Utf8Path;
#[cfg(feature = "utf8-paths")]
type PathBuf = camino::Utf8PathBuf;
#[derive(Debug)]
struct CacheItem<T>
where
T: AsyncFileRepr,
{
needs_flush: AtomicBool,
content: Arc<T>,
}
impl<T> CacheItem<T>
where
T: AsyncFileRepr,
{
fn new_loaded(content: T) -> Self {
Self {
needs_flush: AtomicBool::new(false),
content: Arc::new(content),
}
}
fn new_pushed(content: T) -> Self {
Self {
needs_flush: AtomicBool::new(true),
content: Arc::new(content),
}
}
}
#[derive(Debug)]
pub struct FileBackedLfuCache<K, T>
where
K: Key,
T: AsyncFileRepr,
{
directory: PathBuf,
cache: LfuCache<K, CacheItem<T>>,
}
impl<K, T> FileBackedLfuCache<K, T>
where
K: Key,
T: AsyncFileRepr,
{
pub fn init(path: impl AsRef<Path>, capacity: usize) -> Result<Self, Error<K, T::Err>> {
let path = path.as_ref().to_owned();
if !(path.is_dir() || path.canonicalize().map(|p| p.is_dir()).unwrap_or(false)) {
return Err(Error::Init(path));
}
let cache = LfuCache::with_capacity(capacity);
Ok(Self { directory: path, cache })
}
pub fn loaded_count(&self) -> usize {
self.cache.len()
}
pub fn get_backing_directory(&self) -> &Path {
&self.directory
}
pub fn get_path_for(&self, key: impl Borrow<K>) -> PathBuf {
self.directory.join(key.borrow().as_filename())
}
pub fn has_key(&self, key: impl Borrow<K>) -> bool {
let key = key.borrow();
self.has_loaded_key(key) || self.has_flushed_key(key)
}
pub fn has_loaded_key(&self, key: impl Borrow<K>) -> bool {
self.cache.keys().any(|k| k == key.borrow())
}
pub fn has_flushed_key(&self, key: impl Borrow<K>) -> bool {
self.get_path_for(key).is_file()
}
pub fn get(&mut self, key: impl Borrow<K>) -> Result<Arc<T>, Error<K, T::Err>> {
let key = key.borrow();
self.cache
.get(key)
.map(|item| Arc::clone(&item.content))
.ok_or(Error::NotInCache(key.clone()))
}
pub fn get_mut(&mut self, key: impl Borrow<K>) -> Result<&mut T, Error<K, T::Err>> {
let key = key.borrow();
let Some(CacheItem { needs_flush, content }) = self.cache.get_mut(key) else {
Err(Error::NotInCache(key.clone()))?
};
let mut_ref = match Arc::get_mut(content) {
Some(r) => {
needs_flush.store(true, Ordering::Relaxed);
r
}
None => Err(Error::Immutable(key.clone()))?,
};
Ok(mut_ref)
}
pub async fn get_or_load(&mut self, key: impl Borrow<K>) -> Result<Arc<T>, Error<K, T::Err>> {
let key = key.borrow();
if let Some(item) = self.cache.get(key) {
return Ok(Arc::clone(&item.content));
}
let item = self.read_from_disk(key, Error::NotFound).await?;
let content = Arc::clone(&item.content);
self.insert_and_handle_eviction(key.clone(), item).await?;
Ok(content)
}
pub async fn get_or_load_mut(
&mut self,
key: impl Borrow<K>,
) -> Result<&mut T, Error<K, T::Err>> {
let key = key.borrow();
if !self.has_loaded_key(key) {
let item = self.read_from_disk(key, Error::NotFound).await?;
self.insert_and_handle_eviction(key.clone(), item).await?;
}
let CacheItem { needs_flush, content } = self
.cache
.get_mut(key)
.expect("something is wrong with Arc");
let mut_ref = match Arc::get_mut(content) {
Some(r) => {
needs_flush.store(true, Ordering::Relaxed);
r
}
None => Err(Error::Immutable(key.clone()))?,
};
Ok(mut_ref)
}
pub async fn push(&mut self, item: T) -> Result<K, Error<K, T::Err>> {
let key = K::new();
self.insert_and_handle_eviction(key.clone(), CacheItem::new_pushed(item))
.await?;
Ok(key)
}
pub async fn direct_flush(&self, item: T) -> Result<K, Error<K, T::Err>> {
let key = K::new();
let flush_path = self.get_path_for(&key);
Arc::new(item).flush(flush_path).await?;
Ok(key)
}
pub async fn flush(&self, key: impl Borrow<K>) -> Result<(), Error<K, T::Err>> {
let key = key.borrow();
let CacheItem { needs_flush, content } = self
.cache
.peek_iter()
.find_map(|(k, v)| (k == key).then_some(v))
.ok_or(Error::NotInCache(key.clone()))?;
if needs_flush.load(Ordering::Acquire) {
let flush_path = self.get_path_for(key);
content.flush(flush_path).await?;
needs_flush.store(false, Ordering::Release);
}
Ok(())
}
pub async fn flush_all(&self) -> Result<(), Vec<Error<K, T::Err>>> {
let mut errors = vec![];
for (key, CacheItem { needs_flush, content }) in self.cache.peek_iter() {
if needs_flush.load(Ordering::Acquire) {
let flush_path = self.get_path_for(key);
if let Err(err) = content.flush(flush_path).await {
errors.push(err.into());
}
needs_flush.store(false, Ordering::Release);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub async fn clear_cache(&mut self, do_flush: bool) -> Result<(), Vec<Error<K, T::Err>>> {
if do_flush {
self.flush_all().await?;
}
self.cache.clear();
Ok(())
}
pub async fn delete(&mut self, key: impl Borrow<K>) -> Result<(), Error<K, T::Err>> {
let key = key.borrow();
if !self.has_key(key) {
Err(Error::NotFound(key.clone()))?
}
self.cache.remove(key);
let path = self.get_path_for(key);
if path.is_file() {
T::delete(path).await?;
}
Ok(())
}
async fn read_from_disk<F>(
&self,
key: impl Borrow<K>,
not_found_variant: F,
) -> Result<CacheItem<T>, Error<K, T::Err>>
where
F: FnOnce(K) -> Error<K, T::Err>,
{
let key = key.borrow();
let load_path = self.get_path_for(key);
if !load_path.is_file() {
Err(not_found_variant(key.clone()))?
}
let content = T::load(load_path).await?;
let item = CacheItem::new_loaded(content);
Ok(item)
}
async fn insert_and_handle_eviction(
&mut self,
key: K,
item: CacheItem<T>,
) -> Result<(), T::Err> {
assert!(!self.has_loaded_key(&key), "key already present in cache");
let flush_key = self.cache.peek_lfu_key().cloned();
let evicted_item = self.cache.insert(key.clone(), item);
match (flush_key, evicted_item) {
(Some(key), Some(CacheItem { needs_flush, content })) => {
if needs_flush.load(Ordering::Relaxed) {
let flush_path = self.get_path_for(key);
content.flush(flush_path).await?;
}
}
(_, None) => {
}
(None, Some(_)) => unreachable!("something is wrong with the LFU implementation"),
};
Ok(())
}
}