~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
294 lines · 5.5 KB Raw History
package ttlcache

import (
	"container/list"
	"context"
	"strconv"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"
)

type Options struct {
	TTL        time.Duration
	MaxEntries int
	Now        func() time.Time
}

type Cache[K comparable, V any] struct {
	ttl       time.Duration
	max       int
	now       func() time.Time
	mu        sync.Mutex
	order     *list.List
	items     map[K]*list.Element
	flights   map[K]*flight
	seq       uint64
	nextPrune int
	onEvict   func(K, V)
	group     singleflight.Group
}

type entry[K comparable, V any] struct {
	key     K
	value   V
	expires time.Time
}

type flight struct {
	id    string
	stale bool
}

const minPrune = 64

func New[K comparable, V any](opts Options) *Cache[K, V] {
	if opts.Now == nil {
		opts.Now = time.Now
	}
	return &Cache[K, V]{
		ttl:       opts.TTL,
		max:       opts.MaxEntries,
		now:       opts.Now,
		order:     list.New(),
		items:     make(map[K]*list.Element),
		flights:   make(map[K]*flight),
		nextPrune: minPrune,
	}
}

func (c *Cache[K, V]) OnEvict(f func(K, V)) *Cache[K, V] {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.onEvict = f
	return c
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.getLocked(key)
}

func (c *Cache[K, V]) Set(key K, value V) {
	c.SetTTL(key, value, 0)
}

func (c *Cache[K, V]) SetTTL(key K, value V, ttl time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.setLocked(key, value, ttl)
}

func (c *Cache[K, V]) Add(key K, value V, ttl time.Duration) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if _, ok := c.getLocked(key); ok {
		return false
	}
	c.setLocked(key, value, ttl)
	return true
}

func (c *Cache[K, V]) Delete(key K) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		c.removeLocked(el)
	}
	if f, ok := c.flights[key]; ok {
		f.stale = true
		delete(c.flights, key)
	}
}

func (c *Cache[K, V]) DeleteFunc(drop func(K, V) bool) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	dropped := 0
	for el := c.order.Front(); el != nil; {
		next := el.Next()
		e := el.Value.(*entry[K, V])
		if drop(e.key, e.value) {
			c.removeLocked(el)
			dropped++
		}
		el = next
	}
	return dropped
}

func (c *Cache[K, V]) DeleteKeys(drop func(K) bool) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	dropped := 0
	for key, f := range c.flights {
		if drop(key) {
			f.stale = true
			delete(c.flights, key)
			dropped++
		}
	}
	for el := c.order.Front(); el != nil; {
		next := el.Next()
		if drop(el.Value.(*entry[K, V]).key) {
			c.removeLocked(el)
			dropped++
		}
		el = next
	}
	return dropped
}

func (c *Cache[K, V]) Range(visit func(K, V) bool) {
	c.mu.Lock()
	now := c.now()
	keys := make([]K, 0, len(c.items))
	values := make([]V, 0, len(c.items))
	for el := c.order.Front(); el != nil; el = el.Next() {
		e := el.Value.(*entry[K, V])
		if e.expired(now) {
			continue
		}
		keys = append(keys, e.key)
		values = append(values, e.value)
	}
	c.mu.Unlock()
	for i := range keys {
		if !visit(keys[i], values[i]) {
			return
		}
	}
}

func (c *Cache[K, V]) Len() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.pruneLocked()
	return len(c.items)
}

func (c *Cache[K, V]) Prune() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.pruneLocked()
}

func (c *Cache[K, V]) Loading() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return len(c.flights)
}

func (c *Cache[K, V]) GetOrLoad(ctx context.Context, key K, load func(context.Context) (V, time.Duration, error)) (V, error) {
	c.mu.Lock()
	if value, ok := c.getLocked(key); ok {
		c.mu.Unlock()
		return value, nil
	}
	f, ok := c.flights[key]
	if !ok {
		c.seq++
		f = &flight{id: strconv.FormatUint(c.seq, 36)}
		c.flights[key] = f
	}
	c.mu.Unlock()
	ch := c.group.DoChan(f.id, func() (any, error) {
		value, ttl, err := load(ctx)
		c.mu.Lock()
		defer c.mu.Unlock()
		if c.flights[key] == f {
			delete(c.flights, key)
		}
		if err == nil && !f.stale {
			c.setLocked(key, value, ttl)
		}
		return value, err
	})
	select {
	case res := <-ch:
		value, _ := res.Val.(V)
		return value, res.Err
	case <-ctx.Done():
		var zero V
		return zero, ctx.Err()
	}
}

func (c *Cache[K, V]) getLocked(key K) (V, bool) {
	el, ok := c.items[key]
	if !ok {
		var zero V
		return zero, false
	}
	e := el.Value.(*entry[K, V])
	if e.expired(c.now()) {
		c.removeLocked(el)
		var zero V
		return zero, false
	}
	c.order.MoveToFront(el)
	return e.value, true
}

func (c *Cache[K, V]) setLocked(key K, value V, ttl time.Duration) {
	if ttl == 0 {
		ttl = c.ttl
	}
	if ttl < 0 {
		if el, ok := c.items[key]; ok {
			c.removeLocked(el)
		}
		return
	}
	var expires time.Time
	if ttl > 0 {
		expires = c.now().Add(ttl)
	}
	if el, ok := c.items[key]; ok {
		e := el.Value.(*entry[K, V])
		if c.onEvict != nil {
			c.onEvict(e.key, e.value)
		}
		e.value = value
		e.expires = expires
		c.order.MoveToFront(el)
		return
	}
	c.items[key] = c.order.PushFront(&entry[K, V]{key: key, value: value, expires: expires})
	if len(c.items) >= c.nextPrune {
		c.pruneLocked()
		c.nextPrune = max(2*len(c.items), minPrune)
	}
	for c.max > 0 && len(c.items) > c.max {
		c.removeLocked(c.order.Back())
	}
}

func (c *Cache[K, V]) pruneLocked() int {
	now := c.now()
	dropped := 0
	for el := c.order.Front(); el != nil; {
		next := el.Next()
		if el.Value.(*entry[K, V]).expired(now) {
			c.removeLocked(el)
			dropped++
		}
		el = next
	}
	return dropped
}

func (c *Cache[K, V]) removeLocked(el *list.Element) {
	e := el.Value.(*entry[K, V])
	delete(c.items, e.key)
	c.order.Remove(el)
	if c.onEvict != nil {
		c.onEvict(e.key, e.value)
	}
}

func (e *entry[K, V]) expired(now time.Time) bool {
	return !e.expires.IsZero() && !now.Before(e.expires)
}