[ML] Fix concurrent LFU cache count invariant under lock timeouts#3090
Open
edsavage wants to merge 2 commits into
Open
[ML] Fix concurrent LFU cache count invariant under lock timeouts#3090edsavage wants to merge 2 commits into
edsavage wants to merge 2 commits into
Conversation
The concurrent cache only incremented m_NumberLookups inside the read-guard lambda, which does not run when the read lock times out. The subsequent miss path can still add a count (insert/increment) or record a lost update, so under contention the surviving counts could exceed the number of accounted lookups and intermittently trip checkInvariants() (observed as a flaky failure of testConcurrentReadsAndWrites on loaded debug CI agents). Count every lookup exactly once, before taking the read lock. Also relax the count invariant for the timeout-capable cache to a one-sided bound: counts can legitimately be lost under concurrency (an item hit under the read lock may be evicted before its count is incremented, one insert can evict several items, and write updates can time out) but must never be fabricated. The exact equality is retained for the single-threaded cache, which has no timeouts. Add a regression test that drives lock timeouts via a zero maximum wait under heavy contention; it fails reliably against the previous code. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Pinging @elastic/ml-core (Team:ML) |
There was a problem hiding this comment.
Pull request overview
This PR fixes a flaky invariant check in core::CCompressedLfuCache under contention by ensuring lookups are always accounted for (even when lock acquisition times out) and by making the invariant tolerant of lost counts in the timeout-capable concurrent cache.
Changes:
- Move
m_NumberLookupsincrement outside the read-guard lambda so read-lock timeouts can’t desynchronise lookup accounting. - Relax the count invariant for the concurrent (timeout-capable) cache to require a one-sided bound (
totalCount <= accountedLookups) while keeping strict equality for the non-timeout cache. - Add a new stress/regression test that forces lock timeouts (
maximumWait = 0ms) and validates invariants remain satisfied.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| lib/core/unittest/CCompressedLfuCacheTest.cc | Adds a regression test that heavily contends the concurrent cache with zero lock wait to ensure the invariant remains stable under timeouts. |
| include/core/CCompressedLfuCache.h | Fixes lookup counting under lock timeouts and relaxes the invariant appropriately for the concurrent cache. |
| docs/changelog/3090.yaml | Adds a changelog entry documenting the bug fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+296
to
+298
| // number of accounted lookups. When updates cannot time out (the | ||
| // single-threaded cache) there is no concurrency and the relation is | ||
| // exact. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
testConcurrentReadsAndWrites(inCCompressedLfuCacheTest) intermittently fails on loaded debug CI agents with:Root cause
CCompressedLfuCache::lookupincrementedm_NumberLookupsinside the read-guard lambda, which does not run when the read lock acquisition times out (m_MaximumWaitis only 50 ms). When the read lock times out the lookup falls through to the miss path, which can still:m_LostCount.In every case the write side changed the bookkeeping without the matching lookup being counted, so the surviving counts could exceed
m_NumberLookups - m_LostCount, tripping the strict-equality check incheckInvariants(). This is a benign accounting asymmetry, not cache corruption, but it produces a real flaky test under contention.There is also an inherent, opposite race: an item seen as a hit under the read lock can be evicted before the write lock increments its count, so counts can also legitimately be lost. The pre-existing
!=(strict equality) check was fragile in both directions.Fix
totalCount <= accountedLookups). The exact equality is retained for the single-threaded cache, which has no timeouts or races. The structural invariants (size / link / memory) remain strict.Test
Adds
testConcurrentCountInvariantWithTimeouts, which drives read- and write-lock timeouts via a zero maximum wait under heavy contention. It fails 20/20 against the previous code and passes with the fix.Test plan
testConcurrentReadsAndWritespassestestConcurrentCountInvariantWithTimeoutspasses with the fix and fails reliably (20/20) without ittestLookup,testMemoryUsage,testResize) still pass (strict-equality invariant path unchanged)Made with Cursor