⚡ perf: reduce lock contention in storage-backed middleware#4373
⚡ perf: reduce lock contention in storage-backed middleware#4373Copilot wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.
| Benchmark suite | Current: b0023b9 | Previous: 3ceebd3 | Ratio |
|---|---|---|---|
Benchmark_Limiter_Custom_Store (github.com/gofiber/fiber/v3/middleware/limiter) - B/op |
335 B/op |
175 B/op |
1.91 |
Benchmark_Limiter (github.com/gofiber/fiber/v3/middleware/limiter) - B/op |
176 B/op |
16 B/op |
11 |
Benchmark_Limiter (github.com/gofiber/fiber/v3/middleware/limiter) - allocs/op |
4 allocs/op |
2 allocs/op |
2 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
Pull request overview
This PR reduces lock contention in storage-backed middleware by introducing sharded keyed locking and narrowing mutex scope around storage operations.
Changes:
- Adds an internal 128-shard keyed locking primitive.
- Updates limiter fixed/sliding windows to serialize storage updates per limiter key.
- Splits cache heap/accounting locking from per-cache-key coordination and narrows session initialization locking.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
internal/keylock/keylock.go |
Adds shared keyed lock implementation. |
internal/keylock/keylock_test.go |
Tests same-key blocking and different-key non-blocking behavior. |
middleware/limiter/limiter_fixed.go |
Replaces global limiter mutex with keyed locking. |
middleware/limiter/limiter_sliding.go |
Applies keyed locking to sliding-window limiter updates. |
middleware/limiter/limiter_test.go |
Adds concurrency tests for different limiter keys. |
middleware/cache/cache.go |
Splits cache key coordination from heap/accounting locking. |
middleware/session/middleware.go |
Moves session store loading outside middleware mutex. |
middleware/session/middleware_test.go |
Adds coverage for narrowed session initialization locking. |
- keylock: capture owner channel in release closure and only delete/close if s.owners[key] still points to the same channel, preventing stale release from corrupting a re-acquired lock. - cache: wrap removeHeapEntry calls after relock() with heapMu to protect shared heap and storedBytes from concurrent corruption across keys.
|
@copilot Three checks are red: Linter (4 findings):
Test ( Benchmark ( |
…ssign - Add mutex to failingLimiterStorage to protect concurrent map access (exposed by per-key locking allowing parallel storage calls) - Fix fieldalignment in keylock.go and limiter_test.go structs - Fix ineffassign: use _, ok instead of ch, ok when ch is unused
All three issues are fixed in commit Data race: The Linter:
Benchmark: The |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4373 +/- ##
==========================================
+ Coverage 92.70% 92.83% +0.13%
==========================================
Files 138 138
Lines 13486 13495 +9
==========================================
+ Hits 12502 12528 +26
+ Misses 614 604 -10
+ Partials 370 363 -7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@copilot The test coverage for the following files requires improvement:
|
Improved test coverage for both files in the latest commit:
|
Merge latest main into the lock-contention branch and reconcile the diverged cache/session refactors: - cache: keep per-key keylock + heapMu accounting while adopting main's cfg.now() timestamp model; drop the obsolete atomic timestamp ticker and the now-unused timestampUpdatePeriod constant. - session: combine main's error-returning initialize signature with the PR's narrowed locking so storage I/O (getSession) runs outside m.mu. - tests: keep both sides' added tests; remove cache coverage tests for the canonicalQueryString/Header/CookieSubset helpers that main replaced with buildVaryKey; check the new initialize error in the lock test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBx3vGQUUm6Mg1XBYmfMnS
Make the per-key lock shard count adaptive instead of a fixed 128. The shard mutex only guards brief map bookkeeping and at most GOMAXPROCS goroutines contend for it at once, so the effective count is now max(minShards, 4*GOMAXPROCS) rounded up to a power of two. The 128 constants in cache and limiter become a floor (minShards). Rounding to a power of two lets Lock select a shard with a bitmask instead of a modulo. Adds unit tests for nextPow2 and shardCount. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBx3vGQUUm6Mg1XBYmfMnS
|
Thanks for the work here. After review we're going to close this rather than merge, and split the underlying issue (#4358) into separately-scoped fixes. Rationale: Locking approach
Worth keeping
Plan for #4358 - re-scope into three efforts, since the three middleware differ:
Closing for now; happy to revisit the coverage tests as their own PR. |
Description
Reduces lock contention in storage-backed middleware by introducing sharded keyed locking and narrowing mutex scope around storage operations.
Changes introduced
Adds an internal 128-shard keyed locking primitive (
internal/keylock) that serializes work per key while limiting global contention.Updates limiter fixed/sliding windows to serialize storage updates per limiter key instead of using a global mutex.
Splits cache heap/accounting locking from per-cache-key coordination and narrows session initialization locking.
Fixes keylock release safety: the release closure now captures the specific owner channel and only deletes/closes it if
s.owners[key]still references that same channel, preventing a stale release from corrupting a re-acquired lock.Fixes cache heap mutex protection:
removeHeapEntrycalls afterrelock()are now wrapped withheapMu.Lock()/heapMu.Unlock()to protect the shared heap andstoredBytesfrom concurrent corruption across different keys.Fixes data race in limiter test storage: adds
sync.MutextofailingLimiterStorageto protect concurrent map access, which was exposed by per-key locking allowing parallel storage calls for different keys.Fixes struct fieldalignment in
keylock.goandlimiter_test.goper govet/betteralign.Fixes ineffectual assignment in
blockingLimiterStorage.SetWithContext.Improves test coverage for
middleware/cache/cache.goandmiddleware/limiter/limiter_sliding.go:rotateWindow75%→100%,bucketForOriginalHit40%→100%,ttlDuration66.7%→100%,secondsToDuration75%→100%,Newhandler 84.1%→96.8%. Added unit tests for all helper functions (fresh entry, full/partial expiration, all bucket return paths, overflow cases), sliding storage get/set error paths, Next callback bypass, and post-handler error paths.secondsToTime80%→100%,secondsToDuration75%→100%,storeVaryManifest75%→100%,loadVaryManifest80%→100%,canonicalHeaderSubset91.7%→100%,canonicalCookieSubset91.7%→100%,makeHashAuthFunc81.2%→100%. Added tests for negative expiration, oversized key buffers, eviction delete errors, vary manifest edge cases, pool fallback paths, and query/header/cookie canonicalization.Benchmarks: Limiter benchmarks may show improvement under high concurrency with distinct keys due to reduced global lock contention. The
Benchmark_SetLoggerregression flagged by CI is unrelated (inlogpackage). TheBenchmark_Limiterdifference is expected under single-key patterns since per-key locking adds a channel allocation vs. a simple mutex; the benefit is under high concurrency with distinct keys.Documentation Update: No documentation changes required.
Changelog/What's New: Reduced lock contention in cache, limiter, and session middleware via sharded per-key locking.
Migration Guide: No migration needed; changes are internal.
API Alignment with Express: N/A
API Longevity: Internal-only changes with no public API impact.
Examples: N/A
Type of change
Checklist
Commit formatting
Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: CONTRIBUTING.md
Fixes #4358