Skip to content

fix: rebalance LFU queues on resize to prevent post-shrink cache freeze - #19

Open
detail-app[bot] wants to merge 2 commits into
mainfrom
detail/bug-fix/fix-rebalance-lfu-queues-on-resize-to-prevent-post-f1a55c
Open

detail-app[bot] wants to merge 2 commits into
mainfrom
detail/bug-fix/fix-rebalance-lfu-queues-on-resize-to-prevent-post-f1a55c

Conversation

@detail-app

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

Copy link
Copy Markdown

Detail bug report: View on Detail

Bug

Lfu::update (foyer-memory/src/eviction/lfu.rs), invoked on every Cache::resize via RawCacheShard::resize to refresh per-queue weight budgets, recomputed the budgets but never rebalanced records across queues. After a capacity shrink, the pop-driven eviction loop drained window and probation completely before touching protected (pop only evicts from protected when both are empty), leaving protected over its new budget and window/probation empty. From that state every new insert was unconditionally evicted (pop saw an empty probation and removed from window with no frequency comparison), so the cache froze and rejected all new entries until old protected entries were removed by TTL/invalidation. The update path had no test coverage, so this went undetected.

Fix

Rebalance the queues inside Lfu::update after recomputing budgets: overflow protected→probation while protected_weight > protected_weight_capacity, then window→probation while window_weight > window_weight_capacity. This mirrors Lru::update's may_overflow_high_priority_pool() and reuses the exact primitives already in push (window→probation) and acquire (protected→probation), including push_back so demoted entries become MRU of probation — preserving LRU/MRU ordering and w-TinyLFU admission (recently-hot demoted entries shouldn't be evicted ahead of fresh window entries).

Testing

  • New regression tests (all in-tree):
    • eviction::lfu::tests::test_lfu_update_rebalance_on_shrink — reproduces a 100→50 resize, asserts protected/window are within budget after update, total weight is unchanged across update (move-only), invariants survive the subsequent evict, probation is non-empty (no freeze), and a re-inserted high-frequency key is admitted.
    • eviction::lfu::tests::test_lfu_update_grow_is_noop — grow moves no records (guards against spurious demotion on grow).
    • eviction::lfu::tests::test_lfu_update_to_zero — resize to 0 demotes all entries to probation, then clears (loop termination at zero budget).
    • eviction::lfu::tests::test_lfu_update_with_new_config — new ratios are applied and rebalanced against; an invalid config is rejected with budgets left untouched.
    • cache::tests::test_lfu_cache_resize_admits_after_shrink — end-to-end through the public Cache::resize API cited in the bug; re-inserts a high-frequency key after the shrink and asserts it's admitted.
  • Regression-catch confirmed: with the fix reverted to the original Ok(()) body, the shrink/zero/config/public-API tests fail with the expected invariant panics (e.g. protected_weight 60 must be <= protected_weight_capacity 30; public test: high-frequency re-inserted key must be admitted after shrink); all pass with the fix.
  • Routine checks: build, clippy, fmt --check, and the full foyer-memory --features "strict_assertions" suite (36 tests, including the pre-existing test_lfu_cache_resize, all 5 concurrent fuzzy stress tests, and test_lru_pin_resize_no_panic) pass. The public Cache::resize path also builds across foyer, foyer-memory, and foyer-storage.

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