use crate::infrastructure::{FileError, FileSystem, Metadata};
use alloc::sync::Arc;
use core::hash::Hasher;
use rapidhash::{fast::RandomState, quality::RapidHasher};
use scc::HashIndex;
use tokio::sync::OnceCell;
type Cache<T> = HashIndex<Arc<str>, Arc<OnceCell<T>>, RandomState>;
pub struct FileCache {
file_system: Arc<dyn FileSystem + Send + Sync>,
metadata: Cache<Metadata>,
content_hashes: Cache<u64>,
}
impl FileCache {
pub fn new(file_system: Arc<dyn FileSystem + Send + Sync>, capacity: usize) -> Self {
Self {
file_system,
metadata: HashIndex::with_capacity_and_hasher(capacity, Default::default()),
content_hashes: HashIndex::with_capacity_and_hasher(capacity, Default::default()),
}
}
pub async fn exists(&self, path: &Arc<str>) -> Result<bool, FileError> {
Ok(self.metadata(path).await?.is_some())
}
pub async fn metadata(&self, path: &Arc<str>) -> Result<Option<Metadata>, FileError> {
match Self::get(&self.metadata, path, || async {
self.file_system
.metadata(path.as_ref().as_ref())
.await?
.ok_or(None)
})
.await
{
Ok(metadata) => Ok(Some(metadata)),
Err(None) => Ok(None),
Err(Some(error)) => Err(error),
}
}
pub async fn set_metadata(&self, path: &Arc<str>, metadata: Metadata) {
self.metadata
.entry_async(path.clone())
.await
.or_default()
.get()
.set(metadata)
.ok();
}
pub async fn content_hash(&self, path: &Arc<str>) -> Result<u64, FileError> {
Self::get(&self.content_hashes, path, || async {
let mut hasher = RapidHasher::default();
hasher.write(&self.file_system.read_file(path.as_ref().as_ref()).await?);
Ok(hasher.finish())
})
.await
}
pub async fn invalidate(&self, path: &str) {
self.metadata.remove_async(path).await;
self.content_hashes.remove_async(path).await;
}
async fn get<T: Copy, E, F: Future<Output = Result<T, E>>>(
cache: &Cache<T>,
path: &Arc<str>,
fetch: impl FnOnce() -> F,
) -> Result<T, E> {
if let Some(Some(value)) = cache.peek_with(path, |_, cell| cell.get().copied()) {
return Ok(value);
}
let cell = cache
.entry_async(path.clone())
.await
.or_default()
.get()
.clone();
cell.get_or_try_init(fetch).await.copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infrastructure::FakeFileSystem;
fn create_cache(file_system: &FakeFileSystem) -> FileCache {
FileCache::new(Arc::new(file_system.clone()), 0)
}
mod exists {
use super::*;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn check_file() {
let file_system = FakeFileSystem::default();
file_system.write_file("foo", "");
assert_eq!(
create_cache(&file_system).exists(&"foo".into()).await,
Ok(true)
);
}
#[tokio::test]
async fn check_directory() {
let file_system = FakeFileSystem::default();
file_system.create_directory("foo".as_ref()).await.unwrap();
assert_eq!(
create_cache(&file_system).exists(&"foo".into()).await,
Ok(true)
);
}
#[tokio::test]
async fn check_missing_file() {
assert_eq!(
create_cache(&Default::default())
.exists(&"foo".into())
.await,
Ok(false)
);
}
#[tokio::test]
async fn cache_existence() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
assert_eq!(cache.exists(&"foo".into()).await, Ok(true));
file_system.remove_file("foo".as_ref()).await.unwrap();
assert_eq!(cache.exists(&"foo".into()).await, Ok(true));
}
#[tokio::test]
async fn cache_existence_per_path() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
assert_eq!(cache.exists(&"foo".into()).await, Ok(true));
assert_eq!(cache.exists(&"bar".into()).await, Ok(false));
}
#[tokio::test]
async fn do_not_cache_missing_file() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
assert_eq!(cache.exists(&"foo".into()).await, Ok(false));
file_system.write_file("foo", "");
assert_eq!(cache.exists(&"foo".into()).await, Ok(true));
}
}
mod metadata {
use super::*;
use core::time::Duration;
use pretty_assertions::assert_eq;
use std::time::SystemTime;
#[tokio::test]
async fn get_file() {
let file_system = FakeFileSystem::default();
file_system.write_file("foo", "");
assert_eq!(
create_cache(&file_system).metadata(&"foo".into()).await,
Ok(Some(Metadata::new(SystemTime::UNIX_EPOCH, false)))
);
}
#[tokio::test]
async fn get_missing_file() {
assert_eq!(
create_cache(&Default::default())
.metadata(&"foo".into())
.await,
Ok(None)
);
}
#[tokio::test]
async fn cache_metadata() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
let metadata = cache.metadata(&"foo".into()).await.unwrap();
file_system.write_file("foo", "");
assert_eq!(cache.metadata(&"foo".into()).await, Ok(metadata));
}
#[tokio::test]
async fn cache_metadata_per_path() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
file_system.write_file("bar", "");
assert_eq!(
cache.metadata(&"foo".into()).await,
Ok(Some(Metadata::new(SystemTime::UNIX_EPOCH, false)))
);
assert_eq!(
cache.metadata(&"bar".into()).await,
Ok(Some(Metadata::new(
SystemTime::UNIX_EPOCH + Duration::from_secs(1),
false
)))
);
}
#[tokio::test]
async fn cache_metadata_on_existence_check() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
assert_eq!(cache.exists(&"foo".into()).await, Ok(true));
file_system.write_file("foo", "");
assert_eq!(
cache.metadata(&"foo".into()).await,
Ok(Some(Metadata::new(SystemTime::UNIX_EPOCH, false)))
);
}
#[tokio::test]
async fn do_not_cache_missing_file() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
assert_eq!(cache.metadata(&"foo".into()).await, Ok(None));
file_system.write_file("foo", "");
assert_eq!(
cache.metadata(&"foo".into()).await,
Ok(Some(Metadata::new(SystemTime::UNIX_EPOCH, false)))
);
}
}
mod set_metadata {
use super::*;
use core::{pin::pin, task::Poll, time::Duration};
use futures::poll;
use pretty_assertions::assert_eq;
use std::time::SystemTime;
use tokio::sync::Notify;
#[tokio::test]
async fn set_metadata() {
let cache = create_cache(&Default::default());
let metadata = Metadata::new(SystemTime::UNIX_EPOCH, false);
cache.set_metadata(&"foo".into(), metadata).await;
assert_eq!(cache.metadata(&"foo".into()).await, Ok(Some(metadata)));
}
#[tokio::test]
async fn set_metadata_per_path() {
let cache = create_cache(&Default::default());
cache
.set_metadata(&"foo".into(), Metadata::new(SystemTime::UNIX_EPOCH, false))
.await;
assert_eq!(cache.metadata(&"bar".into()).await, Ok(None));
}
#[tokio::test]
async fn keep_cached_metadata() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
let metadata = cache.metadata(&"foo".into()).await.unwrap();
cache
.set_metadata(
&"foo".into(),
Metadata::new(SystemTime::UNIX_EPOCH + Duration::from_secs(1), false),
)
.await;
assert_eq!(cache.metadata(&"foo".into()).await, Ok(metadata));
}
#[tokio::test]
async fn keep_metadata_of_in_flight_fetch() {
let cache = create_cache(&Default::default());
let path = "foo".into();
let metadata = Metadata::new(SystemTime::UNIX_EPOCH, false);
let notify = Notify::new();
let mut future = pin!(FileCache::get(&cache.metadata, &path, || async {
notify.notified().await;
Ok::<_, FileError>(metadata)
}));
assert!(poll!(&mut future).is_pending());
assert_eq!(
poll!(pin!(cache.set_metadata(
&path,
Metadata::new(SystemTime::UNIX_EPOCH + Duration::from_secs(1), false),
))),
Poll::Ready(())
);
notify.notify_one();
assert_eq!(future.await, Ok(metadata));
}
}
mod content_hash {
use super::*;
use pretty_assertions::{assert_eq, assert_ne};
#[tokio::test]
async fn hash_same_contents() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "baz");
file_system.write_file("bar", "baz");
assert_eq!(
cache.content_hash(&"foo".into()).await.unwrap(),
cache.content_hash(&"bar".into()).await.unwrap()
);
}
#[tokio::test]
async fn hash_different_contents() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "1");
file_system.write_file("bar", "2");
assert_ne!(
cache.content_hash(&"foo".into()).await.unwrap(),
cache.content_hash(&"bar".into()).await.unwrap()
);
}
#[tokio::test]
async fn cache_hash() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "1");
let hash = cache.content_hash(&"foo".into()).await.unwrap();
file_system.write_file("foo", "2");
assert_eq!(cache.content_hash(&"foo".into()).await, Ok(hash));
}
#[tokio::test]
async fn fail_to_hash_missing_file() {
assert_eq!(
create_cache(&Default::default())
.content_hash(&"foo".into())
.await,
Err(FileError::new("file not found"))
);
}
#[tokio::test]
async fn hash_file_created_after_error() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
cache.content_hash(&"foo".into()).await.unwrap_err();
file_system.write_file("foo", "");
assert!(cache.content_hash(&"foo".into()).await.is_ok());
}
}
mod invalidate {
use super::*;
use pretty_assertions::{assert_eq, assert_ne};
#[tokio::test]
async fn check_removed_file() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
assert_eq!(cache.exists(&"foo".into()).await, Ok(true));
file_system.remove_file("foo".as_ref()).await.unwrap();
cache.invalidate("foo").await;
assert_eq!(cache.exists(&"foo".into()).await, Ok(false));
}
#[tokio::test]
async fn get_updated_metadata() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "");
let metadata = cache.metadata(&"foo".into()).await.unwrap();
file_system.write_file("foo", "");
cache.invalidate("foo").await;
assert_ne!(cache.metadata(&"foo".into()).await, Ok(metadata));
}
#[tokio::test]
async fn hash_updated_content() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "1");
let hash = cache.content_hash(&"foo".into()).await.unwrap();
file_system.write_file("foo", "2");
cache.invalidate("foo").await;
assert_ne!(cache.content_hash(&"foo".into()).await, Ok(hash));
}
#[tokio::test]
async fn keep_other_path() {
let file_system = FakeFileSystem::default();
let cache = create_cache(&file_system);
file_system.write_file("foo", "1");
file_system.write_file("bar", "1");
let metadata = cache.metadata(&"bar".into()).await;
let hash = cache.content_hash(&"bar".into()).await;
file_system.write_file("bar", "2");
cache.invalidate("foo").await;
assert_eq!(cache.metadata(&"bar".into()).await, metadata);
assert_eq!(cache.content_hash(&"bar".into()).await, hash);
}
}
mod get {
use super::*;
use core::{future::pending, pin::pin, task::Poll};
use futures::{future::join, poll};
use pretty_assertions::assert_eq;
use tokio::sync::Notify;
const PATH_COUNT: usize = 64;
#[tokio::test]
async fn share_in_flight_fetch() {
let cache = Cache::default();
let path = "foo".into();
let notify = Notify::new();
let mut first = pin!(FileCache::get(&cache, &path, || async {
notify.notified().await;
Ok::<_, FileError>(1)
}));
assert!(poll!(&mut first).is_pending());
let mut second = pin!(FileCache::get(&cache, &path, || async {
Ok::<_, FileError>(2)
}));
assert!(poll!(&mut second).is_pending());
notify.notify_one();
assert_eq!(join(first, second).await, (Ok(1), Ok(1)));
}
#[tokio::test]
async fn fetch_again_after_cancelled_fetch() {
let cache = Cache::default();
assert!(
poll!(pin!(FileCache::get(
&cache,
&"foo".into(),
pending::<Result<_, FileError>>
)))
.is_pending()
);
assert_eq!(
FileCache::get(&cache, &"foo".into(), || async { Ok::<_, FileError>(1) }).await,
Ok(1)
);
}
#[tokio::test]
async fn fetch_again_after_removal_during_in_flight_fetch() {
let cache = Cache::default();
let path = "foo".into();
let notify = Notify::new();
let mut first = pin!(FileCache::get(&cache, &path, || async {
notify.notified().await;
Ok::<_, FileError>(1)
}));
assert!(poll!(&mut first).is_pending());
cache.remove_async("foo").await;
assert_eq!(
poll!(pin!(FileCache::get(&cache, &path, || async {
Ok::<_, FileError>(2)
}))),
Poll::Ready(Ok(2))
);
notify.notify_one();
assert_eq!(first.await, Ok(1));
assert_eq!(
FileCache::get(&cache, &path, || async { Ok::<_, FileError>(3) }).await,
Ok(2)
);
}
#[tokio::test]
async fn fetch_other_paths_during_in_flight_fetch() {
let cache = Cache::default();
let path = "foo".into();
let mut future = pin!(FileCache::get(
&cache,
&path,
pending::<Result<(), FileError>>
));
assert!(poll!(&mut future).is_pending());
for index in 0..PATH_COUNT {
assert_eq!(
poll!(pin!(FileCache::get(
&cache,
&index.to_string().into(),
|| async { Ok::<_, FileError>(()) }
))),
Poll::Ready(Ok(()))
);
}
}
#[tokio::test]
async fn get_cached_value_during_entry_lock() {
let cache = Cache::default();
let path = "foo".into();
FileCache::get(&cache, &path, || async { Ok::<_, FileError>(1) })
.await
.unwrap();
let _entry = cache.entry_async(path.clone()).await;
assert_eq!(
poll!(pin!(FileCache::get(&cache, &path, || async {
Ok::<_, FileError>(2)
}))),
Poll::Ready(Ok(1))
);
}
}
}