Files
2026-07-18 23:28:51 +02:00

88 lines
2.2 KiB
Go

// Package cache provides a small, concurrency-safe TTL + LRU cache. It's a
// generic building block for sharing an expensive computation across requests
// (e.g. the reader shares one relevance match between /api/stream and
// /api/facets): a hit returns the stored value, a miss recomputes. The TTL
// bounds staleness; the capacity bounds memory.
package cache
import (
"container/list"
"sync"
"time"
)
type entry[K comparable, V any] struct {
key K
val V
at time.Time
}
// TTLLRU is a fixed-capacity cache that evicts the least-recently-used entry on
// overflow and treats entries older than ttl as misses. The zero value is not
// usable — construct with New. All methods are safe for concurrent use.
type TTLLRU[K comparable, V any] struct {
mu sync.Mutex
ll *list.List // front = most-recently-used
m map[K]*list.Element
cap int
ttl time.Duration
}
// New returns a cache holding up to capacity entries, each valid for ttl.
func New[K comparable, V any](capacity int, ttl time.Duration) *TTLLRU[K, V] {
if capacity < 1 {
capacity = 1
}
return &TTLLRU[K, V]{
ll: list.New(),
m: make(map[K]*list.Element, capacity),
cap: capacity,
ttl: ttl,
}
}
// Get returns the cached value for key. The boolean is false on a miss or when
// the entry has expired (in which case it is also evicted). A hit refreshes the
// entry's recency.
func (c *TTLLRU[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el, ok := c.m[key]
if !ok {
var zero V
return zero, false
}
ent := el.Value.(*entry[K, V])
if time.Since(ent.at) > c.ttl {
c.ll.Remove(el)
delete(c.m, key)
var zero V
return zero, false
}
c.ll.MoveToFront(el)
return ent.val, true
}
// Put stores val under key, refreshing its timestamp and recency, evicting the
// least-recently-used entry if that exceeds the capacity.
func (c *TTLLRU[K, V]) Put(key K, val V) {
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.m[key]; ok {
ent := el.Value.(*entry[K, V])
ent.val = val
ent.at = time.Now()
c.ll.MoveToFront(el)
return
}
c.m[key] = c.ll.PushFront(&entry[K, V]{key: key, val: val, at: time.Now()})
for c.ll.Len() > c.cap {
back := c.ll.Back()
if back == nil {
break
}
c.ll.Remove(back)
delete(c.m, back.Value.(*entry[K, V]).key)
}
}