Context
Raised as a suggestion on the ValidatingCache.Get implementation.
Currently, singleflight is used only to deduplicate concurrent cache misses (the load path). The cache hit + liveness check path runs outside the singleflight group, meaning concurrent goroutines can independently trigger storage.Load for liveness checks on the same key.
Problem
The current design requires ContainsOrAdd + a follow-up Get to handle the race between concurrent loaders, adding complexity:
ok, _ := c.lruCache.ContainsOrAdd(key, v)
if ok {
winner, found := c.lruCache.Get(key)
if !found {
// Winner was evicted between ContainsOrAdd and Get; keep our freshly loaded value
return result{v: v}, nil
}
if c.onEvict != nil {
c.onEvict(key, v)
}
return result{v: winner}, nil
}
This race-handling logic exists solely because multiple goroutines can reach the miss+load path concurrently.
Suggestion
Move the singleflight boundary to wrap the entire Get — both the hit+check and miss+load paths. With only one goroutine running per key at a time:
- The miss path can use a plain
Add instead of ContainsOrAdd — the concurrent-writer race disappears entirely
- The
ContainsOrAdd + Get race-handling block can be removed
- Concurrent liveness checks are coalesced into a single
storage.Load round-trip per key, reducing storage pressure under concurrent access
Acceptance Criteria
References
- PR where the current
ContainsOrAdd pattern was introduced
Context
Raised as a suggestion on the
ValidatingCache.Getimplementation.Currently,
singleflightis used only to deduplicate concurrent cache misses (the load path). The cache hit + liveness check path runs outside the singleflight group, meaning concurrent goroutines can independently triggerstorage.Loadfor liveness checks on the same key.Problem
The current design requires
ContainsOrAdd+ a follow-upGetto handle the race between concurrent loaders, adding complexity:This race-handling logic exists solely because multiple goroutines can reach the miss+load path concurrently.
Suggestion
Move the singleflight boundary to wrap the entire
Get— both the hit+check and miss+load paths. With only one goroutine running per key at a time:Addinstead ofContainsOrAdd— the concurrent-writer race disappears entirelyContainsOrAdd+Getrace-handling block can be removedstorage.Loadround-trip per key, reducing storage pressure under concurrent accessAcceptance Criteria
singleflightwraps the fullGetoperation (hit+check and miss+load)ContainsOrAddis replaced with a plainAddin the miss pathwinner, found := c.lruCache.Get(key)) is removedstorage.LoadcallReferences
ContainsOrAddpattern was introduced