mirror of
https://github.com/kemko/reproxy.git
synced 2026-01-01 15:55:49 +03:00
Bumps the go-modules-updates group with 2 updates: [github.com/didip/tollbooth/v7](https://github.com/didip/tollbooth) and [golang.org/x/crypto](https://github.com/golang/crypto). Updates `github.com/didip/tollbooth/v7` from 7.0.1 to 7.0.2 - [Commits](https://github.com/didip/tollbooth/compare/v7.0.1...v7.0.2) Updates `golang.org/x/crypto` from 0.23.0 to 0.24.0 - [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: github.com/didip/tollbooth/v7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates ... Signed-off-by: dependabot[bot] <support@github.com>
372 lines
9.6 KiB
Go
372 lines
9.6 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[K comparable, V any] interface {
|
|
fmt.Stringer
|
|
options[K, V]
|
|
Add(key K, value V) bool
|
|
Set(key K, value V, ttl time.Duration)
|
|
Get(key K) (V, bool)
|
|
GetExpiration(key K) (time.Time, bool)
|
|
GetOldest() (K, V, bool)
|
|
Contains(key K) (ok bool)
|
|
Peek(key K) (V, bool)
|
|
Values() []V
|
|
Keys() []K
|
|
Len() int
|
|
Remove(key K) bool
|
|
Invalidate(key K)
|
|
InvalidateFn(fn func(key K) bool)
|
|
RemoveOldest() (K, V, bool)
|
|
DeleteExpired()
|
|
Purge()
|
|
Resize(int) int
|
|
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[K comparable, V any] struct {
|
|
ttl time.Duration
|
|
maxKeys int
|
|
isLRU bool
|
|
onEvicted func(key K, value V)
|
|
|
|
sync.Mutex
|
|
stat Stats
|
|
items map[K]*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[K comparable, V any]() Cache[K, V] {
|
|
return &cacheImpl[K, V]{
|
|
items: map[K]*list.Element{},
|
|
evictList: list.New(),
|
|
ttl: noEvictionTTL,
|
|
maxKeys: 0,
|
|
}
|
|
}
|
|
|
|
// Add adds a value to the cache. Returns true if an eviction occurred.
|
|
// Returns false if there was no eviction: the item was already in the cache,
|
|
// or the size was not exceeded.
|
|
func (c *cacheImpl[K, V]) Add(key K, value V) (evicted bool) {
|
|
return c.addWithTTL(key, value, c.ttl)
|
|
}
|
|
|
|
// Set key, ttl of 0 would use cache-wide TTL
|
|
func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) {
|
|
c.addWithTTL(key, value, ttl)
|
|
}
|
|
|
|
// Returns true if an eviction occurred.
|
|
// Returns false if there was no eviction: the item was already in the cache,
|
|
// or the size was not exceeded.
|
|
func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted bool) {
|
|
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[K, V]).value = value
|
|
ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl)
|
|
return false
|
|
}
|
|
|
|
// Add new item
|
|
ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)}
|
|
entry := c.evictList.PushFront(ent)
|
|
c.items[key] = entry
|
|
c.stat.Added++
|
|
|
|
// Remove the oldest entry if it is expired, only in case of non-default TTL.
|
|
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
|
|
c.removeOldestIfExpired()
|
|
}
|
|
|
|
evict := c.maxKeys > 0 && len(c.items) > c.maxKeys
|
|
// Verify size not exceeded
|
|
if evict {
|
|
c.removeOldest()
|
|
}
|
|
return evict
|
|
}
|
|
|
|
// Get returns the key value if it's not expired
|
|
func (c *cacheImpl[K, V]) Get(key K) (V, bool) {
|
|
def := *new(V)
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
// Expired item check
|
|
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
|
c.stat.Misses++
|
|
return ent.Value.(*cacheItem[K, V]).value, false
|
|
}
|
|
if c.isLRU {
|
|
c.evictList.MoveToFront(ent)
|
|
}
|
|
c.stat.Hits++
|
|
return ent.Value.(*cacheItem[K, V]).value, true
|
|
}
|
|
c.stat.Misses++
|
|
return def, false
|
|
}
|
|
|
|
// Contains checks if a key is in the cache, without updating the recent-ness
|
|
// or deleting it for being stale.
|
|
func (c *cacheImpl[K, V]) Contains(key K) (ok bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
_, ok = c.items[key]
|
|
return ok
|
|
}
|
|
|
|
// 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[K, V]) Peek(key K) (V, bool) {
|
|
def := *new(V)
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
// Expired item check
|
|
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
|
c.stat.Misses++
|
|
return ent.Value.(*cacheItem[K, V]).value, false
|
|
}
|
|
c.stat.Hits++
|
|
return ent.Value.(*cacheItem[K, V]).value, true
|
|
}
|
|
c.stat.Misses++
|
|
return def, false
|
|
}
|
|
|
|
// GetExpiration returns the expiration time of the key. Non-existing key returns zero time.
|
|
func (c *cacheImpl[K, V]) GetExpiration(key K) (time.Time, bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
return ent.Value.(*cacheItem[K, V]).expiresAt, true
|
|
}
|
|
return time.Time{}, false
|
|
}
|
|
|
|
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
|
func (c *cacheImpl[K, V]) Keys() []K {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.keys()
|
|
}
|
|
|
|
// Values returns a slice of the values in the cache, from oldest to newest.
|
|
// Expired entries are filtered out.
|
|
func (c *cacheImpl[K, V]) Values() []V {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
values := make([]V, 0, len(c.items))
|
|
now := time.Now()
|
|
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
|
if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
|
continue
|
|
}
|
|
values = append(values, ent.Value.(*cacheItem[K, V]).value)
|
|
}
|
|
return values
|
|
}
|
|
|
|
// Len return count of items in cache, including expired
|
|
func (c *cacheImpl[K, V]) Len() int {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.evictList.Len()
|
|
}
|
|
|
|
// Resize changes the cache size. Size of 0 means unlimited.
|
|
func (c *cacheImpl[K, V]) Resize(size int) int {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if size <= 0 {
|
|
c.maxKeys = 0
|
|
return 0
|
|
}
|
|
diff := c.evictList.Len() - size
|
|
if diff < 0 {
|
|
diff = 0
|
|
}
|
|
for i := 0; i < diff; i++ {
|
|
c.removeOldest()
|
|
}
|
|
c.maxKeys = size
|
|
return diff
|
|
}
|
|
|
|
// Invalidate key (item) from the cache
|
|
func (c *cacheImpl[K, V]) Invalidate(key K) {
|
|
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[K, V]) InvalidateFn(fn func(key K) bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for key, ent := range c.items {
|
|
if fn(key) {
|
|
c.removeElement(ent)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove removes the provided key from the cache, returning if the
|
|
// key was contained.
|
|
func (c *cacheImpl[K, V]) Remove(key K) bool {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent, ok := c.items[key]; ok {
|
|
c.removeElement(ent)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RemoveOldest remove the oldest element in the cache
|
|
func (c *cacheImpl[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent := c.evictList.Back(); ent != nil {
|
|
c.removeElement(ent)
|
|
return ent.Value.(*cacheItem[K, V]).key, ent.Value.(*cacheItem[K, V]).value, true
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetOldest returns the oldest entry
|
|
func (c *cacheImpl[K, V]) GetOldest() (key K, value V, ok bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if ent := c.evictList.Back(); ent != nil {
|
|
return ent.Value.(*cacheItem[K, V]).key, ent.Value.(*cacheItem[K, V]).value, true
|
|
}
|
|
return
|
|
}
|
|
|
|
// DeleteExpired clears cache of expired items
|
|
func (c *cacheImpl[K, V]) DeleteExpired() {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
for _, key := range c.keys() {
|
|
if time.Now().After(c.items[key].Value.(*cacheItem[K, V]).expiresAt) {
|
|
c.removeElement(c.items[key])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Purge clears the cache completely.
|
|
func (c *cacheImpl[K, V]) 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[K, V]).value)
|
|
}
|
|
}
|
|
c.evictList.Init()
|
|
}
|
|
|
|
// Stat gets the current stats for cache
|
|
func (c *cacheImpl[K, V]) Stat() Stats {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.stat
|
|
}
|
|
|
|
func (c *cacheImpl[K, V]) 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[K, V]) keys() []K {
|
|
keys := make([]K, 0, len(c.items))
|
|
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
|
keys = append(keys, ent.Value.(*cacheItem[K, V]).key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// removeOldest removes the oldest item from the cache. Has to be called with lock!
|
|
func (c *cacheImpl[K, V]) 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[K, V]) removeOldestIfExpired() {
|
|
ent := c.evictList.Back()
|
|
if ent != nil && time.Now().After(ent.Value.(*cacheItem[K, V]).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[K, V]) removeElement(e *list.Element) {
|
|
c.evictList.Remove(e)
|
|
kv := e.Value.(*cacheItem[K, V])
|
|
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[K comparable, V any] struct {
|
|
expiresAt time.Time
|
|
key K
|
|
value V
|
|
}
|