Files
remark42/backend/vendor/github.com/go-pkgz/expirable-cache/v3/cache.go
T
Dmitry VerkhoturovandUmputun 09110c792f Bump backend Go modules to latest
Updates every backend dependency with a newer release available, and
tidies the example module alongside as any change to backend/go.mod
requires.
2026-08-19 03:39:11 -05:00

377 lines
9.9 KiB
Go

// Package cache implements Cache similar to hashicorp/golang-lru
//
// Support LRC, LRU and TTL-based eviction.
// Package is thread-safe and doesn't spawn any goroutines.
// On every Set() call, cache deletes single oldest entry in case it's expired.
// In case MaxSize is set, cache deletes the oldest entry disregarding its expiration date to maintain the size,
// either using LRC or LRU eviction.
// In case of default TTL (10 years) and default MaxSize (0, unlimited) the cache will be truly unlimited
// and will never delete entries from itself automatically.
//
// Get, Peek, Contains and Values treat an expired entry as missing, while Len and Keys report
// everything the cache still holds, expired entries included.
//
// Important: only reliable way of not having expired entries stuck in a cache is to
// run cache.DeleteExpired periodically using time.Ticker, advisable period is 1/2 of TTL.
package cache
import (
"container/list"
"fmt"
"sync"
"time"
)
// Cache defines cache interface
type Cache[K comparable, V any] interface {
fmt.Stringer
options[K, V]
Add(key K, value V) bool
Set(key K, value V, ttl time.Duration)
Get(key K) (V, bool)
GetExpiration(key K) (time.Time, bool)
GetOldest() (K, V, bool)
Contains(key K) (ok bool)
Peek(key K) (V, bool)
Values() []V
Keys() []K
Len() int
Remove(key K) bool
Invalidate(key K)
InvalidateFn(fn func(key K) bool)
RemoveOldest() (K, V, bool)
DeleteExpired()
Purge()
Resize(int) int
Stat() Stats
}
// Stats provides statistics for cache
type Stats struct {
Hits, Misses int // cache effectiveness
Added, Evicted int // number of added and evicted records
}
// cacheImpl provides Cache interface implementation.
type cacheImpl[K comparable, V any] struct {
ttl time.Duration
maxKeys int
isLRU bool
onEvicted func(key K, value V)
sync.Mutex
stat Stats
items map[K]*list.Element
evictList *list.List
}
// noEvictionTTL - very long ttl to prevent eviction
const noEvictionTTL = time.Hour * 24 * 365 * 10
// NewCache returns a new Cache.
// Default MaxKeys is unlimited (0).
// Default TTL is 10 years, sane value for expirable cache is 5 minutes.
// Default eviction mode is LRC, appropriate option allow to change it to LRU.
func NewCache[K comparable, V any]() Cache[K, V] {
return &cacheImpl[K, V]{
items: map[K]*list.Element{},
evictList: list.New(),
ttl: noEvictionTTL,
maxKeys: 0,
}
}
// Add adds a value to the cache. Returns true if an eviction occurred, either
// because the size was exceeded or because the oldest entry expired.
// Returns false if there was no eviction: the item was already in the cache,
// or nothing had to be removed.
func (c *cacheImpl[K, V]) Add(key K, value V) (evicted bool) {
return c.addWithTTL(key, value, c.ttl)
}
// Set key, ttl of 0 would use cache-wide TTL
func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) {
c.addWithTTL(key, value, ttl)
}
// Returns true if an eviction occurred, either because the size was exceeded
// or because the oldest entry expired.
// Returns false if there was no eviction: the item was already in the cache,
// or nothing had to be removed.
func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted bool) {
if ttl == 0 {
ttl = c.ttl
}
now := time.Now()
c.Lock()
defer c.Unlock()
// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
ent.Value.(*cacheItem[K, V]).value = value
ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl)
return false
}
// Add new item
ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)}
entry := c.evictList.PushFront(ent)
c.items[key] = entry
c.stat.Added++
// Remove the oldest entry if it is expired, only in case of non-default TTL.
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
ent := c.evictList.Back()
if ent != nil && now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.removeElement(ent)
evicted = true
}
}
// Verify size not exceeded
if c.maxKeys > 0 && len(c.items) > c.maxKeys {
c.removeOldest()
evicted = true
}
return evicted
}
// Get returns the key value if it's not expired
func (c *cacheImpl[K, V]) Get(key K) (V, bool) {
def := *new(V)
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
// Expired item check
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.stat.Misses++
return ent.Value.(*cacheItem[K, V]).value, false
}
if c.isLRU {
c.evictList.MoveToFront(ent)
}
c.stat.Hits++
return ent.Value.(*cacheItem[K, V]).value, true
}
c.stat.Misses++
return def, false
}
// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale. Expired entries are reported as missing.
func (c *cacheImpl[K, V]) Contains(key K) (ok bool) {
c.Lock()
defer c.Unlock()
ent, ok := c.items[key]
if !ok {
return false
}
return !time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt)
}
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
// Works exactly the same as Get in case of LRC mode (default one).
func (c *cacheImpl[K, V]) Peek(key K) (V, bool) {
def := *new(V)
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
// Expired item check
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.stat.Misses++
return ent.Value.(*cacheItem[K, V]).value, false
}
c.stat.Hits++
return ent.Value.(*cacheItem[K, V]).value, true
}
c.stat.Misses++
return def, false
}
// GetExpiration returns the expiration time of the key. Non-existing key returns zero time.
func (c *cacheImpl[K, V]) GetExpiration(key K) (time.Time, bool) {
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
return ent.Value.(*cacheItem[K, V]).expiresAt, true
}
return time.Time{}, false
}
// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *cacheImpl[K, V]) Keys() []K {
c.Lock()
defer c.Unlock()
return c.keys()
}
// Values returns a slice of the values in the cache, from oldest to newest.
// Expired entries are filtered out.
func (c *cacheImpl[K, V]) Values() []V {
now := time.Now()
c.Lock()
defer c.Unlock()
values := make([]V, 0, len(c.items))
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
if !now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
values = append(values, ent.Value.(*cacheItem[K, V]).value)
}
}
return values
}
// Len return count of items in cache, including expired
func (c *cacheImpl[K, V]) Len() int {
c.Lock()
defer c.Unlock()
return c.evictList.Len()
}
// Resize changes the cache size. Size of 0 means unlimited.
func (c *cacheImpl[K, V]) Resize(size int) int {
c.Lock()
defer c.Unlock()
if size <= 0 {
c.maxKeys = 0
return 0
}
diff := c.evictList.Len() - size
if diff < 0 {
diff = 0
}
for i := 0; i < diff; i++ {
c.removeOldest()
}
c.maxKeys = size
return diff
}
// Invalidate key (item) from the cache
func (c *cacheImpl[K, V]) Invalidate(key K) {
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
}
}
// InvalidateFn deletes multiple keys if predicate is true
func (c *cacheImpl[K, V]) InvalidateFn(fn func(key K) bool) {
c.Lock()
defer c.Unlock()
for key, ent := range c.items {
if fn(key) {
c.removeElement(ent)
}
}
}
// Remove removes the provided key from the cache, returning if the
// key was contained.
func (c *cacheImpl[K, V]) Remove(key K) bool {
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
return true
}
return false
}
// RemoveOldest remove the oldest element in the cache
func (c *cacheImpl[K, V]) RemoveOldest() (key K, value V, ok bool) {
c.Lock()
defer c.Unlock()
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
return ent.Value.(*cacheItem[K, V]).key, ent.Value.(*cacheItem[K, V]).value, true
}
return
}
// GetOldest returns the oldest entry
func (c *cacheImpl[K, V]) GetOldest() (key K, value V, ok bool) {
c.Lock()
defer c.Unlock()
if ent := c.evictList.Back(); ent != nil {
return ent.Value.(*cacheItem[K, V]).key, ent.Value.(*cacheItem[K, V]).value, true
}
return
}
// DeleteExpired clears cache of expired items
func (c *cacheImpl[K, V]) DeleteExpired() {
now := time.Now()
c.Lock()
defer c.Unlock()
var nextEnt *list.Element
for ent := c.evictList.Back(); ent != nil; ent = nextEnt {
nextEnt = ent.Prev()
if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.removeElement(ent)
}
}
}
// Purge clears the cache completely.
func (c *cacheImpl[K, V]) Purge() {
c.Lock()
defer c.Unlock()
for k, v := range c.items {
delete(c.items, k)
c.stat.Evicted++
if c.onEvicted != nil {
c.onEvicted(k, v.Value.(*cacheItem[K, V]).value)
}
}
c.evictList.Init()
}
// Stat gets the current stats for cache
func (c *cacheImpl[K, V]) Stat() Stats {
c.Lock()
defer c.Unlock()
return c.stat
}
func (c *cacheImpl[K, V]) String() string {
stats := c.Stat()
size := c.Len()
return fmt.Sprintf("Size: %d, Stats: %+v (%0.1f%%)", size, stats, 100*float64(stats.Hits)/float64(stats.Hits+stats.Misses))
}
// Keys returns a slice of the keys in the cache, from oldest to newest. Has to be called with lock!
func (c *cacheImpl[K, V]) keys() []K {
keys := make([]K, 0, len(c.items))
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
keys = append(keys, ent.Value.(*cacheItem[K, V]).key)
}
return keys
}
// removeOldest removes the oldest item from the cache. Has to be called with lock!
func (c *cacheImpl[K, V]) removeOldest() {
ent := c.evictList.Back()
if ent != nil {
c.removeElement(ent)
}
}
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
func (c *cacheImpl[K, V]) removeElement(e *list.Element) {
c.evictList.Remove(e)
kv := e.Value.(*cacheItem[K, V])
delete(c.items, kv.key)
c.stat.Evicted++
if c.onEvicted != nil {
c.onEvicted(kv.key, kv.value)
}
}
// cacheItem is used to hold a value in the evictList
type cacheItem[K comparable, V any] struct {
expiresAt time.Time
key K
value V
}