mirror of
https://github.com/datarhei/core.git
synced 2025-09-26 20:11:29 +08:00
252 lines
5.6 KiB
Go
252 lines
5.6 KiB
Go
package cache
|
|
|
|
import (
|
|
"container/list"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/datarhei/core/v16/log"
|
|
)
|
|
|
|
// LRUConfig is the configuration for a new LRU cache
|
|
type LRUConfig struct {
|
|
TTL time.Duration // For how long the object should stay in cache
|
|
MaxSize uint64 // Max. size of the cache, 0 for unlimited, bytes
|
|
MaxFileSize uint64 // Max. file size allowed to put in cache, 0 for unlimited, bytes
|
|
AllowExtensions []string // List of file extension allowed to cache, empty list for all files
|
|
BlockExtensions []string // List of file extensions not allowed to cache, empty list for none
|
|
Logger log.Logger
|
|
}
|
|
|
|
type lrucache struct {
|
|
ttl time.Duration
|
|
maxSize uint64
|
|
maxFileSize uint64
|
|
allowExtensions []string
|
|
blockExtensions []string
|
|
objects map[string]*list.Element
|
|
list *list.List
|
|
size uint64
|
|
lock sync.Mutex
|
|
logger log.Logger
|
|
}
|
|
|
|
type value struct {
|
|
key string
|
|
obj interface{}
|
|
expireAt time.Time
|
|
size uint64
|
|
}
|
|
|
|
// NewLRUCache returns an implementation of the Cacher interface that implements a LRU cache.
|
|
func NewLRUCache(config LRUConfig) (Cacher, error) {
|
|
if config.MaxSize != 0 && config.MaxFileSize > config.MaxSize {
|
|
return nil, fmt.Errorf("the max cache size has to be bigger than the max file size")
|
|
}
|
|
|
|
cache := &lrucache{
|
|
ttl: config.TTL,
|
|
maxSize: config.MaxSize,
|
|
maxFileSize: config.MaxFileSize,
|
|
list: list.New(),
|
|
objects: make(map[string]*list.Element),
|
|
logger: config.Logger,
|
|
}
|
|
|
|
if cache.logger == nil {
|
|
cache.logger = log.New("")
|
|
}
|
|
|
|
cache.allowExtensions = make([]string, len(config.AllowExtensions))
|
|
copy(cache.allowExtensions, config.AllowExtensions)
|
|
|
|
cache.blockExtensions = make([]string, len(config.BlockExtensions))
|
|
copy(cache.blockExtensions, config.BlockExtensions)
|
|
|
|
return cache, nil
|
|
}
|
|
|
|
// createValue create a value type from a key and object. This will be used as
|
|
// value for list.Element.
|
|
func (c *lrucache) createValue(key string, o interface{}, expireAt time.Time, size uint64) *value {
|
|
v := &value{
|
|
key: key,
|
|
obj: o,
|
|
expireAt: expireAt,
|
|
size: size,
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func (c *lrucache) Get(key string) (interface{}, time.Duration, error) {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// Check if the object exists
|
|
elm, ok := c.objects[key]
|
|
if !ok {
|
|
return nil, c.ttl, nil
|
|
}
|
|
|
|
// Move it to the front of the list
|
|
c.list.MoveToFront(elm)
|
|
|
|
// Calculate the expiry date
|
|
expire := elm.Value.(*value).expireAt
|
|
|
|
// If it is expired, remove it from the list
|
|
// and return as if it wasn't in the cache
|
|
if expire.Before(time.Now()) {
|
|
c.removeElement(elm)
|
|
delete(c.objects, key)
|
|
|
|
return nil, c.ttl, nil
|
|
}
|
|
|
|
// Get the actual cached object
|
|
o := elm.Value.(*value).obj
|
|
|
|
return o, time.Until(expire), nil
|
|
}
|
|
|
|
func (c *lrucache) Put(key string, o interface{}, size uint64) error {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// Check if the object fits the cache
|
|
if !c.IsSizeCacheable(size) {
|
|
return fmt.Errorf("object too big to cache")
|
|
}
|
|
|
|
expireAt := time.Now().Add(c.ttl)
|
|
|
|
// Check if we already have an object with this key in order
|
|
// to replace it. Otherwise, create a new object.
|
|
if elm, ok := c.objects[key]; ok {
|
|
c.list.MoveToFront(elm)
|
|
|
|
c.size -= elm.Value.(*value).size
|
|
elm.Value = c.createValue(key, o, expireAt, size)
|
|
c.size += elm.Value.(*value).size
|
|
} else {
|
|
elm = c.list.PushFront(c.createValue(key, o, expireAt, size))
|
|
|
|
c.objects[key] = elm
|
|
c.size += elm.Value.(*value).size
|
|
}
|
|
|
|
c.logger.WithFields(log.Fields{
|
|
"key": key,
|
|
"size_bytes": size,
|
|
}).Debug().Log("Added key")
|
|
|
|
// If the size of the cache is exceeded, remove all least used
|
|
// objects from the cache until the cache size is in its limits.
|
|
if c.maxSize > 0 {
|
|
for c.size > c.maxSize {
|
|
elm := c.list.Back()
|
|
if elm == nil {
|
|
break
|
|
}
|
|
|
|
key := elm.Value.(*value).key
|
|
|
|
c.logger.WithFields(log.Fields{
|
|
"key": key,
|
|
"size_bytes": elm.Value.(*value).size,
|
|
}).Debug().Log("Evicting key")
|
|
|
|
c.removeElement(elm)
|
|
delete(c.objects, key)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *lrucache) Delete(key string) error {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
// Check if the object is in the cache. If not, do nothing.
|
|
elm, ok := c.objects[key]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
c.logger.WithFields(log.Fields{
|
|
"key": key,
|
|
"size_bytes": elm.Value.(*value).size,
|
|
}).Debug().Log("Purging key")
|
|
|
|
c.removeElement(elm)
|
|
delete(c.objects, key)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *lrucache) Purge() {
|
|
c.lock.Lock()
|
|
defer c.lock.Unlock()
|
|
|
|
c.list.Init()
|
|
c.objects = make(map[string]*list.Element)
|
|
|
|
c.logger.WithField("size_bytes", c.size).Debug().Log("Purged all keys")
|
|
|
|
c.size = 0
|
|
}
|
|
|
|
func (c *lrucache) TTL() time.Duration {
|
|
return c.ttl
|
|
}
|
|
|
|
func (c *lrucache) IsExtensionCacheable(extension string) bool {
|
|
if len(c.allowExtensions) == 0 && len(c.blockExtensions) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, e := range c.blockExtensions {
|
|
if extension == e {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if len(c.allowExtensions) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, e := range c.allowExtensions {
|
|
if extension == e {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *lrucache) IsSizeCacheable(size uint64) bool {
|
|
// If the cache has a maximum size, the object can't be bigger than this size
|
|
if c.maxSize != 0 && size > c.maxSize {
|
|
return false
|
|
}
|
|
|
|
// If the cache has an object size limit, the object can't be bigger than this size
|
|
if c.maxFileSize != 0 && size > c.maxFileSize {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// removeElement removes an element from the list.
|
|
func (c *lrucache) removeElement(elm *list.Element) {
|
|
c.list.Remove(elm)
|
|
|
|
v := elm.Value.(*value)
|
|
|
|
c.size -= v.size
|
|
}
|