Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions glidefs/src/block/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
}

// ============================================================================
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟡 Medium] [🔵 Bug]

FoyerBlockCache::close logs and suppresses self.inner.close() failures, so capture_once cannot detect that cleanup did not complete and may still leave deleted-but-held region files pinned in $TMPDIR; this weakens the new leak fix under error conditions. Return a Result from BlockCache::close and propagate/handle it at the call site so failed shutdown is explicit and retryable instead of silently continuing.

// glidefs/src/block/cache.rs
async fn close(&self) {
    if let Err(e) = self.inner.close().await {
        tracing::warn!(error = %e, "foyer clean cache close failed");
    }
}

tracing::warn!(error = %e, "foyer clean cache close failed");
}
}
}

// ============================================================================
Expand Down
19 changes: 19 additions & 0 deletions glidefs/src/oci/boot_capture_served.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)]
Expand Down
Loading