88 lines
2.2 KiB
Go
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)
|
|
}
|
|
}
|