🐛 fix: Heap accounting and data race in cache middleware#3985
🐛 fix: Heap accounting and data race in cache middleware#3985gaby merged 8 commits intoupdate-cache-handling-for-authenticationfrom
Conversation
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Comment |
Remove heap entries before revalidating to prevent duplicate heap records and byte counting issues. This fixes: - maxAge forced revalidation - minFresh forced revalidation - forceRevalidate directive handling - expired entry revalidation Addresses feedback from code review on lines 284-288. Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
|
@copilot Also fix this other issue raised here: #3973 (comment) |
Avoid mutating shared cache entry in memory mode by computing clamped date locally instead of modifying e.date. When using in-memory storage, the same *item pointer is shared across concurrent requests, so mutating e.date causes data races. Addresses feedback: #3973 (comment) Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Fixed in commit 07e46b9. Changed from mutating |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## update-cache-handling-for-authentication #3985 +/- ##
=========================================================================
Coverage 90.86% 90.87%
=========================================================================
Files 119 119
Lines 10749 10758 +9
=========================================================================
+ Hits 9767 9776 +9
Misses 623 623
Partials 359 359
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07e46b9656
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if reqDirectives.maxAgeSet && (reqDirectives.maxAge == 0 || entryAge > reqDirectives.maxAge) { | ||
| revalidate = true | ||
| removeHeapEntry(key, e.heapidx) | ||
| if cfg.Storage != nil { | ||
| manager.release(e) |
There was a problem hiding this comment.
Keep heap accounting when revalidation skips store
When a request forces revalidation (e.g., Cache-Control: max-age=0 or min-fresh), the code now drops the cached item from the heap accounting up front. If the origin response is subsequently not cacheable (no-store, non-cacheable status, Next veto, etc.), the old entry stays in storage but is no longer represented in storedBytes/heap, while the eviction loop that enforces MaxBytes only consults those counters (see the eviction loop around the MaxBytes check). This leaves resident bodies untracked and allows later writes to exceed MaxBytes without eviction. The heap entry should only be removed once a replacement is written or the old entry is actually deleted to keep size accounting correct.
Useful? React with 👍 / 👎.
Only remove old heap entry when replacement is actually stored. Previously, heap entry was removed immediately when revalidation was triggered, which caused accounting issues if the new response wasn't cacheable. Now we: - Track the old heap index when marking for revalidation - Keep the old entry in heap accounting until replacement - Remove old heap entry only when storing the new entry This ensures MaxBytes accounting remains accurate even when revalidation fails or response is not cacheable. Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Fixed in commit 8316bb6. Changed approach to only remove the old heap entry when the replacement is actually stored. Now we:
This ensures accurate MaxBytes accounting even when revalidation fails or the new response isn't cacheable. |
There was a problem hiding this comment.
Pull request overview
This PR addresses two issues in the cache middleware: heap accounting during revalidation and a data race in cache entry date mutation. The date mutation fix correctly eliminates the race condition, but the heap accounting implementation has a critical bug that contradicts the PR's stated goal.
Key Changes:
- Track old heap index (
oldHeapIdx) when marking entries for revalidation across multiple code paths (max-age=0, min-fresh, forceRevalidate, and expired entries marked for revalidation) - Change date handling from mutating
e.dateto using a localclampedDatevariable to prevent data races in concurrent access - Attempt to remove old heap entries before storing new ones during revalidation
|
@copilot - The old heap entry is being removed before the storage operation completes successfully. If storage fails (lines 770, 778, or 786), the cleanupOnStoreError function will remove the new heap entry (line 752), leaving no entry in the heap even though the old cached entry should have been preserved. This contradicts the PR description which states "old heap entries remain tracked until a replacement is actually stored." To fix this properly, the old heap entry should only be removed after the storage operation succeeds. Consider moving the removeHeapEntry call to after the successful storage operations (after line 792), or handle cleanup differently to restore the old entry if storage fails. |
Remove old heap entry AFTER successful storage instead of before. This ensures that if storage fails, the old entry remains tracked in the heap. Previously: - Old heap entry was removed before storage - If storage failed, cleanup would remove the new entry - Result: no heap entry at all, but old cached data still in storage Now: - Add new heap entry before storage (as before) - If storage fails, cleanup removes new entry, old stays tracked - If storage succeeds, remove old heap entry after - Result: accurate accounting in all scenarios Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Added 4 test cases covering: - max-age=0 revalidation removes old entry on success - min-fresh revalidation with MaxBytes - Revalidation respects MaxBytes eviction - Non-cacheable response preserves old entry These tests verify heap accounting remains accurate during revalidation scenarios when MaxBytes is enabled. Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Fixed in commit be74d15. The old heap entry is now removed only after storage succeeds (lines 790-795), not before. This ensures that if storage fails, the old entry remains tracked in the heap and accurate heap accounting is maintained. |
|
@copilot Run the following commands: - make generate - make format |
Applied code formatting with gofumpt to ensure consistency with project standards. Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
|
@copilot Error: middleware/cache/cache_test.go:3453:10: Error return value of |
Check error return values from io.ReadAll in test cases to satisfy errcheck linter. Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
1e57098
into
update-cache-handling-for-authentication
Description
This PR addresses two critical issues in the cache middleware that could cause data corruption and race conditions:
Heap accounting during revalidation: When clients force cache revalidation (via
Cache-Control: max-age=0,min-fresh, or other directives), the middleware needed to properly manage heap entries to maintain accurate byte accounting forMaxBytesenforcement. The fix ensures that old heap entries remain tracked until a replacement is successfully stored, preventing accounting errors when revalidation fails or the new response isn't cacheable.Data race in cache entry date mutation: The middleware was mutating the shared cache entry's
datefield when using in-memory storage. Since the same*itempointer is shared across concurrent requests in memory mode, this mutation caused data races without proper synchronization.Changes introduced
Heap accounting on revalidation: Modified revalidation handling to defer heap entry removal until after successful storage:
max-age=0, aged-out entries, insufficient remaining freshness,forceRevalidatedirective, or expired entries marked for revalidation)storedBytesaccounting throughout the revalidation processMaxBytesaccounting in all scenarios, including storage failuresThread-safe date handling: Changed from mutating
e.datedirectly to computing the clamped date in a local variable. This prevents data races when using in-memory storage where the same*itempointer is shared across concurrent requests.Comprehensive test coverage: Added
Test_Cache_RevalidationWithMaxByteswith 4 test scenarios:These tests verify heap accounting and storedBytes remain consistent during revalidation with MaxBytes enabled, with proper error handling for all
io.ReadAllcalls.Code quality: Applied
make generateandmake formatto ensure code consistency with project standards using gofumpt. Fixed errcheck linter warnings by properly checking error return values in test code.These changes ensure heap consistency, accurate byte accounting for cache eviction under
MaxBytesconstraints even during storage failures, and thread-safe operation of the cache middleware.Type of change
Checklist
/docs/directory for Fiber's documentation.💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.