mirror of
				https://github.com/datarhei/core.git
				synced 2025-10-31 19:32:56 +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
 | |
| }
 | 
