diff --git a/glidefs/src/block/cache.rs b/glidefs/src/block/cache.rs index 66879b4..76b8dcc 100644 --- a/glidefs/src/block/cache.rs +++ b/glidefs/src/block/cache.rs @@ -29,6 +29,14 @@ pub trait BlockCache: Send + Sync { fn insert(&self, hash: Blake3Hash, data: Bytes); /// Remove a block from the cache. fn remove(&self, hash: &Blake3Hash); + /// Flush and shut down the cache, releasing any backing device fds. + /// + /// Must be awaited before a short-lived cache's directory is removed: + /// foyer's SSD-tier region files stay open until the storage engine is + /// closed, so dropping the dir first orphans them as deleted-but-held fds + /// that pin the filesystem for the whole process lifetime. Default no-op + /// for in-memory implementations that hold no fds. + async fn close(&self) {} } // ============================================================================ @@ -135,6 +143,12 @@ impl BlockCache for FoyerBlockCache { fn remove(&self, hash: &Blake3Hash) { self.inner.remove(hash); } + + async fn close(&self) { + if let Err(e) = self.inner.close().await { + tracing::warn!(error = %e, "foyer clean cache close failed"); + } + } } // ============================================================================ diff --git a/glidefs/src/oci/boot_capture_served.rs b/glidefs/src/oci/boot_capture_served.rs index 6c9b1cc..e37801b 100644 --- a/glidefs/src/oci/boot_capture_served.rs +++ b/glidefs/src/oci/boot_capture_served.rs @@ -233,6 +233,15 @@ async fn capture_once( .await .ok()?, ); + // Keep a handle to close the foyer clean cache before `tmp` (the TempDir) + // is dropped. foyer's SSD-tier region files stay open until the storage + // engine is closed; if the TempDir's `remove_dir_all` runs first, those + // files become deleted-but-held fds that never free until the daemon + // exits — so a daemon that profiles many images leaks them unbounded into + // $TMPDIR (which is tmpfs on many hosts → node-wide ENOSPC). + let clean_for_close = Arc::clone(&clean); + + let result = async { let pack_index_cache = Arc::new(PackIndexCache::open(tmp.path()).await.ok()?); let handler = Arc::new( BlockHandler::new( @@ -301,6 +310,16 @@ async fn capture_once( return None; } Some(blocks) + } + .await; + + // Close the foyer clean cache (flush + release device fds) BEFORE `tmp` is + // dropped, so the TempDir cleanup actually frees the region files instead + // of orphaning them. Runs on every exit path (including the early `None`s + // above) because the work above is wrapped in the `result` async block. + clean_for_close.close().await; + + result } #[cfg(test)]