1use fuchsia_inspect as finspect;
6use futures::future::BoxFuture;
7use futures::FutureExt as _;
8use std::collections::HashMap;
9use std::sync::{Arc, Weak};
10
11#[derive(Debug, Clone)]
43pub struct RootDirCache<S> {
44 non_meta_storage: S,
45 dirs: Arc<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<crate::RootDir<S>>>>>,
46}
47
48impl<S: crate::NonMetaStorage + Clone> RootDirCache<S> {
49 pub fn new(non_meta_storage: S) -> Self {
52 let dirs = Arc::new(std::sync::Mutex::new(HashMap::new()));
53 Self { non_meta_storage, dirs }
54 }
55
56 pub async fn get_or_insert(
63 &self,
64 hash: fuchsia_hash::Hash,
65 root_dir: Option<crate::RootDir<S>>,
66 ) -> Result<Arc<crate::RootDir<S>>, crate::Error> {
67 Ok(if let Some(root_dir) = self.get(&hash) {
68 root_dir
69 } else {
70 let dropper = Box::new(Dropper { dirs: Arc::downgrade(&self.dirs), hash });
72 let new_root_dir = match root_dir {
73 Some(mut root_dir) => match root_dir.set_dropper(dropper) {
74 Ok(()) => Arc::new(root_dir),
75 Err(_) => {
78 return Err(crate::Error::DropperAlreadySet);
79 }
80 },
81 None => {
82 crate::RootDir::new_with_dropper(self.non_meta_storage.clone(), hash, dropper)
88 .await?
89 }
90 };
91 use std::collections::hash_map::Entry::*;
92 let root_dir = match self.dirs.lock().expect("poisoned mutex").entry(hash) {
94 Occupied(mut o) => {
96 let old_root_dir = o.get_mut();
97 if let Some(old_root_dir) = old_root_dir.upgrade() {
98 old_root_dir
99 } else {
100 *old_root_dir = Arc::downgrade(&new_root_dir);
101 new_root_dir
102 }
103 }
104 Vacant(v) => {
105 v.insert(Arc::downgrade(&new_root_dir));
106 new_root_dir
107 }
108 };
109 root_dir
110 })
111 }
112
113 pub fn get(&self, hash: &fuchsia_hash::Hash) -> Option<Arc<crate::RootDir<S>>> {
118 self.dirs.lock().expect("poisoned mutex").get(hash)?.upgrade()
119 }
120
121 pub fn list(&self) -> Vec<Arc<crate::RootDir<S>>> {
124 self.dirs.lock().expect("poisoned mutex").iter().filter_map(|(_, v)| v.upgrade()).collect()
125 }
126
127 pub fn record_lazy_inspect(
130 &self,
131 ) -> impl Fn() -> BoxFuture<'static, Result<finspect::Inspector, anyhow::Error>>
132 + Send
133 + Sync
134 + 'static {
135 let dirs = Arc::downgrade(&self.dirs);
136 move || {
137 let dirs = dirs.clone();
138 async move {
139 let inspector = finspect::Inspector::default();
140 if let Some(dirs) = dirs.upgrade() {
141 let package_counts: HashMap<_, _> = {
142 let dirs = dirs.lock().expect("poisoned mutex");
143 dirs.iter().map(|(k, v)| (*k, v.strong_count() as u64)).collect()
144 };
145 let root = inspector.root();
146 let () = package_counts.into_iter().for_each(|(pkg, count)| {
147 root.record_child(pkg.to_string(), |n| n.record_uint("instances", count))
148 });
149 }
150 Ok(inspector)
151 }
152 .boxed()
153 }
154 }
155}
156
157struct Dropper<S> {
159 dirs: Weak<std::sync::Mutex<HashMap<fuchsia_hash::Hash, Weak<crate::RootDir<S>>>>>,
160 hash: fuchsia_hash::Hash,
161}
162
163impl<S> Drop for Dropper<S> {
164 fn drop(&mut self) {
165 let Some(dirs) = self.dirs.upgrade() else {
166 return;
167 };
168 use std::collections::hash_map::Entry::*;
169 match dirs.lock().expect("poisoned mutex").entry(self.hash) {
170 Occupied(o) => {
171 if o.get().strong_count() == 0 {
173 o.remove_entry();
174 }
175 }
176 Vacant(_) => (),
178 };
179 }
180}
181
182impl<S> std::fmt::Debug for Dropper<S> {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 f.debug_struct("Dropper").field("dirs", &self.dirs).field("hash", &self.hash).finish()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use assert_matches::assert_matches;
192 use diagnostics_assertions::assert_data_tree;
193 use fidl_fuchsia_io as fio;
194 use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
195 use fuchsia_pkg_testing::PackageBuilder;
196 use vfs::directory::entry_container::Directory as _;
197
198 #[fuchsia::test]
199 async fn get_or_insert_new_entry() {
200 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
201 let (metafar_blob, _) = pkg.contents();
202 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
203 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
204 let server = RootDirCache::new(blobfs_client);
205
206 let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
207
208 assert_eq!(server.list().len(), 1);
209
210 drop(dir);
211 assert_eq!(server.list().len(), 0);
212 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
213 }
214
215 #[fuchsia::test]
216 async fn closing_package_connection_closes_package() {
217 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
218 let (metafar_blob, _) = pkg.contents();
219 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
220 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
221 let server = RootDirCache::new(blobfs_client);
222
223 let dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
224 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
225 let scope = vfs::execution_scope::ExecutionScope::new();
226 let () = dir.open(
227 scope.clone(),
228 fio::OpenFlags::RIGHT_READABLE,
229 vfs::path::Path::dot(),
230 server_end.into_channel().into(),
231 );
232 let _: fio::ConnectionInfo =
233 proxy.get_connection_info().await.expect("directory succesfully handling requests");
234 assert_eq!(server.list().len(), 1);
235
236 drop(proxy);
237 let () = scope.wait().await;
238 assert_eq!(server.list().len(), 0);
239 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
240 }
241
242 #[fuchsia::test]
243 async fn get_or_insert_existing_entry() {
244 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
245 let (metafar_blob, _) = pkg.contents();
246 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
247 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
248 let server = RootDirCache::new(blobfs_client);
249
250 let dir0 = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
251
252 let dir1 = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
253 assert_eq!(server.list().len(), 1);
254 assert_eq!(Arc::strong_count(&server.list()[0]), 3);
255
256 drop(dir0);
257 drop(dir1);
258 assert_eq!(server.list().len(), 0);
259 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
260 }
261
262 #[fuchsia::test]
263 async fn get_or_insert_provided_root_dir() {
264 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
265 let (metafar_blob, _) = pkg.contents();
266 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
267 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
268 let root_dir = crate::RootDir::new_raw(blobfs_client.clone(), metafar_blob.merkle, None)
269 .await
270 .unwrap();
271 blobfs_fake.delete_blob(metafar_blob.merkle);
272 let server = RootDirCache::new(blobfs_client);
273
274 let dir = server.get_or_insert(metafar_blob.merkle, Some(root_dir)).await.unwrap();
275 assert_eq!(server.list().len(), 1);
276
277 drop(dir);
278 assert_eq!(server.list().len(), 0);
279 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
280 }
281
282 #[fuchsia::test]
283 async fn get_or_insert_provided_root_dir_error_if_already_has_dropper() {
284 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
285 let (metafar_blob, _) = pkg.contents();
286 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
287 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
288 let root_dir =
289 crate::RootDir::new_raw(blobfs_client.clone(), metafar_blob.merkle, Some(Box::new(())))
290 .await
291 .unwrap();
292 let server = RootDirCache::new(blobfs_client);
293
294 assert_matches!(
295 server.get_or_insert(metafar_blob.merkle, Some(root_dir)).await,
296 Err(crate::Error::DropperAlreadySet)
297 );
298 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
299 }
300
301 #[fuchsia::test]
302 async fn get_or_insert_fails_if_root_dir_creation_fails() {
303 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
304 let server = RootDirCache::new(blobfs_client);
305
306 assert_matches!(
307 server.get_or_insert([0; 32].into(), None).await,
308 Err(crate::Error::MissingMetaFar)
309 );
310 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
311 }
312
313 #[fuchsia::test]
314 async fn get_or_insert_concurrent_race_to_insert_new_root_dir() {
315 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
316 let (metafar_blob, _) = pkg.contents();
317 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
318 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
319 let server = RootDirCache::new(blobfs_client);
320
321 let fut0 = server.get_or_insert(metafar_blob.merkle, None);
322
323 let fut1 = server.get_or_insert(metafar_blob.merkle, None);
324
325 let (res0, res1) = futures::future::join(fut0, fut1).await;
326 let (dir0, dir1) = (res0.unwrap(), res1.unwrap());
327
328 assert_eq!(server.list().len(), 1);
329 assert_eq!(Arc::strong_count(&server.list()[0]), 3);
330
331 drop(dir0);
332 drop(dir1);
333 assert_eq!(server.list().len(), 0);
334 assert!(server.dirs.lock().expect("poisoned mutex").is_empty());
335 }
336
337 #[fuchsia::test]
338 async fn inspect() {
339 let pkg = PackageBuilder::new("pkg-name").build().await.unwrap();
340 let (metafar_blob, _) = pkg.contents();
341 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
342 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
343 let server = RootDirCache::new(blobfs_client);
344 let _dir = server.get_or_insert(metafar_blob.merkle, None).await.unwrap();
345
346 let inspector = finspect::Inspector::default();
347 inspector.root().record_lazy_child("open-packages", server.record_lazy_inspect());
348
349 assert_data_tree!(inspector, root: {
350 "open-packages": {
351 pkg.hash().to_string() => {
352 "instances": 1u64,
353 },
354 }
355 });
356 }
357}