mirror of
https://github.com/rclone/rclone.git
synced 2026-10-09 14:39:28 -04:00
An entry is expired when it hasn't been used for a while, but the user
of the cache may know it is still doing something in the background.
SetCanExpire sets a function which is asked before expiring an entry
and can keep it in the cache until it is ready to go.
The function is called without the cache locked as it may take a
while, so an entry which is used in the meantime isn't expired.
(cherry picked from commit 49d8566ec5)
314 lines
7.8 KiB
Go
314 lines
7.8 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 (
|
|
"strings"
|
|
"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
|
|
finalize func(value any)
|
|
canExpire func(value any) bool // if set, asked before expiring a value
|
|
}
|
|
|
|
// 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,
|
|
finalize: func(_ any) {},
|
|
}
|
|
}
|
|
|
|
// SetExpireDuration sets the interval at which things expire
|
|
//
|
|
// If it is less than or equal to 0 then things are never cached
|
|
func (c *Cache) SetExpireDuration(d time.Duration) *Cache {
|
|
c.expireDuration = d
|
|
return c
|
|
}
|
|
|
|
// returns true if we aren't to cache anything
|
|
func (c *Cache) noCache() bool {
|
|
return c.expireDuration <= 0
|
|
}
|
|
|
|
// SetExpireInterval sets the interval at which the cache expiry runs
|
|
//
|
|
// Set to 0 or a -ve number to disable
|
|
func (c *Cache) SetExpireInterval(d time.Duration) *Cache {
|
|
if d <= 0 {
|
|
d = 100 * 365 * 24 * time.Hour
|
|
}
|
|
c.expireInterval = d
|
|
return c
|
|
}
|
|
|
|
// cacheEntry is stored in the cache
|
|
type cacheEntry struct {
|
|
value any // cached item
|
|
err error // creation error
|
|
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 any, 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 any, err error) {
|
|
c.mu.Lock()
|
|
entry, ok := c.cache[key]
|
|
if !ok {
|
|
c.mu.Unlock() // Unlock in case Get is called recursively
|
|
value, ok, err = create(key)
|
|
if err != nil && !ok {
|
|
return value, err
|
|
}
|
|
entry = &cacheEntry{
|
|
value: value,
|
|
key: key,
|
|
err: err,
|
|
}
|
|
c.mu.Lock()
|
|
if !c.noCache() {
|
|
c.cache[key] = entry
|
|
}
|
|
}
|
|
defer c.mu.Unlock()
|
|
c.used(entry)
|
|
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)
|
|
}
|
|
|
|
// PutErr puts a value named key with err into the cache
|
|
func (c *Cache) PutErr(key string, value any, err error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.noCache() {
|
|
return
|
|
}
|
|
entry := &cacheEntry{
|
|
value: value,
|
|
key: key,
|
|
err: err,
|
|
}
|
|
c.used(entry)
|
|
c.cache[key] = entry
|
|
}
|
|
|
|
// Put puts a value named key into the cache
|
|
func (c *Cache) Put(key string, value any) {
|
|
c.PutErr(key, value, nil)
|
|
}
|
|
|
|
// GetMaybe returns the key and true if found, nil and false if not
|
|
func (c *Cache) GetMaybe(key string) (value any, found bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry, found := c.cache[key]
|
|
if !found {
|
|
return nil, found
|
|
}
|
|
c.used(entry)
|
|
return entry.value, found
|
|
}
|
|
|
|
// Delete the entry passed in
|
|
//
|
|
// Returns true if the entry was found
|
|
func (c *Cache) Delete(key string) bool {
|
|
c.mu.Lock()
|
|
entry, found := c.cache[key]
|
|
if found {
|
|
c.finalize(entry.value)
|
|
}
|
|
delete(c.cache, key)
|
|
c.mu.Unlock()
|
|
return found
|
|
}
|
|
|
|
// DeletePrefix deletes all entries with the given prefix
|
|
//
|
|
// Returns number of entries deleted
|
|
func (c *Cache) DeletePrefix(prefix string) (deleted int) {
|
|
c.mu.Lock()
|
|
for key, entry := range c.cache {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
continue
|
|
}
|
|
c.finalize(entry.value)
|
|
delete(c.cache, key)
|
|
deleted++
|
|
}
|
|
c.mu.Unlock()
|
|
return deleted
|
|
}
|
|
|
|
// 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 any, found bool) {
|
|
c.mu.Lock()
|
|
if newEntry, newFound := c.cache[newKey]; newFound {
|
|
// If new entry is found use that
|
|
if oldEntry, oldFound := c.cache[oldKey]; oldFound {
|
|
// If there's an old entry that is different we must finalize it
|
|
if newEntry.value != oldEntry.value {
|
|
c.finalize(c.cache[oldKey].value)
|
|
}
|
|
}
|
|
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
|
|
// No need to shutdown here, as value lives on under newKey
|
|
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()
|
|
expired := func(entry *cacheEntry) bool {
|
|
return entry.pinCount <= 0 && now.Sub(entry.lastUsed) > c.expireDuration
|
|
}
|
|
candidates := map[string]*cacheEntry{}
|
|
for key, entry := range c.cache {
|
|
if expired(entry) {
|
|
candidates[key] = entry
|
|
}
|
|
}
|
|
if c.canExpire != nil && len(candidates) != 0 {
|
|
// canExpire may block so call it without the lock
|
|
canExpire := c.canExpire
|
|
c.mu.Unlock()
|
|
for key, entry := range candidates {
|
|
if !canExpire(entry.value) {
|
|
delete(candidates, key)
|
|
}
|
|
}
|
|
c.mu.Lock()
|
|
}
|
|
for key, entry := range candidates {
|
|
// The entry may have been used or replaced while the lock was released
|
|
if c.cache[key] == entry && expired(entry) {
|
|
c.finalize(entry.value)
|
|
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 key, entry := range c.cache {
|
|
c.finalize(entry.value)
|
|
delete(c.cache, key)
|
|
}
|
|
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
|
|
}
|
|
|
|
// SetFinalizer sets a function to be called when a value drops out of the cache
|
|
func (c *Cache) SetFinalizer(finalize func(any)) {
|
|
c.mu.Lock()
|
|
c.finalize = finalize
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// SetCanExpire sets a function which is asked whether a value which
|
|
// hasn't been used recently can be expired from the cache. If it
|
|
// returns false the value is kept and asked about again the next time
|
|
// the cache expiry runs.
|
|
//
|
|
// It is called without the cache locked so may use the cache. It
|
|
// isn't called for values removed in other ways, e.g. Delete or Clear.
|
|
func (c *Cache) SetCanExpire(canExpire func(value any) bool) {
|
|
c.mu.Lock()
|
|
c.canExpire = canExpire
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// EntriesWithPinCount returns the number of pinned and unpinned entries in the cache
|
|
//
|
|
// Each entry is counted only once, regardless of entry.pinCount
|
|
func (c *Cache) EntriesWithPinCount() (pinned, unpinned int) {
|
|
c.mu.Lock()
|
|
for _, entry := range c.cache {
|
|
if entry.pinCount <= 0 {
|
|
unpinned++
|
|
} else {
|
|
pinned++
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
return pinned, unpinned
|
|
}
|