Jason
5 years ago
8 changed files with 393 additions and 117 deletions
@ -0,0 +1,148 @@ |
|||
package cache |
|||
|
|||
// Modified by https://github.com/die-net/lrucache
|
|||
|
|||
import ( |
|||
"container/list" |
|||
"sync" |
|||
"time" |
|||
) |
|||
|
|||
// Option is part of Functional Options Pattern
|
|||
type Option func(*LruCache) |
|||
|
|||
// WithUpdateAgeOnGet update expires when Get element
|
|||
func WithUpdateAgeOnGet() Option { |
|||
return func(l *LruCache) { |
|||
l.updateAgeOnGet = true |
|||
} |
|||
} |
|||
|
|||
// WithAge defined element max age (second)
|
|||
func WithAge(maxAge int64) Option { |
|||
return func(l *LruCache) { |
|||
l.maxAge = maxAge |
|||
} |
|||
} |
|||
|
|||
// WithSize defined max length of LruCache
|
|||
func WithSize(maxSize int) Option { |
|||
return func(l *LruCache) { |
|||
l.maxSize = maxSize |
|||
} |
|||
} |
|||
|
|||
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
|||
// least recently used entries from memory when (if set) the entries are
|
|||
// older than maxAge (in seconds). Use the New constructor to create one.
|
|||
type LruCache struct { |
|||
maxAge int64 |
|||
maxSize int |
|||
mu sync.Mutex |
|||
cache map[interface{}]*list.Element |
|||
lru *list.List // Front is least-recent
|
|||
updateAgeOnGet bool |
|||
} |
|||
|
|||
// NewLRUCache creates an LruCache
|
|||
func NewLRUCache(options ...Option) *LruCache { |
|||
lc := &LruCache{ |
|||
lru: list.New(), |
|||
cache: make(map[interface{}]*list.Element), |
|||
} |
|||
|
|||
for _, option := range options { |
|||
option(lc) |
|||
} |
|||
|
|||
return lc |
|||
} |
|||
|
|||
// Get returns the interface{} representation of a cached response and a bool
|
|||
// set to true if the key was found.
|
|||
func (c *LruCache) Get(key interface{}) (interface{}, bool) { |
|||
c.mu.Lock() |
|||
defer c.mu.Unlock() |
|||
|
|||
le, ok := c.cache[key] |
|||
if !ok { |
|||
return nil, false |
|||
} |
|||
|
|||
if c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() { |
|||
c.deleteElement(le) |
|||
c.maybeDeleteOldest() |
|||
|
|||
return nil, false |
|||
} |
|||
|
|||
c.lru.MoveToBack(le) |
|||
entry := le.Value.(*entry) |
|||
if c.maxAge > 0 && c.updateAgeOnGet { |
|||
entry.expires = time.Now().Unix() + c.maxAge |
|||
} |
|||
value := entry.value |
|||
|
|||
return value, true |
|||
} |
|||
|
|||
// Set stores the interface{} representation of a response for a given key.
|
|||
func (c *LruCache) Set(key interface{}, value interface{}) { |
|||
c.mu.Lock() |
|||
defer c.mu.Unlock() |
|||
|
|||
expires := int64(0) |
|||
if c.maxAge > 0 { |
|||
expires = time.Now().Unix() + c.maxAge |
|||
} |
|||
|
|||
if le, ok := c.cache[key]; ok { |
|||
c.lru.MoveToBack(le) |
|||
e := le.Value.(*entry) |
|||
e.value = value |
|||
e.expires = expires |
|||
} else { |
|||
e := &entry{key: key, value: value, expires: expires} |
|||
c.cache[key] = c.lru.PushBack(e) |
|||
|
|||
if c.maxSize > 0 { |
|||
if len := c.lru.Len(); len > c.maxSize { |
|||
c.deleteElement(c.lru.Front()) |
|||
} |
|||
} |
|||
} |
|||
|
|||
c.maybeDeleteOldest() |
|||
} |
|||
|
|||
// Delete removes the value associated with a key.
|
|||
func (c *LruCache) Delete(key string) { |
|||
c.mu.Lock() |
|||
|
|||
if le, ok := c.cache[key]; ok { |
|||
c.deleteElement(le) |
|||
} |
|||
|
|||
c.mu.Unlock() |
|||
} |
|||
|
|||
func (c *LruCache) maybeDeleteOldest() { |
|||
if c.maxAge > 0 { |
|||
now := time.Now().Unix() |
|||
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() { |
|||
c.deleteElement(le) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (c *LruCache) deleteElement(le *list.Element) { |
|||
c.lru.Remove(le) |
|||
e := le.Value.(*entry) |
|||
delete(c.cache, e.key) |
|||
} |
|||
|
|||
type entry struct { |
|||
key interface{} |
|||
value interface{} |
|||
expires int64 |
|||
} |
@ -0,0 +1,117 @@ |
|||
package cache |
|||
|
|||
import ( |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
var entries = []struct { |
|||
key string |
|||
value string |
|||
}{ |
|||
{"1", "one"}, |
|||
{"2", "two"}, |
|||
{"3", "three"}, |
|||
{"4", "four"}, |
|||
{"5", "five"}, |
|||
} |
|||
|
|||
func TestLRUCache(t *testing.T) { |
|||
c := NewLRUCache() |
|||
|
|||
for _, e := range entries { |
|||
c.Set(e.key, e.value) |
|||
} |
|||
|
|||
c.Delete("missing") |
|||
_, ok := c.Get("missing") |
|||
assert.False(t, ok) |
|||
|
|||
for _, e := range entries { |
|||
value, ok := c.Get(e.key) |
|||
if assert.True(t, ok) { |
|||
assert.Equal(t, e.value, value.(string)) |
|||
} |
|||
} |
|||
|
|||
for _, e := range entries { |
|||
c.Delete(e.key) |
|||
|
|||
_, ok := c.Get(e.key) |
|||
assert.False(t, ok) |
|||
} |
|||
} |
|||
|
|||
func TestLRUMaxAge(t *testing.T) { |
|||
c := NewLRUCache(WithAge(86400)) |
|||
|
|||
now := time.Now().Unix() |
|||
expected := now + 86400 |
|||
|
|||
// Add one expired entry
|
|||
c.Set("foo", "bar") |
|||
c.lru.Back().Value.(*entry).expires = now |
|||
|
|||
// Reset
|
|||
c.Set("foo", "bar") |
|||
e := c.lru.Back().Value.(*entry) |
|||
assert.True(t, e.expires >= now) |
|||
c.lru.Back().Value.(*entry).expires = now |
|||
|
|||
// Set a few and verify expiration times
|
|||
for _, s := range entries { |
|||
c.Set(s.key, s.value) |
|||
e := c.lru.Back().Value.(*entry) |
|||
assert.True(t, e.expires >= expected && e.expires <= expected+10) |
|||
} |
|||
|
|||
// Make sure we can get them all
|
|||
for _, s := range entries { |
|||
_, ok := c.Get(s.key) |
|||
assert.True(t, ok) |
|||
} |
|||
|
|||
// Expire all entries
|
|||
for _, s := range entries { |
|||
le, ok := c.cache[s.key] |
|||
if assert.True(t, ok) { |
|||
le.Value.(*entry).expires = now |
|||
} |
|||
} |
|||
|
|||
// Get one expired entry, which should clear all expired entries
|
|||
_, ok := c.Get("3") |
|||
assert.False(t, ok) |
|||
assert.Equal(t, c.lru.Len(), 0) |
|||
} |
|||
|
|||
func TestLRUpdateOnGet(t *testing.T) { |
|||
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet()) |
|||
|
|||
now := time.Now().Unix() |
|||
expires := now + 86400/2 |
|||
|
|||
// Add one expired entry
|
|||
c.Set("foo", "bar") |
|||
c.lru.Back().Value.(*entry).expires = expires |
|||
|
|||
_, ok := c.Get("foo") |
|||
assert.True(t, ok) |
|||
assert.True(t, c.lru.Back().Value.(*entry).expires > expires) |
|||
} |
|||
|
|||
func TestMaxSize(t *testing.T) { |
|||
c := NewLRUCache(WithSize(2)) |
|||
// Add one expired entry
|
|||
c.Set("foo", "bar") |
|||
_, ok := c.Get("foo") |
|||
assert.True(t, ok) |
|||
|
|||
c.Set("bar", "foo") |
|||
c.Set("baz", "foo") |
|||
|
|||
_, ok = c.Get("foo") |
|||
assert.False(t, ok) |
|||
} |
Loading…
Reference in new issue