Before this fix, if several Get requests were submitted very quickly, this could run the item create function multiple times due to the unlock of the mutex in the creation code. This fixes the problem by having a mutex in each cache entry which is held when the item is being created.
184 lines
4.6 KiB
Go
184 lines
4.6 KiB
Go
// Package cache implements a simple cache where the entries are
|
|
// expired after a given time (5 minutes of disuse by default).
|
|
package cache
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Cache holds values indexed by string, but expired after a given (5
|
|
// minutes by default).
|
|
type Cache struct {
|
|
mu sync.Mutex
|
|
cache map[string]*cacheEntry
|
|
expireRunning bool
|
|
expireDuration time.Duration // expire the cache entry when it is older than this
|
|
expireInterval time.Duration // interval to run the cache expire
|
|
}
|
|
|
|
// New creates a new cache with the default expire duration and interval
|
|
func New() *Cache {
|
|
return &Cache{
|
|
cache: map[string]*cacheEntry{},
|
|
expireRunning: false,
|
|
expireDuration: 300 * time.Second,
|
|
expireInterval: 60 * time.Second,
|
|
}
|
|
}
|
|
|
|
// cacheEntry is stored in the cache
|
|
type cacheEntry struct {
|
|
createMu sync.Mutex // held while creating the item
|
|
value interface{} // cached item
|
|
err error // creation error
|
|
ok bool // true if entry is valid
|
|
key string // key
|
|
lastUsed time.Time // time used for expiry
|
|
pinCount int // non zero if the entry should not be removed
|
|
}
|
|
|
|
// CreateFunc is called to create new values. If the create function
|
|
// returns an error it will be cached if ok is true, otherwise the
|
|
// error will just be returned, allowing negative caching if required.
|
|
type CreateFunc func(key string) (value interface{}, ok bool, error error)
|
|
|
|
// used marks an entry as accessed now and kicks the expire timer off
|
|
// should be called with the lock held
|
|
func (c *Cache) used(entry *cacheEntry) {
|
|
entry.lastUsed = time.Now()
|
|
if !c.expireRunning {
|
|
time.AfterFunc(c.expireInterval, c.cacheExpire)
|
|
c.expireRunning = true
|
|
}
|
|
}
|
|
|
|
// Get gets a value named key either from the cache or creates it
|
|
// afresh with the create function.
|
|
func (c *Cache) Get(key string, create CreateFunc) (value interface{}, err error) {
|
|
c.mu.Lock()
|
|
entry, found := c.cache[key]
|
|
if !found {
|
|
entry = &cacheEntry{
|
|
key: key,
|
|
}
|
|
c.cache[key] = entry
|
|
}
|
|
c.mu.Unlock()
|
|
// Only one racing Get will have found=false here
|
|
entry.createMu.Lock()
|
|
if !found {
|
|
entry.value, entry.ok, entry.err = create(key)
|
|
}
|
|
entry.createMu.Unlock()
|
|
c.mu.Lock()
|
|
if !found && !entry.ok {
|
|
delete(c.cache, key)
|
|
} else {
|
|
c.used(entry)
|
|
}
|
|
c.mu.Unlock()
|
|
return entry.value, entry.err
|
|
}
|
|
|
|
func (c *Cache) addPin(key string, count int) {
|
|
c.mu.Lock()
|
|
entry, ok := c.cache[key]
|
|
if ok {
|
|
entry.pinCount += count
|
|
c.used(entry)
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Pin a value in the cache if it exists
|
|
func (c *Cache) Pin(key string) {
|
|
c.addPin(key, 1)
|
|
}
|
|
|
|
// Unpin a value in the cache if it exists
|
|
func (c *Cache) Unpin(key string) {
|
|
c.addPin(key, -1)
|
|
}
|
|
|
|
// Put puts a value named key into the cache
|
|
func (c *Cache) Put(key string, value interface{}) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry := &cacheEntry{
|
|
value: value,
|
|
key: key,
|
|
ok: true,
|
|
}
|
|
c.used(entry)
|
|
c.cache[key] = entry
|
|
}
|
|
|
|
// GetMaybe returns the key and true if found, nil and false if not
|
|
func (c *Cache) GetMaybe(key string) (value interface{}, found bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry, found := c.cache[key]
|
|
if !found || !entry.ok {
|
|
return nil, found
|
|
}
|
|
c.used(entry)
|
|
return entry.value, found
|
|
}
|
|
|
|
// Rename renames the item at oldKey to newKey.
|
|
//
|
|
// If there was an existing item at newKey then it takes precedence
|
|
// and is returned otherwise the item (if any) at oldKey is returned.
|
|
func (c *Cache) Rename(oldKey, newKey string) (value interface{}, found bool) {
|
|
c.mu.Lock()
|
|
if newEntry, newFound := c.cache[newKey]; newFound {
|
|
// If new entry is found use that
|
|
delete(c.cache, oldKey)
|
|
value, found = newEntry.value, newFound
|
|
c.used(newEntry)
|
|
} else if oldEntry, oldFound := c.cache[oldKey]; oldFound {
|
|
// If old entry is found rename it to new and use that
|
|
c.cache[newKey] = oldEntry
|
|
delete(c.cache, oldKey)
|
|
c.used(oldEntry)
|
|
value, found = oldEntry.value, oldFound
|
|
}
|
|
c.mu.Unlock()
|
|
return value, found
|
|
}
|
|
|
|
// cacheExpire expires any entries that haven't been used recently
|
|
func (c *Cache) cacheExpire() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
now := time.Now()
|
|
for key, entry := range c.cache {
|
|
if entry.pinCount <= 0 && now.Sub(entry.lastUsed) > c.expireDuration {
|
|
delete(c.cache, key)
|
|
}
|
|
}
|
|
if len(c.cache) != 0 {
|
|
time.AfterFunc(c.expireInterval, c.cacheExpire)
|
|
c.expireRunning = true
|
|
} else {
|
|
c.expireRunning = false
|
|
}
|
|
}
|
|
|
|
// Clear removes everything from the cache
|
|
func (c *Cache) Clear() {
|
|
c.mu.Lock()
|
|
for k := range c.cache {
|
|
delete(c.cache, k)
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Entries returns the number of entries in the cache
|
|
func (c *Cache) Entries() int {
|
|
c.mu.Lock()
|
|
entries := len(c.cache)
|
|
c.mu.Unlock()
|
|
return entries
|
|
}
|