Files
core/http/cache/lru.go
2022-08-02 19:10:28 +02:00

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
}