use fuchsia_inspect as finspect;
use futures::future::BoxFuture;
use futures::FutureExt as _;
use std::collections::HashMap;
use std::sync::{Arc, Weak};
#[derive(Debug, Clone)]
pub struct RootDirCache<S> {
non_meta_storage: S,
dirs: Arc<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<crate::RootDir<S>>>>>,
}
impl<S: crate::NonMetaStorage + Clone> RootDirCache<S> {
pub fn new(non_meta_storage: S) -> Self {
let dirs = Arc::new(std::sync::Mutex::new(HashMap::new()));
Self { non_meta_storage, dirs }
}
pub async fn get_or_insert(
&self,
hash: fuchsia_hash::Hash,
root_dir: Option<crate::RootDir<S>>,
) -> Result<Arc<crate::RootDir<S>>, crate::Error> {
Ok(if let Some(root_dir) = self.get(&hash) {
root_dir
} else {
let dropper = Box::new(Dropper { dirs: Arc::downgrade(&self.dirs), hash });
let new_root_dir = match root_dir {
Some(mut root_dir) => match root_dir.set_dropper(dropper) {
Ok(()) => Arc::new(root_dir),
Err(_) => {
return Err(crate::Error::DropperAlreadySet);
}
},
None => {
crate::RootDir::new_with_dropper(self.non_meta_storage.clone(), hash, dropper)
.await?
}
};
use std::collections::hash_map::Entry::*;
let root_dir = match self.dirs.lock().expect("poisoned mutex").entry(hash) {
Occupied(mut o) => {
let old_root_dir = o.get_mut();
if let Some(old_root_dir) = old_root_dir.upgrade() {
old_root_dir
} else {
*old_root_dir = Arc::downgrade(&new_root_dir);
new_root_dir
}
}
Vacant(v) => {
v.insert(Arc::downgrade(&new_root_dir));
new_root_dir
}
};
root_dir
})
}
pub fn get(&self, hash: &fuchsia_hash::Hash) -> Option<Arc<crate::RootDir<S>>> {
self.dirs.lock().expect("poisoned mutex").get(hash)?.upgrade()
}
pub fn list(&self) -> Vec<Arc<crate::RootDir<S>>> {
self.dirs.lock().expect("poisoned mutex").iter().filter_map(|(_, v)| v.upgrade()).collect()
}
pub fn record_lazy_inspect(
&self,
) -> impl Fn() -> BoxFuture<'static, Result<finspect::Inspector, anyhow::Error>>
+ Send
+ Sync
+ 'static {
let dirs = Arc::downgrade(&self.dirs);
move || {
let dirs = dirs.clone();
async move {
let inspector = finspect::Inspector::default();
if let Some(dirs) = dirs.upgrade() {
let package_counts: HashMap<_, _> = {
let dirs = dirs.lock().expect("poisoned mutex");
dirs.iter().map(|(k, v)| (*k, v.strong_count() as u64)).collect()
};
let root = inspector.root();
let () = package_counts.into_iter().for_each(|(pkg, count)| {
root.record_child(pkg.to_string(), |n| n.record_uint("instances", count))
});
}
Ok(inspector)
}
.boxed()
}
}
}
struct Dropper<S> {
dirs: Weak<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<crate::RootDir<S>>>>>,
hash: fuchsia_hash::Hash,
}
impl<S> Drop for Dropper<S> {
fn drop(&mut self) {
let Some(dirs) = self.dirs.upgrade() else {
return;
};
use std::collections::hash_map::Entry::*;
match dirs.lock().expect("poisoned mutex").entry(self.hash) {
Occupied(o) => {
if o.get().strong_count() == 0 {
o.remove_entry();
}
}
Vacant(_) => (),
};
}
}
impl<S> std::fmt::Debug for Dropper<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dropper").field("dirs", &self.dirs).field("hash", &self.hash).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use diagnostics_assertions::assert_data_tree;
use fidl_fuchsia_io as fio;
use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
use fuchsia_pkg_testing::PackageBuilder;
use vfs::directory::entry_container::Directory as _;
#[fuchsia::test]
async fn get_or_insert_new_entry() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let server = RootDirCache::new(blobfs_client);
let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
assert_eq!(server.list().len(), 1);
drop(dir);
assert_eq!(server.list().len(), 0);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn closing_package_connection_closes_package() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let server = RootDirCache::new(blobfs_client);
let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
let scope = vfs::execution_scope::ExecutionScope::new();
let () = dir.open(
scope.clone(),
fio::OpenFlags::RIGHT_READABLE,
vfs::path::Path::dot(),
server_end.into_channel().into(),
);
let _: fio::ConnectionInfo =
proxy.get_connection_info().await.expect("directory succesfully handling requests");
assert_eq!(server.list().len(), 1);
drop(proxy);
let () = scope.wait().await;
assert_eq!(server.list().len(), 0);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn get_or_insert_existing_entry() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let server = RootDirCache::new(blobfs_client);
let dir0 = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
let dir1 = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
assert_eq!(server.list().len(), 1);
assert_eq!(Arc::strong_count(&server.list()[0]), 3);
drop(dir0);
drop(dir1);
assert_eq!(server.list().len(), 0);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn get_or_insert_provided_root_dir() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let root_dir = crate::RootDir::new_raw(blobfs_client.clone(), metafar_blob.merkle, None)
.await
.unwrap();
blobfs_fake.delete_blob(metafar_blob.merkle);
let server = RootDirCache::new(blobfs_client);
let dir = server.get_or_insert(metafar_blob.merkle, Some(root_dir)).await.unwrap();
assert_eq!(server.list().len(), 1);
drop(dir);
assert_eq!(server.list().len(), 0);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn get_or_insert_provided_root_dir_error_if_already_has_dropper() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let root_dir =
crate::RootDir::new_raw(blobfs_client.clone(), metafar_blob.merkle, Some(Box::new(())))
.await
.unwrap();
let server = RootDirCache::new(blobfs_client);
assert_matches!(
server.get_or_insert(metafar_blob.merkle, Some(root_dir)).await,
Err(crate::Error::DropperAlreadySet)
);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn get_or_insert_fails_if_root_dir_creation_fails() {
let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
let server = RootDirCache::new(blobfs_client);
assert_matches!(
server.get_or_insert([0; 32].into(), None).await,
Err(crate::Error::MissingMetaFar)
);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn get_or_insert_concurrent_race_to_insert_new_root_dir() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let server = RootDirCache::new(blobfs_client);
let fut0 = server.get_or_insert(metafar_blob.merkle, None);
let fut1 = server.get_or_insert(metafar_blob.merkle, None);
let (res0, res1) = futures::future::join(fut0, fut1).await;
let (dir0, dir1) = (res0.unwrap(), res1.unwrap());
assert_eq!(server.list().len(), 1);
assert_eq!(Arc::strong_count(&server.list()[0]), 3);
drop(dir0);
drop(dir1);
assert_eq!(server.list().len(), 0);
assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
}
#[fuchsia::test]
async fn inspect() {
let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
let (metafar_blob, _) = pkg.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
let server = RootDirCache::new(blobfs_client);
let _dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
let inspector = finspect::Inspector::default();
inspector.root().record_lazy_child("open-packages", server.record_lazy_inspect());
assert_data_tree!(inspector, root: {
"open-packages": {
pkg.hash().to_string() => {
"instances": 1u64,
},
}
});
}
}