# UNDF: UNDF-2026-000001087 --- a/cache/ttl_cache.go +++ b/cache/ttl_cache.go @@ -14,6 +14,7 @@ import ( "context" "fmt" "slices" + "sync" "sync/atomic" "time" @@ -25,6 +26,7 @@ type TTLCache[K comparable, V any] struct { mx sync.RWMutex cache map[K]cacheEntry[V] purgeStop chan struct{} + group singleflightGroup[K, V] getter Getter[K, V] maxAge time.Duration countHit atomic.Int64 @@ -200,13 +202,27 @@ func (c *TTLCache[K, V]) Get(ctx context.Context, key K) (V, error) { item, ok := c.fetch(key, now) if ok { return item, nil } - item, err := c.getter.Find(ctx, key) + // Use singleflight to prevent thundering herd: concurrent goroutines that + // miss the cache for the same key share a single backend fetch instead of + // each issuing their own database call. + item, err, _ := c.group.Do(key, func() (V, error) { + return c.getter.Find(ctx, key) + }) if err != nil { return nothing, fmt.Errorf("cache: failed to find one: %w", err) } c.mx.Lock() c.cache[key] = cacheEntry[V]{ added: now, data: item, } c.mx.Unlock() return item, nil } +// singleflightGroup is a minimal generic singleflight implementation. +type singleflightGroup[K comparable, V any] struct { + mu sync.Mutex + m map[K]*call[V] +} + +type call[V any] struct { + wg sync.WaitGroup + val V + err error +} + +func (g *singleflightGroup[K, V]) Do(key K, fn func() (V, error)) (V, error, bool) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[K]*call[V]) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err, true + } + c := &call[V]{} + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err, false +}