Skip to content

fix(storage): shut down dedicated runtime on engine drop to break self-cycle leak - #20

Open
detail-app[bot] wants to merge 2 commits into
mainfrom
detail/bug-fix/fix-storage-shut-down-dedicated-runtime-on-engine-632120
Open

detail-app[bot] wants to merge 2 commits into
mainfrom
detail/bug-fix/fix-storage-shut-down-dedicated-runtime-on-engine-632120

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 11, 2026

Copy link
Copy Markdown

Detail bug report: View on Detail

Bug

A disk cache built with a dedicated runtime via with_spawner(Spawner::Runtime(...)) (the path used by the README quickstart and examples/hybrid_full.rs) leaks the entire dedicated tokio::Runtime — worker threads, pending tasks, channels, and buffers — when the cache is dropped in a long-running process.

Root cause: the block engine spawns its flusher/reclaimer workers as tasks on that dedicated runtime, and each worker future captures a Spawner clone (Arc<BackgroundShutdownRuntime>). This forms a self-cycle — Arc<BackgroundShutdownRuntime> → Runtime → spawned task future → Spawner(Arc<…>) — that keeps the Arc strong count above zero forever. Since Runtime::shutdown_background() was only ever called from BackgroundShutdownRuntime::drop (which fires on the last Arc clone), it never ran: the runtime was never torn down. The default Spawner::Handle path was unaffected (no Arc<BSR> exists there; the external runtime owns the lifecycle).

Fix

  • foyer-common/src/spawn.rs: Redesigned BackgroundShutdownRuntime to hold Mutex<Option<Runtime>> + a stored Handle (spawning now goes through Deref<Target=Handle>; Handle exposes both spawn and spawn_blocking, which are the only methods used in-tree). Added an idempotent BackgroundShutdownRuntime::shutdown() that takes the Runtime out of the Mutex and calls shutdown_background() (or drop(runtime) under madsim); Drop calls it. Added Spawner::shutdown() (no-op for Spawner::Handle). shutdown_background is non-blocking (signals + returns; worker threads self-exit), so it is safe even when called from within the dedicated runtime itself.
  • foyer-storage/src/engine/block/engine.rs: Added impl Drop for BlockEngineInner that calls self._spawner.shutdown(), forcing the dedicated runtime to tear down when the engine is dropped. The forced shutdown drops the spawned flusher/reclaimer task futures (releasing their captured Arc<…> clones), so the Arc<BackgroundShutdownRuntime> finally reaches zero.
  • foyer-storage/Cargo.toml: Added rt-multi-thread,macros to the tokio dev-dependency so the dedicated-runtime regression tests (which build a new_multi_thread runtime) compile under single-package cargo nextest run -p foyer-storage.

Testing

Added 4 regression tests (gated #[cfg(not(madsim))]) that hold only a Weak<BackgroundShutdownRuntime> across drop(store)/drop(engine) and assert weak.upgrade().is_none() afterward (impossible before the fix — the bug report's repro showed weak.upgrade().is_some()=true):

  • store::tests::test_dedicated_runtime_released_on_drop — drop after wait()
  • store::tests::test_dedicated_runtime_released_on_close_then_drop — close() does not tear down (still alive after close()), but subsequent drop() releases the runtime
  • store::tests::test_dedicated_runtime_no_accumulation — dropping N=4 caches releases all N runtimes (no linear growth)
  • engine::block::engine::tests::test_dedicated_runtime_released_on_engine_drop — engine-level teardown works without the Store wrapper

Verification:

  • Unit tests, typecheck, lint, and build all pass: foyer-common (36), foyer-storage (25 default / 28 with test_utils incl. fuzzy), foyer-memory (31), foyer (12 incl. hybrid fuzzy). No regressions on the default Spawner::Handle path.
  • cargo fmt --all -- --check and nightly ffmt both clean; cargo clippy clean on the touched crates (only the pre-existing environmental chunks_exact_to_as_chunks unknown-lint note).
  • madsim build + tests pass (103/103); the #[cfg(madsim)] drop(runtime) shutdown branch is exercised via BlockEngineInner::Drop in every test_store_* test.
  • End-to-end: cargo run -p examples --example hybrid_full completes without hanging; cargo run -r -p foyer-bench -- --runtime dedicated ... completes with Close takes: 6.7ms (clean teardown instead of leaking until process exit).

Automatic Fixes PRs can be configured here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants