mirror of
https://github.com/kemko/reproxy.git
synced 2026-01-01 15:55:49 +03:00
273 lines
6.8 KiB
Go
273 lines
6.8 KiB
Go
// Package cache implements Cache similar to hashicorp/golang-lru
|
|
//
|
|
// Support LRC, LRU and TTL-based eviction.
|
|
// Package is thread-safe and doesn't spawn any goroutines.
|
|
// On every Set() call, cache deletes single oldest entry in case it's expired.
|
|
// In case MaxSize is set, cache deletes the oldest entry disregarding its expiration date to maintain the size,
|
|
// either using LRC or LRU eviction.
|
|
// In case of default TTL (10 years) and default MaxSize (0, unlimited) the cache will be truly unlimited
|
|
// and will never delete entries from itself automatically.
|
|
//
|
|
// Important: only reliable way of not having expired entries stuck in a cache is to
|
|
// run cache.DeleteExpired periodically using time.Ticker, advisable period is 1/2 of TTL.
|
|
package cache
|
|
|
|
import (
|
|
"container/list"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Cache defines cache interface
|
|
type Cache interface {
|
|
fmt.Stringer
|
|
Set(key string, value interface{}, ttl time.Duration)
|
|
Get(key string) (interface{}, bool)
|
|
Peek(key string) (interface{}, bool)
|
|
Keys() []string
|
|
Len() int
|
|
Invalidate(key string)
|
|
InvalidateFn(fn func(key string) bool)
|
|
RemoveOldest()
|
|
DeleteExpired()
|
|
Purge()
|
|
Stat() Stats
|
|
}
|
|
|
|
// Stats provides statistics for cache
|
|
type Stats struct {
|
|
Hits, Misses int // cache effectiveness
|
|
Added, Evicted int // number of added and evicted records
|
|
}
|
|
|
|
// cacheImpl provides Cache interface implementation.
|
|
type cacheImpl struct {
|
|
ttl time.Duration
|
|
maxKeys int
|
|
isLRU bool
|
|
onEvicted func(key string, value interface{})
|
|
|
|
sync.Mutex
|
|
stat Stats
|
|
items map[string]*list.Element
|
|
evictList *list.List
|
|
}
|
|
|
|
// noEvictionTTL - very long ttl to prevent eviction
|
|
const noEvictionTTL = time.Hour * 24 * 365 * 10
|
|
|
|
// NewCache returns a new Cache.
|
|
// Default MaxKeys is unlimited (0).
|
|
// Default TTL is 10 years, sane value for expirable cache is 5 minutes.
|
|
// Default eviction mode is LRC, appropriate option allow to change it to LRU.
|
|
func NewCache(options ...Option) (Cache, error) {
|
|
res := cacheImpl{
|
|
items: map[string]*list.Element{},
|
|
evictList: list.New(),
|
|
ttl: noEvictionTTL,
|
|
maxKeys: 0,
|
|
}
|
|
|
|
for _, opt := range options {
|
|
if err := opt(&res); err != nil {
|
|
return nil, fmt.Errorf("failed to set cache option: %w", err)
|
|
}
|
|
}
|
|
return &res, nil
|
|
}
|
|
|
|
// Set key, ttl of 0 would use cache-wide TTL
|
|
func (c *cacheImpl) Set(key string, value interface{}, ttl time.Duration) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
now := time.Now()
|
|
if ttl == 0 {
|
|
ttl = c.ttl
|
|
}
|
|
|
|
// Check for existing item
|
|
if ent, ok := c.items[key]; ok {
|
|
c.evictList.MoveToFront(ent)
|
|
ent.Value.(*cacheItem).value = value
|
|
ent.Value.(*cacheItem).expiresAt = now.Add(ttl)
|
|
return
|
|
}
|
|
|
|
// Add new item
|
|
ent := &cacheItem{key: key, value: value, expiresAt: now.Add(ttl)}
|
|
entry := c.evictList.PushFront(ent)
|
|
c.items[key] = entry
|
|
c.stat.Added++
|
|
|
|
// Remove oldest entry if it is expired, only in case of non-default TTL.
|
|
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
|
|
c.removeOldestIfExpired()
|
|
}
|
|
|
|
// Verify size not exceeded
|
|
if c.maxKeys > 0 && len(c.items) > c.maxKeys {
|
|
c.removeOldest()
|
|
}
|
|
}
|
|
|
|
// Get returns the key value if it's not expired
|
|
func (c *cacheImpl) Get(key string) (interface{}, bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
// Expired item check
|
|
if time.Now().After(ent.Value.(*cacheItem).expiresAt) {
|
|
c.stat.Misses++
|
|
return nil, false
|
|
}
|
|
if c.isLRU {
|
|
c.evictList.MoveToFront(ent)
|
|
}
|
|
c.stat.Hits++
|
|
return ent.Value.(*cacheItem).value, true
|
|
}
|
|
c.stat.Misses++
|
|
return nil, false
|
|
}
|
|
|
|
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
|
// Works exactly the same as Get in case of LRC mode (default one).
|
|
func (c *cacheImpl) Peek(key string) (interface{}, bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
// Expired item check
|
|
if time.Now().After(ent.Value.(*cacheItem).expiresAt) {
|
|
c.stat.Misses++
|
|
return nil, false
|
|
}
|
|
c.stat.Hits++
|
|
return ent.Value.(*cacheItem).value, true
|
|
}
|
|
c.stat.Misses++
|
|
return nil, false
|
|
}
|
|
|
|
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
func (c *cacheImpl) Keys() []string {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.keys()
|
|
}
|
|
|
|
// Len return count of items in cache, including expired
|
|
func (c *cacheImpl) Len() int {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.evictList.Len()
|
|
}
|
|
|
|
// Invalidate key (item) from the cache
|
|
func (c *cacheImpl) Invalidate(key string) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
c.removeElement(ent)
|
|
}
|
|
}
|
|
|
|
// InvalidateFn deletes multiple keys if predicate is true
|
|
func (c *cacheImpl) InvalidateFn(fn func(key string) bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for key, ent := range c.items {
|
|
if fn(key) {
|
|
c.removeElement(ent)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RemoveOldest remove oldest element in the cache
|
|
func (c *cacheImpl) RemoveOldest() {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.removeOldest()
|
|
}
|
|
|
|
// DeleteExpired clears cache of expired items
|
|
func (c *cacheImpl) DeleteExpired() {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for _, key := range c.keys() {
|
|
if time.Now().After(c.items[key].Value.(*cacheItem).expiresAt) {
|
|
c.removeElement(c.items[key])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Purge clears the cache completely.
|
|
func (c *cacheImpl) Purge() {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for k, v := range c.items {
|
|
delete(c.items, k)
|
|
c.stat.Evicted++
|
|
if c.onEvicted != nil {
|
|
c.onEvicted(k, v.Value.(*cacheItem).value)
|
|
}
|
|
}
|
|
c.evictList.Init()
|
|
}
|
|
|
|
// Stat gets the current stats for cache
|
|
func (c *cacheImpl) Stat() Stats {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.stat
|
|
}
|
|
|
|
func (c *cacheImpl) String() string {
|
|
stats := c.Stat()
|
|
size := c.Len()
|
|
return fmt.Sprintf("Size: %d, Stats: %+v (%0.1f%%)", size, stats, 100*float64(stats.Hits)/float64(stats.Hits+stats.Misses))
|
|
}
|
|
|
|
// Keys returns a slice of the keys in the cache, from oldest to newest. Has to be called with lock!
|
|
func (c *cacheImpl) keys() []string {
|
|
keys := make([]string, 0, len(c.items))
|
|
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
|
keys = append(keys, ent.Value.(*cacheItem).key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// removeOldest removes the oldest item from the cache. Has to be called with lock!
|
|
func (c *cacheImpl) removeOldest() {
|
|
ent := c.evictList.Back()
|
|
if ent != nil {
|
|
c.removeElement(ent)
|
|
}
|
|
}
|
|
|
|
// removeOldest removes the oldest item from the cache in case it's already expired. Has to be called with lock!
|
|
func (c *cacheImpl) removeOldestIfExpired() {
|
|
ent := c.evictList.Back()
|
|
if ent != nil && time.Now().After(ent.Value.(*cacheItem).expiresAt) {
|
|
c.removeElement(ent)
|
|
}
|
|
}
|
|
|
|
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
|
|
func (c *cacheImpl) removeElement(e *list.Element) {
|
|
c.evictList.Remove(e)
|
|
kv := e.Value.(*cacheItem)
|
|
delete(c.items, kv.key)
|
|
c.stat.Evicted++
|
|
if c.onEvicted != nil {
|
|
c.onEvicted(kv.key, kv.value)
|
|
}
|
|
}
|
|
|
|
// cacheItem is used to hold a value in the evictList
|
|
type cacheItem struct {
|
|
expiresAt time.Time
|
|
key string
|
|
value interface{}
|
|
}
|