[release/10.0] Fix MemoryCache negative _cacheSize drift that permanently latches a size-limited cache#129510
[release/10.0] Fix MemoryCache negative _cacheSize drift that permanently latches a size-limited cache#129510github-actions[bot] wants to merge 8 commits into
Conversation
Decrement the prior entry's size exactly once, atomically with the TryUpdate that swaps it out, instead of speculatively inside UpdateCacheSizeExceedsCapacity before the swap. The speculative subtraction races with a concurrent RemoveEntry of the prior entry (expiration/explicit Remove/eviction), double-counts the decrement, drives _cacheSize negative, and permanently latches the cache into silently rejecting all inserts. Restores the .NET 8 accounting semantics. Fixes #129186
The first revision removed priorEntry.Size from the capacity check as well as the commit, which regressed CapacityTests.ReplaceOldEntryWithSameSizeOrLessNew EntryAtSizeLimitCapacity (a same-or-smaller replace at the size limit was falsely rejected). Restore the prior-aware capacity decision while still committing only entry.Size to _cacheSize; the prior entry's size is decremented exactly once, atomically with the TryUpdate swap, which is what fixes the race.
Back the concurrency workers with dedicated threads via TaskCreationOptions.LongRunning instead of Task.Run so the storm cannot saturate the shared ThreadPool and starve timing-sensitive post-eviction callbacks in sibling tests. Sample CurrentEstimatedSize inline (dropping the busy-spin monitor task), bound the work to a fixed iteration count, and gate the test on PlatformDetection.IsThreadingSupported.
|
Tagging subscribers to this area: @dotnet/area-extensions-caching |
|
Hi, the code complete date for 10.0.10 (the July 2026 release) is 24 June. Make sure to merge this PR on that date at the latest (or explicitly let me know that I should merge it), or it won't make it into that release. As a reminder, if this is a product change, you also need Tactics approval before merging this PR (test-only or infra-only changes don't require Tactics approval). |
|
Approved via email. Waiting for customer validation. |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "2b13233fc12848285f003bb067277ec82c9b4c23",
"last_dispatched_base_ref": "release/10.0",
"last_dispatched_base_sha": "eca70963946257444a6e4cfbb06f5eec58156370",
"last_reviewed_commit": "2b13233fc12848285f003bb067277ec82c9b4c23",
"last_reviewed_base_ref": "release/10.0",
"last_reviewed_base_sha": "eca70963946257444a6e4cfbb06f5eec58156370",
"last_recorded_worker_run_id": "29684900070",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "2b13233fc12848285f003bb067277ec82c9b4c23",
"review_id": 4730670457
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: The problem is real and well-documented. Regression #103931 (shipped in 9.0) moved the prior-entry _cacheSize decrement out of the post-swap if (entryAdded) block into the speculative capacity computation in UpdateCacheSizeExceedsCapacity, before the TryUpdate swap. A concurrent RemoveEntry(priorEntry) in that window double-counts the decrement, driving _cacheSize negative; the (ulong) cast in the capacity check then permanently latches the cache into rejecting every Set. This is confirmed as a silent, permanent, customer-impacting data-loss bug.
Approach: Correct and minimal. The fix keeps #103931's prior-aware capacity check (preserving the #36039 replace-at-limit behavior) but commits only +entry.Size to _cacheSize in UpdateCacheSizeExceedsCapacity, and decrements priorEntry.Size exactly once, atomically tied to a successful TryUpdate swap — restoring the pre-#103931 (8.x) ordering. The failure-path rollback is correctly narrowed to -entry.Size only, since a failed TryUpdate means the prior entry was never swapped out and its size must not be touched here. The only remaining transient window over-counts by a positive priorEntry.Size, which is self-correcting and can never latch the cache.
Summary: ✅ LGTM. This is a faithful backport of merged upstream PR #129215 to release/10.0. I diffed the head commit's MemoryCache.cs against the upstream merge commit (98d5787): the four fix hunks in SetEntry and UpdateCacheSizeExceedsCapacity are byte-for-byte identical; the remaining source differences are unrelated metrics/IMeterFactory features that exist only on main and are correctly absent from the servicing branch. The one intentional test adaptation — IsThreadingSupported in place of upstream's IsMultithreadingSupported — is correct, because IsMultithreadingSupported does not exist in release/10.0's PlatformDetection while IsThreadingSupported does and carries equivalent WASM/browser-skip semantics. The new [OuterLoop] stress test is well-targeted (working set far below SizeLimit, asserts no negative drift and no latching) and would fail against the unfixed code. No actionable findings. This change is servicing-approved and appropriately low-risk for a concurrency fix.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 81.8 AIC · ⌖ 10.7 AIC · ⊞ 10K
Backport of #129215 to release/10.0
/cc @cincuranet @sablancoleis
Customer Impact
Customer reported - description from @sablancoleis who reported it:
Regression
From #103931 in .NET 9
Testing
Tests added.
Risk
Medium. Concurrency-sensitive accounting code. Stress test added.