Repository navigation
Fix v3 data race and Add eviction reporting, correct docs and bump deps - #26
Merged
Merged
Conversation
Values sized the result slice from c.items before taking the mutex, so a concurrent Set or Remove could race on the map, or crash with concurrent map read and map write.
Add documents the simplelru contract, returns true if an eviction occurred, but reported only the capacity eviction. With a non-default TTL it silently dropped the expired oldest entry, fired onEvicted and counted the eviction in stats while still returning false.
The v3 example did not compile, it type-asserted an already typed string, and claimed an expired Get returns nil while it returns the stored value with ok set to false. The example is now mirrored by v3/example_test.go, so the documented output is verified by go test. LRC is described as it behaves: the order is defined by the last write, an existing key is moved to the front on Set, and reads leave the order alone. Go version requirements per major version are documented as well.
Ristretto applies writes asynchronously, and only BenchmarkRistretto_Set waited for them to be applied. The mixed benchmarks measured enqueueing a write and counted rejected writes as successful ones, so their numbers were not comparable with the synchronous Set of the other libraries. Rejected writes are now counted and reported as drops/op, and the published tables are regenerated from a single run on Apple M3.
Testify moves to v1.12.0 in all three versions, benchmarks get ttlcache v3.4.1 with x/sync and x/sys pinned to the last releases compatible with the declared go version. Published tables are regenerated: ttlcache is noticeably faster since v3.4.1 and no longer allocates on Get.
Coverage Report for CI Build 32192741761Coverage remained the same at 100.0%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Contains looked the key up in the map only, so it returned true for an entry Get and Peek already report as a miss, and without a background purge that disagreement lasts until DeleteExpired or a Set that pops the entry off the back. The lookup functions now agree, while Len and Keys keep reporting everything the cache holds.
Set returns false only when the write buffer is full, an accepted write can still be discarded later by the admission policy, so the metric is a lower bound rather than a count of policy rejections. The readme example ran on a 10ms TTL and asserted a hit right after the write, which a scheduler stall could expire.
umputun
approved these changes
Aug 18, 2026
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.
Seven independent fixes, one commit each, so they can be split or dropped individually.
Data race in
ValuesPreviously,
Valuessized the result slice fromc.itemsbefore taking the mutex, so a concurrentSetorRemoveraced on the map and could terminate the process with "concurrent map read and map write". After this change the allocation happens under the lock. The newTestCacheValuesConcurrencyfails under-racewithout the fix.Adddid not report the eviction of an expired entryAddimplements thesimplelru.LRUCachecontract and returns true if an eviction occurred, but it reported the capacity eviction only. With a non-default TTL,Addremoves the expired oldest entry, callsonEvictedand counts the eviction in stats, while still returning false, soWithTTL(...); Add(k1); wait; Add(k2)returns false after evictingk1. After this change both cases set the result. Only v3 exposesAdd, v1 and v2 haveSetwith no return value and are unaffected.The v3 module was not tidy
github.com/hashicorp/golang-lru/v2is a direct test import but was marked indirect, andgo.sumkept obsolete testify hashes.The readme example did not compile
The v3 example type-asserted an already typed
string, and the documented output claimed an expiredGetreturns<nil>while it returns the stored value withokset to false. The example is now mirrored byv3/example_test.go, so its output is verified bygo test. LRC is described as it behaves: the order follows the last write,Seton an existing key moves it to the front, and reads leave the order alone. Go version requirements per major version are documented as well.Ristretto benchmarks measured different work than the others
Ristretto applies writes asynchronously, and only
BenchmarkRistretto_Setwaited for them to be applied. The mixed benchmarks measured enqueueing a write and counted rejected writes as successful ones, so their numbers were not comparable with the synchronousSetof the other libraries. After this change every timed write is followed bycache.Wait(), rejected writes are counted and reported as adrops/opmetric, and the published tables are regenerated. Ristretto Set+Get moves from 198 to 529 ns/op once the write actually completes.Containsignored expirationContainslooked the key up in the map only, so it returned true for an entry thatGetandPeekalready report as a miss. This library does not purge in the background, so the disagreement lasts untilDeleteExpiredis called or aSetpops the entry off the back of the list, which makesif c.Contains(k) { v, _ := c.Get(k); ... }hand back a stale value. After this change the lookup functions agree with each other, whileLenandKeyskeep reporting everything the cache still holds. This is a deliberate divergence fromhashicorp/golang-lru/v2/expirable, whereContainsis expiry-blind as well, but a background goroutine keeps the window short.Dependency bump
Testify moves to v1.12.0 in all three versions, benchmarks get ttlcache v3.4.1, with
x/syncandx/syspinned to the last releases compatible with the declared go version, so nogodirective changes. The published tables are regenerated on the updated dependencies: ttlcache is noticeably faster since v3.4.1 and no longer allocates on Get.