fix FlatKV cache memory leak - #4084
Conversation
PR SummaryMedium Risk Overview Retirement now sets Adds integration and fuzz tests that the bound-channel path still delivers results and that no terminal entry keeps a channel buffer that disagrees with Reviewed by Cursor Bugbot for commit 1842b15. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4084 +/- ##
==========================================
- Coverage 61.23% 60.16% -1.07%
==========================================
Files 2177 2069 -108
Lines 190632 178137 -12495
==========================================
- Hits 116729 107181 -9548
+ Misses 62892 60987 -1905
+ Partials 11011 9969 -1042
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
The channel-binding refactor is sound: entry.valueChan is only read in lookupLocked's statusScheduled branch, and both retire paths move the entry to a terminal status under the same lock, so nil'ing the field cannot strand a waiter or hand a reader a nil channel — while it does release the stale buffered value that caused the leak. One non-blocking note on the new fuzz test's effective coverage.
Findings: 0 blocking | 1 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- None at the file/PR level.
- 1 suggestion(s)/nit(s) flagged inline on specific lines.
| // gates, or timing dependency. | ||
| func TestRetireNeverLeavesStaleChannelValue(t *testing.T) { | ||
| db := newTestDB(nil) | ||
| shard := newTestShard(t, 1<<30, db) |
There was a problem hiding this comment.
[suggestion] With maxSize = 1<<30 and 1-byte keys / ≤8-byte values, gcQueue.GetTotalSize() never exceeds the budget, so case 4: evict is a permanent no-op. Since eviction is the only transition back to statusUnknown, each of the four keys reaches a terminal status once and stays there: after the first few iterations case 0 returns immediate, case 1 finds nothing pending, and case 4 does nothing. racedCompletions is therefore capped at 4 (one per key), and the bulk of the 1000 iterations exercise nothing.
The test still catches the bug it was written for (the non-raced retire-after-resolve path fails the invariant pre-fix), so this isn't blocking — but a small maxSize (e.g. a few tens of bytes, matching newTestConfig's style) would make evictions real, recycle entries back through statusUnknown, and let the loop actually explore the read/retire/evict interleavings the doc comment claims. Note entryLocked recreates evicted entries with a fresh channel and pending[key] is overwritten on the new needsSchedule outcome, so no channel receives two injectValue sends and the loop stays hang-free under eviction.
While there: randomValue never returns nil, so injectValue's statusDeleted branch is never hit — occasionally injecting a nil value would cover it.
|
it looks the detach logic is something each caller (multiple callers there not limited to ones we addressed) has to remember rather than an invariant. could we fold all fields into one function under cacheEntry struct ? something like: |
Describe your changes and provide context
fix a (slow) memory leak in the FlatKV cache layer