-
Notifications
You must be signed in to change notification settings - Fork 504
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
704 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
package simplelru | ||
|
||
import ( | ||
"container/list" | ||
"sync" | ||
"time" | ||
) | ||
|
||
// ExpirableLRU implements a thread safe LRU with expirable entries. | ||
type ExpirableLRU[K comparable, V any] struct { | ||
size int | ||
purgeEvery time.Duration | ||
ttl time.Duration | ||
done chan struct{} | ||
onEvicted EvictCallback[K, V] | ||
|
||
sync.Mutex | ||
items map[K]*list.Element | ||
evictList *list.List | ||
} | ||
|
||
// noEvictionTTL - very long ttl to prevent eviction | ||
const noEvictionTTL = time.Hour * 24 * 365 * 10 | ||
|
||
// NewExpirableLRU returns a new cache with expirable entries. | ||
// | ||
// Size parameter set to 0 makes cache of unlimited size. | ||
// | ||
// Providing 0 TTL turns expiring off. | ||
// | ||
// Activates deleteExpired by purgeEvery duration. | ||
// If MaxKeys and TTL are defined and PurgeEvery is zero, PurgeEvery will be set to 5 minutes. | ||
func NewExpirableLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl, purgeEvery time.Duration) *ExpirableLRU[K, V] { | ||
if size < 0 { | ||
size = 0 | ||
} | ||
if ttl <= 0 { | ||
ttl = noEvictionTTL | ||
} | ||
|
||
res := ExpirableLRU[K, V]{ | ||
items: map[K]*list.Element{}, | ||
evictList: list.New(), | ||
ttl: ttl, | ||
purgeEvery: purgeEvery, | ||
size: size, | ||
onEvicted: onEvict, | ||
done: make(chan struct{}), | ||
} | ||
|
||
// enable deleteExpired() running in separate goroutine for cache | ||
// with non-zero TTL and size defined | ||
if res.ttl != noEvictionTTL && (res.size > 0 || res.purgeEvery > 0) { | ||
if res.purgeEvery <= 0 { | ||
res.purgeEvery = time.Minute * 5 // non-zero purge enforced because size defined | ||
} | ||
go func(done <-chan struct{}) { | ||
ticker := time.NewTicker(res.purgeEvery) | ||
for { | ||
select { | ||
case <-done: | ||
return | ||
case <-ticker.C: | ||
res.Lock() | ||
res.deleteExpired() | ||
res.Unlock() | ||
} | ||
} | ||
}(res.done) | ||
} | ||
return &res | ||
} | ||
|
||
// Add key | ||
func (c *ExpirableLRU[K, V]) Add(key K, value V) (evicted bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
now := time.Now() | ||
|
||
// Check for existing item | ||
if ent, ok := c.items[key]; ok { | ||
c.evictList.MoveToFront(ent) | ||
ent.Value.(*expirableEntry[K, V]).value = value | ||
ent.Value.(*expirableEntry[K, V]).expiresAt = now.Add(c.ttl) | ||
return false | ||
} | ||
|
||
// Add new item | ||
ent := &expirableEntry[K, V]{key: key, value: value, expiresAt: now.Add(c.ttl)} | ||
entry := c.evictList.PushFront(ent) | ||
c.items[key] = entry | ||
|
||
// Verify size not exceeded | ||
if c.size > 0 && len(c.items) > c.size { | ||
c.removeOldest() | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
// Get returns the key value | ||
func (c *ExpirableLRU[K, V]) Get(key K) (value V, ok bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
if ent, found := c.items[key]; found { | ||
// Expired item check | ||
if time.Now().After(ent.Value.(*expirableEntry[K, V]).expiresAt) { | ||
return | ||
} | ||
c.evictList.MoveToFront(ent) | ||
return ent.Value.(*expirableEntry[K, V]).value, true | ||
} | ||
return | ||
} | ||
|
||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key. | ||
func (c *ExpirableLRU[K, V]) Peek(key K) (value V, ok bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
if ent, found := c.items[key]; found { | ||
// Expired item check | ||
if time.Now().After(ent.Value.(*expirableEntry[K, V]).expiresAt) { | ||
return | ||
} | ||
return ent.Value.(*expirableEntry[K, V]).value, true | ||
} | ||
return | ||
} | ||
|
||
// GetOldest returns the oldest entry | ||
func (c *ExpirableLRU[K, V]) GetOldest() (key K, value V, ok bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
ent := c.evictList.Back() | ||
if ent != nil { | ||
kv := ent.Value.(*expirableEntry[K, V]) | ||
return kv.key, kv.value, true | ||
} | ||
return | ||
} | ||
|
||
// Contains checks if a key is in the cache, without updating the recent-ness | ||
// or deleting it for being stale. | ||
func (c *ExpirableLRU[K, V]) Contains(key K) (ok bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
_, ok = c.items[key] | ||
return ok | ||
} | ||
|
||
// Remove key from the cache | ||
func (c *ExpirableLRU[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 removes the oldest item from the cache. | ||
func (c *ExpirableLRU[K, V]) RemoveOldest() (key K, value V, ok bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
ent := c.evictList.Back() | ||
if ent != nil { | ||
c.removeElement(ent) | ||
kv := ent.Value.(*expirableEntry[K, V]) | ||
return kv.key, kv.value, true | ||
} | ||
return | ||
} | ||
|
||
// Keys returns a slice of the keys in the cache, from oldest to newest. | ||
func (c *ExpirableLRU[K, V]) Keys() []K { | ||
c.Lock() | ||
defer c.Unlock() | ||
return c.keys() | ||
} | ||
|
||
// Purge clears the cache completely. | ||
func (c *ExpirableLRU[K, V]) Purge() { | ||
c.Lock() | ||
defer c.Unlock() | ||
for k, v := range c.items { | ||
if c.onEvicted != nil { | ||
c.onEvicted(k, v.Value.(*expirableEntry[K, V]).value) | ||
} | ||
delete(c.items, k) | ||
} | ||
c.evictList.Init() | ||
} | ||
|
||
// DeleteExpired clears cache of expired items | ||
func (c *ExpirableLRU[K, V]) DeleteExpired() { | ||
c.Lock() | ||
defer c.Unlock() | ||
c.deleteExpired() | ||
} | ||
|
||
// Len return count of items in cache | ||
func (c *ExpirableLRU[K, V]) Len() int { | ||
c.Lock() | ||
defer c.Unlock() | ||
return c.evictList.Len() | ||
} | ||
|
||
// Resize changes the cache size. size 0 doesn't resize the cache, as it means unlimited. | ||
func (c *ExpirableLRU[K, V]) Resize(size int) (evicted int) { | ||
if size <= 0 { | ||
return 0 | ||
} | ||
c.Lock() | ||
defer c.Unlock() | ||
diff := c.evictList.Len() - size | ||
if diff < 0 { | ||
diff = 0 | ||
} | ||
for i := 0; i < diff; i++ { | ||
c.removeOldest() | ||
} | ||
c.size = size | ||
return diff | ||
} | ||
|
||
// Close cleans the cache and destroys running goroutines | ||
func (c *ExpirableLRU[K, V]) Close() { | ||
c.Lock() | ||
defer c.Unlock() | ||
close(c.done) | ||
} | ||
|
||
// removeOldest removes the oldest item from the cache. Has to be called with lock! | ||
func (c *ExpirableLRU[K, V]) removeOldest() { | ||
ent := c.evictList.Back() | ||
if ent != nil { | ||
c.removeElement(ent) | ||
} | ||
} | ||
|
||
// Keys returns a slice of the keys in the cache, from oldest to newest. Has to be called with lock! | ||
func (c *ExpirableLRU[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.(*expirableEntry[K, V]).key) | ||
} | ||
return keys | ||
} | ||
|
||
// removeElement is used to remove a given list element from the cache. Has to be called with lock! | ||
func (c *ExpirableLRU[K, V]) removeElement(e *list.Element) { | ||
c.evictList.Remove(e) | ||
kv := e.Value.(*expirableEntry[K, V]) | ||
delete(c.items, kv.key) | ||
if c.onEvicted != nil { | ||
c.onEvicted(kv.key, kv.value) | ||
} | ||
} | ||
|
||
// deleteExpired deletes expired records. Has to be called with lock! | ||
func (c *ExpirableLRU[K, V]) deleteExpired() { | ||
for _, key := range c.keys() { | ||
if time.Now().After(c.items[key].Value.(*expirableEntry[K, V]).expiresAt) { | ||
c.removeElement(c.items[key]) | ||
continue | ||
} | ||
} | ||
} | ||
|
||
// expirableEntry is used to hold a value in the evictList | ||
type expirableEntry[K comparable, V any] struct { | ||
key K | ||
value V | ||
expiresAt time.Time | ||
} |
Oops, something went wrong.