cache.go
⎇
Raw
1package util
2
3import (
4 "sync"
5 "time"
6)
7
8type cacheEntry[V any] struct {
9 value V
10 // expiresAt is the zero time when the cache has no TTL.
11 expiresAt time.Time
12}
13
14// Cache is a bounded, concurrency-safe cache that evicts in insertion order.
15//
16// ponytail: FIFO eviction, swap for LRU if hit rate matters.
17type Cache[K comparable, V any] struct {
18 mu sync.Mutex
19 max int
20 ttl time.Duration
21 items map[K]cacheEntry[V]
22 order []K
23}
24
25// NewCache returns a cache holding at most max entries. A zero ttl means
26// entries never expire.
27func NewCache[K comparable, V any](max int, ttl time.Duration) *Cache[K, V] {
28 return &Cache[K, V]{max: max, ttl: ttl, items: make(map[K]cacheEntry[V], max)}
29}
30
31// Get returns the value for key. Expired entries are dropped and report false.
32func (c *Cache[K, V]) Get(key K) (V, bool) {
33 c.mu.Lock()
34 defer c.mu.Unlock()
35 e, ok := c.items[key]
36 if !ok {
37 return *new(V), false
38 }
39 if !e.expiresAt.IsZero() && !time.Now().Before(e.expiresAt) {
40 c.remove(key)
41 return *new(V), false
42 }
43 return e.value, true
44}
45
46// Set stores a value, evicting the oldest entries when the cache is full.
47func (c *Cache[K, V]) Set(key K, v V) {
48 c.mu.Lock()
49 defer c.mu.Unlock()
50 e := cacheEntry[V]{value: v}
51 if c.ttl > 0 {
52 e.expiresAt = time.Now().Add(c.ttl)
53 }
54 if _, exists := c.items[key]; exists {
55 // Refresh in place: the insertion order does not change.
56 c.items[key] = e
57 return
58 }
59 for len(c.order) >= c.max {
60 delete(c.items, c.order[0])
61 c.order = c.order[1:]
62 }
63 c.items[key] = e
64 c.order = append(c.order, key)
65}
66
67// Delete drops one entry.
68func (c *Cache[K, V]) Delete(key K) {
69 c.mu.Lock()
70 defer c.mu.Unlock()
71 c.remove(key)
72}
73
74// Len reports how many entries are held, expired ones included.
75func (c *Cache[K, V]) Len() int {
76 c.mu.Lock()
77 defer c.mu.Unlock()
78 return len(c.items)
79}
80
81// remove deletes key from both the map and the order. The caller holds the lock.
82func (c *Cache[K, V]) remove(key K) {
83 if _, ok := c.items[key]; !ok {
84 return
85 }
86 delete(c.items, key)
87 for i, k := range c.order {
88 if k == key {
89 c.order = append(c.order[:i], c.order[i+1:]...)
90 return
91 }
92 }
93}
94