storj/pkg/cache/cache.go

128 lines
2.8 KiB
Go
Raw Normal View History

// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package cache
import (
"container/list"
"sync"
"time"
)
// Options controls the details of the expiration policy.
type Options struct {
// Expiration is how long an entry will be valid. It is not
// affected by LRU or anything: after this duration, the object
// is invalidated. A non-positive value means no expiration.
Expiration time.Duration
// Capacity is how many objects to keep in memory.
Capacity int
}
// cacheState contains all of the state for a cached entry.
type cacheState struct {
once sync.Once
when time.Time
order *list.Element
value interface{}
}
// ExpiringLRU caches values for string keys with a time based expiration and
// an LRU based eviciton policy.
type ExpiringLRU struct {
mu sync.Mutex
opts Options
data map[string]*cacheState
order *list.List
}
// New constructs an ExpiringLRU with the given options.
func New(opts Options) *ExpiringLRU {
return &ExpiringLRU{
opts: opts,
data: make(map[string]*cacheState, opts.Capacity),
order: list.New(),
}
}
// Get returns the value for some key if it exists and is valid. If not
// it will call the provided function. Concurrent calls will dedupe as
// best as they are able. If the function returns an error, it is not
// cached and further calls will try again.
func (e *ExpiringLRU) Get(key string, fn func() (interface{}, error)) (
value interface{}, err error) {
if e.opts.Capacity <= 0 {
return fn()
}
for {
e.mu.Lock()
state, ok := e.data[key]
switch {
case !ok:
for len(e.data) >= e.opts.Capacity {
back := e.order.Back()
delete(e.data, back.Value.(string))
e.order.Remove(back)
}
state = &cacheState{
when: time.Now(),
order: e.order.PushFront(key),
}
e.data[key] = state
case e.opts.Expiration > 0 && time.Since(state.when) > e.opts.Expiration:
delete(e.data, key)
e.order.Remove(state.order)
e.mu.Unlock()
continue
default:
e.order.MoveToFront(state.order)
}
e.mu.Unlock()
called := false
state.once.Do(func() {
called = true
value, err = fn()
if err == nil {
// careful because we don't want a `(*T)(nil) != nil` situation
// that's why we only assign to state.value if err == nil.
state.value = value
} else {
// the once has been used. delete it so that any other waiters
// will retry.
e.mu.Lock()
if e.data[key] == state {
delete(e.data, key)
e.order.Remove(state.order)
}
e.mu.Unlock()
}
})
if called || state.value != nil {
return state.value, err
}
}
}
// Delete explicitly removes a key from the cache if it exists.
func (e *ExpiringLRU) Delete(key string) {
e.mu.Lock()
defer e.mu.Unlock()
state, ok := e.data[key]
if !ok {
return
}
delete(e.data, key)
e.order.Remove(state.order)
}