Skip to content

Fix v3 data race and Add eviction reporting, correct docs and bump deps - #26

Merged
umputun merged 8 commits into
masterfrom
fix-values-race-and-docs
Aug 18, 2026
Merged

umputun merged 8 commits into
masterfrom
fix-values-race-and-docs

Conversation

@paskal

@paskal paskal commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator

Seven independent fixes, one commit each, so they can be split or dropped individually.

Data race in Values

Previously, Values sized the result slice from c.items before taking the mutex, so a concurrent Set or Remove raced 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 new TestCacheValuesConcurrency fails under -race without the fix.

Add did not report the eviction of an expired entry

Add implements the simplelru.LRUCache contract and returns true if an eviction occurred, but it reported the capacity eviction only. With a non-default TTL, Add removes the expired oldest entry, calls onEvicted and counts the eviction in stats, while still returning false, so WithTTL(...); Add(k1); wait; Add(k2) returns false after evicting k1. After this change both cases set the result. Only v3 exposes Add, v1 and v2 have Set with no return value and are unaffected.

The v3 module was not tidy

github.com/hashicorp/golang-lru/v2 is a direct test import but was marked indirect, and go.sum kept obsolete testify hashes.

The readme example did not compile

The v3 example type-asserted an already typed string, and the documented output 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 its output is verified by go test. LRC is described as it behaves: the order follows the last write, Set on 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_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. After this change every timed write is followed by cache.Wait(), rejected writes are counted and reported as a drops/op metric, and the published tables are regenerated. Ristretto Set+Get moves from 198 to 529 ns/op once the write actually completes.

Contains ignored expiration

Contains looked the key up in the map only, so it returned true for an entry that Get and Peek already report as a miss. This library does not purge in the background, so the disagreement lasts until DeleteExpired is called or a Set pops the entry off the back of the list, which makes if c.Contains(k) { v, _ := c.Get(k); ... } hand back a stale value. After this change the lookup functions agree with each other, while Len and Keys keep reporting everything the cache still holds. This is a deliberate divergence from hashicorp/golang-lru/v2/expirable, where Contains is 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/sync and x/sys pinned to the last releases compatible with the declared go version, so no go directive changes. The published tables are regenerated on the updated dependencies: ttlcache is noticeably faster since v3.4.1 and no longer allocates on Get.

paskal added 6 commits August 18, 2026 22:13
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.
@paskal
paskal requested a review from umputun as a code owner August 18, 2026 21:26
@coveralls

coveralls commented Aug 18, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 32192741761

Coverage remained the same at 100.0%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 10 of 10 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 228
Covered Lines: 228
Line Coverage: 100.0%
Coverage Strength: 2968.51 hits per line

💛 - Coveralls

paskal added 2 commits August 18, 2026 23:12
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
umputun merged commit 2b85c61 into master Aug 18, 2026
8 checks passed
@umputun
umputun deleted the fix-values-race-and-docs branch August 18, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants