naco-api/internal/server/cache.go
Nacorid 19ae0d781a
fix(server): bound post cache size with earliest-expiry eviction
Cap the post TTL cache at 10000 entries; once full, evict the entry with the
earliest expiry instead of growing without bound.
2026-09-13 22:24:42 +00:00

88 lines
1.7 KiB
Go

package server
import (
"sync"
"time"
)
const postCacheTTL = 5 * time.Minute
const postCacheMaxItems = 10000
type ttlCacheEntry struct {
value any
exp time.Time
}
type ttlCache struct {
mu sync.Mutex
items map[string]ttlCacheEntry
ttl time.Duration
maxItems int
}
func newTTLCache(ttl time.Duration) *ttlCache {
return &ttlCache{
items: make(map[string]ttlCacheEntry),
ttl: ttl,
maxItems: postCacheMaxItems,
}
}
func (c *ttlCache) get(key string) (any, bool) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.items[key]
if !ok {
return nil, false
}
if time.Now().After(e.exp) {
delete(c.items, key)
return nil, false
}
return e.value, true
}
func (c *ttlCache) set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if len(c.items) >= c.maxItems {
// Evict the oldest expired entry, or if none expired, the first one.
var oldestKey string
var oldestTime time.Time
first := true
for k, e := range c.items {
if first || e.exp.Before(oldestTime) {
oldestKey = k
oldestTime = e.exp
first = false
}
}
if oldestKey != "" {
delete(c.items, oldestKey)
}
}
c.items[key] = ttlCacheEntry{value: value, exp: time.Now().Add(c.ttl)}
}
// cachedFetch returns the cached value for key or calls fetch to obtain it,
// storing the result in the cache before returning. The zero value is returned
// on fetch error and not cached.
func cachedFetch[T any](c *ttlCache, key string, fetch func() (T, error)) (T, error) {
if v, ok := c.get(key); ok {
if typed, ok := v.(T); ok {
return typed, nil
}
}
val, err := fetch()
if err != nil {
var zero T
return zero, err
}
c.set(key, val)
return val, nil
}
type latestCacheValue struct {
posts []*GetPostResponsePost
aid string
}