From d4418943be0e1077dbd69923873d50b64874aa82 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 00:20:05 -0800 Subject: [PATCH 01/17] support simple expiring cache --- 2q.go | 66 ++-- arc.go | 94 ++++-- doc.go | 3 + expiringlru.go | 356 ++++++++++++++++++++ expiringlru_test.go | 671 +++++++++++++++++++++++++++++++++++++ lru.go | 33 +- rwlocker.go | 24 ++ simplelru/lru.go | 14 +- simplelru/lru_interface.go | 8 +- 9 files changed, 1196 insertions(+), 73 deletions(-) create mode 100644 expiringlru.go create mode 100644 expiringlru_test.go create mode 100644 rwlocker.go diff --git a/2q.go b/2q.go index 15fcad0..72e7d18 100644 --- a/2q.go +++ b/2q.go @@ -33,18 +33,21 @@ type TwoQueueCache struct { recent simplelru.LRUCache frequent simplelru.LRUCache recentEvict simplelru.LRUCache - lock sync.RWMutex + lock RWLocker } +// Option2Q define option to customize TwoQueueCache +type Option2Q func(c *TwoQueueCache) error + // New2Q creates a new TwoQueueCache using the default // values for the parameters. -func New2Q(size int) (*TwoQueueCache, error) { - return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries) +func New2Q(size int, opts ...Option2Q) (*TwoQueueCache, error) { + return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries, opts...) } // New2QParams creates a new TwoQueueCache using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) { +func New2QParams(size int, recentRatio, ghostRatio float64, opts ...Option2Q) (*TwoQueueCache, error) { if size <= 0 { return nil, fmt.Errorf("invalid size") } @@ -80,10 +83,23 @@ func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, err recent: recent, frequent: frequent, recentEvict: recentEvict, + lock: &sync.RWMutex{}, + } + //apply options for customization + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } } return c, nil } +// NoLock2Q disables locking for TwoQueueCache +func NoLock2Q(c *TwoQueueCache) error { + c.lock = NoOpRWLocker{} + return nil +} + // Get looks up a key's value from the cache. func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() @@ -106,8 +122,8 @@ func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { return nil, false } -// Add adds a value to the cache. -func (c *TwoQueueCache) Add(key, value interface{}) { +// Add adds a value to the cache, return evicted key/val if eviction happens. +func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() @@ -126,22 +142,29 @@ func (c *TwoQueueCache) Add(key, value interface{}) { return } + var evictedKey, evictedValue interface{} // If the value was recently evicted, add it to the // frequently used list if c.recentEvict.Contains(key) { - c.ensureSpace(true) + evictedKey, evictedValue, evicted = c.ensureSpace(true) c.recentEvict.Remove(key) c.frequent.Add(key, value) - return + } else { + // Add to the recently seen list + evictedKey, evictedValue, evicted = c.ensureSpace(false) + c.recent.Add(key, value) } - - // Add to the recently seen list - c.ensureSpace(false) - c.recent.Add(key, value) + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return } // ensureSpace is used to ensure we have space in the cache -func (c *TwoQueueCache) ensureSpace(recentEvict bool) { +func (c *TwoQueueCache) ensureSpace(recentEvict bool) (key, value interface{}, evicted bool) { // If we have space, nothing to do recentLen := c.recent.Len() freqLen := c.frequent.Len() @@ -152,13 +175,13 @@ func (c *TwoQueueCache) ensureSpace(recentEvict bool) { // If the recent buffer is larger than // the target, evict from there if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { - k, _, _ := c.recent.RemoveOldest() - c.recentEvict.Add(k, nil) + key, value, evicted = c.recent.RemoveOldest() + c.recentEvict.Add(key, nil) return } // Remove from the frequent list otherwise - c.frequent.RemoveOldest() + return c.frequent.RemoveOldest() } // Len returns the number of items in the cache. @@ -179,18 +202,17 @@ func (c *TwoQueueCache) Keys() []interface{} { } // Remove removes the provided key from the cache. -func (c *TwoQueueCache) Remove(key interface{}) { +func (c *TwoQueueCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() if c.frequent.Remove(key) { - return + return true } if c.recent.Remove(key) { - return - } - if c.recentEvict.Remove(key) { - return + return true } + c.recentEvict.Remove(key) + return false } // Purge is used to completely clear the cache. diff --git a/arc.go b/arc.go index e396f84..df15bdc 100644 --- a/arc.go +++ b/arc.go @@ -24,11 +24,14 @@ type ARCCache struct { t2 simplelru.LRUCache // T2 is the LRU for frequently accessed items b2 simplelru.LRUCache // B2 is the LRU for evictions from t2 - lock sync.RWMutex + lock RWLocker } +// OptionARC defines option to customize ARCCache +type OptionARC func(*ARCCache) error + // NewARC creates an ARC of the given size -func NewARC(size int) (*ARCCache, error) { +func NewARC(size int, opts ...OptionARC) (*ARCCache, error) { // Create the sub LRUs b1, err := simplelru.NewLRU(size, nil) if err != nil { @@ -55,10 +58,23 @@ func NewARC(size int) (*ARCCache, error) { b1: b1, t2: t2, b2: b2, + lock: &sync.RWMutex{}, + } + //apply option settings + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } } return c, nil } +// NoLockARC disables locking for ARCCache +func NoLockARC(c *ARCCache) error { + c.lock = NoOpRWLocker{} + return nil +} + // Get looks up a key's value from the cache. func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() @@ -81,8 +97,8 @@ func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { return nil, false } -// Add adds a value to the cache. -func (c *ARCCache) Add(key, value interface{}) { +// Add adds a value to the cache, return evicted key/val if it happens. +func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() @@ -100,9 +116,10 @@ func (c *ARCCache) Add(key, value interface{}) { return } - // Check if this value was recently evicted as part of the - // recently used list + var evictedKey, evictedValue interface{} if c.b1.Contains(key) { + // Check if this value was recently evicted as part of the + // recently used list // T1 set is too small, increase P appropriately delta := 1 b1Len := c.b1.Len() @@ -118,7 +135,7 @@ func (c *ARCCache) Add(key, value interface{}) { // Potentially need to make room in the cache if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(false) + evictedKey, evictedValue, evicted = c.replace(false) } // Remove from B1 @@ -126,12 +143,10 @@ func (c *ARCCache) Add(key, value interface{}) { // Add the key to the frequently used list c.t2.Add(key, value) - return - } - // Check if this value was recently evicted as part of the - // frequently used list - if c.b2.Contains(key) { + } else if c.b2.Contains(key) { + // Check if this value was recently evicted as part of the + // frequently used list // T2 set is too small, decrease P appropriately delta := 1 b1Len := c.b1.Len() @@ -147,7 +162,7 @@ func (c *ARCCache) Add(key, value interface{}) { // Potentially need to make room in the cache if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(true) + evictedKey, evictedValue, evicted = c.replace(true) } // Remove from B2 @@ -155,41 +170,49 @@ func (c *ARCCache) Add(key, value interface{}) { // Add the key to the frequently used list c.t2.Add(key, value) - return - } + } else { + // Brand new entry + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + evictedKey, evictedValue, evicted = c.replace(false) + } - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - c.replace(false) - } + // Keep the size of the ghost buffers trim + if c.b1.Len() > c.size-c.p { + c.b1.RemoveOldest() + } + if c.b2.Len() > c.p { + c.b2.RemoveOldest() + } - // Keep the size of the ghost buffers trim - if c.b1.Len() > c.size-c.p { - c.b1.RemoveOldest() + // Add to the recently seen list + c.t1.Add(key, value) } - if c.b2.Len() > c.p { - c.b2.RemoveOldest() + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey } - - // Add to the recently seen list - c.t1.Add(key, value) + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return } // replace is used to adaptively evict from either T1 or T2 // based on the current learned value of P -func (c *ARCCache) replace(b2ContainsKey bool) { +func (c *ARCCache) replace(b2ContainsKey bool) (k, v interface{}, ok bool) { t1Len := c.t1.Len() if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { - k, _, ok := c.t1.RemoveOldest() + k, v, ok = c.t1.RemoveOldest() if ok { c.b1.Add(k, nil) } } else { - k, _, ok := c.t2.RemoveOldest() + k, v, ok = c.t2.RemoveOldest() if ok { c.b2.Add(k, nil) } } + return } // Len returns the number of cached entries @@ -209,21 +232,22 @@ func (c *ARCCache) Keys() []interface{} { } // Remove is used to purge a key from the cache -func (c *ARCCache) Remove(key interface{}) { +func (c *ARCCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() if c.t1.Remove(key) { - return + return true } if c.t2.Remove(key) { - return + return true } if c.b1.Remove(key) { - return + return false } if c.b2.Remove(key) { - return + return false } + return false } // Purge is used to clear the cache diff --git a/doc.go b/doc.go index 2547df9..ebb824b 100644 --- a/doc.go +++ b/doc.go @@ -16,6 +16,9 @@ // ARC has been patented by IBM, so do not use it if that is problematic for // your program. // +// ExpiringCache wraps one of the above caches and make their entries expiring +// according to policies: ExpireAfterAccess or ExpireAfterWrite. +// // All caches in this package take locks while operating, and are therefore // thread-safe for consumers. package lru diff --git a/expiringlru.go b/expiringlru.go new file mode 100644 index 0000000..30ac7de --- /dev/null +++ b/expiringlru.go @@ -0,0 +1,356 @@ +package lru + +import ( + "container/list" + "fmt" + "sync" + "time" +) + +//common interface shared by 2q, arc and simple LRU, used as interface of backing LRU +type lruCache interface { + // Adds a value to the cache, returns evicted if happened and + // updates the "recently used"-ness of the key. + Add(k, v interface{}, evictedKeyVal ...*interface{}) (evicted bool) + // Returns key's value from the cache if found and + // updates the "recently used"-ness of the key. + Get(k interface{}) (v interface{}, ok bool) + // Removes a key from the cache + Remove(k interface{}) bool + // Returns key's value without updating the "recently used"-ness of the key. + Peek(key interface{}) (value interface{}, ok bool) + // Checks if a key exists in cache without updating the recent-ness. + Contains(k interface{}) bool + // Returns a slice of the keys in the cache, from oldest to newest. + Keys() []interface{} + // Returns the number of items in the cache. + Len() int + // Clears all cache entries. + Purge() +} + +type entry struct { + key interface{} + val interface{} + expirationTime time.Time + elem *list.Element +} + +func (e entry) String() string { + return fmt.Sprintf("%v,%v %v", e.key, e.val, e.expirationTime) +} + +//two expiration policies +type expiringType byte + +const ( + expireAfterWrite expiringType = iota + expireAfterAccess +) + +// ExpiringCache will wrap an existing LRU and make its entries expiring +// according to two policies: +// expireAfterAccess and expireAfterWrite (default) +// Internally keep a expireList sorted by entries' expirationTime +type ExpiringCache struct { + lru lruCache + expiration time.Duration + expireList *expireList + expireType expiringType + //placeholder for time.Now() for easier testing setup + timeNow func() time.Time + lock RWLocker +} + +// OptionExp defines option to customize ExpiringCache +type OptionExp func(c *ExpiringCache) error + +// NewExpiring2Q creates an expiring cache with specifized +// size and entries lifetime duration, backed by a 2-queue LRU +func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + //create a non synced LRU as backing store + lru, err := New2Q(size, NoLock2Q) + if err != nil { + return + } + elru, err = Expiring(expir, lru, opts...) + return +} + +// NewExpiringARC creates an expiring cache with specifized +// size and entries lifetime duration, backed by a ARC LRU +func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + //create a non synced LRU as backing store + lru, err := NewARC(size, NoLockARC) + if err != nil { + return + } + elru, err = Expiring(expir, lru, opts...) + return +} + +// NewExpiringLRU creates an expiring cache with specifized +// size and entries lifetime duration, backed by a simple LRU +func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + //create a non synced LRU as backing store + lru, err := New(size, NoLock) + if err != nil { + return + } + elru, err = Expiring(expir, lru, opts...) + return +} + +// Expiring will wrap an existing LRU to make its entries +// expiring with specified duration +func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCache, error) { + //create expiring cache with default settings + elru := &ExpiringCache{ + lru: lru, + expiration: expir, + expireList: newExpireList(), + expireType: expireAfterWrite, + timeNow: time.Now, + lock: &sync.RWMutex{}, + } + //apply options to customize + for _, opt := range opts { + if err := opt(elru); err != nil { + return nil, err + } + } + return elru, nil +} + +// NoLockExp disables locking for ExpiringCache +func NoLockExp(elru *ExpiringCache) error { + elru.lock = NoOpRWLocker{} + return nil +} + +// ExpireAfterWrite sets expiring policy +func ExpireAfterWrite(elru *ExpiringCache) error { + elru.expireType = expireAfterWrite + return nil +} + +// ExpireAfterAccess sets expiring policy +func ExpireAfterAccess(elru *ExpiringCache) error { + elru.expireType = expireAfterAccess + return nil +} + +// TimeTicker sets the function used to return current time, for test setup +func TimeTicker(tn func() time.Time) OptionExp { + return func(elru *ExpiringCache) error { + elru.timeNow = tn + return nil + } +} + +// Add add a key/val pair to cache with cache's default expiration duration +// return evicted key/val pair if eviction happens. +// Should be used in most cases for better performance +func (elru *ExpiringCache) Add(k, v interface{}, evictedKeyVal ...*interface{}) (evicted bool) { + return elru.AddWithTTL(k, v, elru.expiration, evictedKeyVal...) +} + +// AddWithTTL add a key/val pair to cache with provided expiration duration +// return evicted key/val pair if eviction happens. +// Using this with variant expiration durations could cause degraded performance +func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration, evictedKeyVal ...*interface{}) (evicted bool) { + elru.lock.Lock() + defer elru.lock.Unlock() + now := elru.timeNow() + var ent *entry + var expired []*entry + if ent0, _ := elru.lru.Peek(k); ent0 != nil { + //update existing cache entry + ent = ent0.(*entry) + ent.val = v + ent.expirationTime = now.Add(expiration) + elru.expireList.MoveToFront(ent) + } else { + //first remove 1 possible expiration to add space for new entry + expired = elru.removeExpired(now, false) + //add new entry to expiration list + ent = &entry{ + key: k, + val: v, + expirationTime: now.Add(expiration), + } + elru.expireList.PushFront(ent) + } + // Add/Update cache entry in backing cache + var evictedKey, evictedVal interface{} + evicted = elru.lru.Add(k, ent, &evictedKey, &evictedVal) + //remove evicted ent from expireList + if evicted { + ent = evictedVal.(*entry) + evictedVal = ent.val + elru.expireList.Remove(ent) + } else if len(expired) > 0 { + evictedKey = expired[0].key + evictedVal = expired[0].val + evicted = true + } + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedVal + } + return +} + +// Get returns key's value from the cache if found +func (elru *ExpiringCache) Get(k interface{}) (v interface{}, ok bool) { + elru.lock.Lock() + defer elru.lock.Unlock() + now := elru.timeNow() + if ent0, ok := elru.lru.Get(k); ok { + ent := ent0.(*entry) + if ent.expirationTime.After(now) { + if elru.expireType == expireAfterAccess { + ent.expirationTime = now.Add(elru.expiration) + elru.expireList.MoveToFront(ent) + } + return ent.val, true + } + } + return +} + +// Remove removes a key from the cache +func (elru *ExpiringCache) Remove(k interface{}) bool { + elru.lock.Lock() + defer elru.lock.Unlock() + if ent, _ := elru.lru.Peek(k); ent != nil { + elru.expireList.Remove(ent.(*entry)) + return elru.lru.Remove(k) + } + return false +} + +// Peek return key's value without updating the "recently used"-ness of the key. +// returns ok=false if k not found or entry expired +func (elru *ExpiringCache) Peek(k interface{}) (v interface{}, ok bool) { + elru.lock.RLock() + defer elru.lock.RUnlock() + if ent0, ok := elru.lru.Peek(k); ok { + ent := ent0.(*entry) + if ent.expirationTime.After(elru.timeNow()) { + return ent.val, true + } + return ent.val, false + } + return +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (elru *ExpiringCache) Contains(k interface{}) bool { + _, ok := elru.Peek(k) + return ok +} + +// Keys returns a slice of the keys in the cache. +// The frequently used keys are first in the returned slice. +func (elru *ExpiringCache) Keys() []interface{} { + elru.lock.Lock() + defer elru.lock.Unlock() + //to get accurate key set, remove all expired + elru.removeExpired(elru.timeNow(), true) + return elru.lru.Keys() +} + +// Len returns the number of items in the cache. +func (elru *ExpiringCache) Len() int { + elru.lock.Lock() + defer elru.lock.Unlock() + //to get accurate size, remove all expired + elru.removeExpired(elru.timeNow(), true) + return elru.lru.Len() +} + +// Purge is used to completely clear the cache. +func (elru *ExpiringCache) Purge() { + elru.lock.Lock() + defer elru.lock.Unlock() + elru.expireList.Init() + elru.lru.Purge() +} + +//either remove one (the oldest expired), or all expired +func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) (res []*entry) { + res = elru.expireList.RemoveExpired(now, removeAllExpired) + for i := 0; i < len(res); i++ { + elru.lru.Remove(res[i].key) + } + return +} + +// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine +func (elru *ExpiringCache) RemoveAllExpired() { + elru.removeExpired(elru.timeNow(), true) +} + +// oldest entries are at front of expire list +type expireList struct { + expList *list.List +} + +func newExpireList() *expireList { + return &expireList{ + expList: list.New(), + } +} + +func (el *expireList) Init() { + el.expList.Init() +} + +func (el *expireList) PushFront(ent *entry) { + //When all operations use ExpiringCache default expiration, + //PushFront should succeed at first/front entry of list + for e := el.expList.Front(); e != nil; e = e.Next() { + if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { + ent.elem = el.expList.InsertBefore(ent, e) + return + } + } + ent.elem = el.expList.PushBack(ent) +} + +func (el *expireList) MoveToFront(ent *entry) { + //When all operations use ExpiringCache default expiration, + //MoveToFront should succeed at first/front entry of list + for e := el.expList.Front(); e != nil; e = e.Next() { + if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { + el.expList.MoveBefore(ent.elem, e) + return + } + } + el.expList.MoveAfter(ent.elem, el.expList.Back()) +} + +func (el *expireList) Remove(ent *entry) interface{} { + return el.expList.Remove(ent.elem) +} + +//either remove one (the oldest expired), or remove all expired +func (el *expireList) RemoveExpired(now time.Time, removeAllExpired bool) (res []*entry) { + for { + back := el.expList.Back() + if back == nil || back.Value.(*entry).expirationTime.After(now) { + break + } + //expired + ent := el.expList.Remove(back).(*entry) + res = append(res, ent) + if !removeAllExpired { + break + } + } + return +} diff --git a/expiringlru_test.go b/expiringlru_test.go new file mode 100644 index 0000000..2d0ce57 --- /dev/null +++ b/expiringlru_test.go @@ -0,0 +1,671 @@ +package lru + +import ( + "math/rand" + "sort" + "testing" + "time" +) + +func BenchmarkExpiring2Q_Rand(b *testing.B) { + l, err := NewExpiring2Q(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiring2Q_Freq(b *testing.B) { + l, err := NewExpiring2Q(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringARC_Rand(b *testing.B) { + l, err := NewExpiringARC(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringARC_Freq(b *testing.B) { + l, err := NewExpiringARC(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringLRU_Rand(b *testing.B) { + l, err := NewExpiringLRU(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkExpiringLRU_Freq(b *testing.B) { + l, err := NewExpiringLRU(8192, 5*time.Minute) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func TestExpiring2Q_RandomOps(t *testing.T) { + size := 128 + l, err := NewExpiring2Q(size, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.Len() > size { + t.Fatalf("bad ExpiringCache size: %d, expected: %d", + l.Len(), size) + } + } +} + +func TestExpiringARC_RandomOps(t *testing.T) { + size := 128 + l, err := NewExpiringARC(size, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.Len() > size { + t.Fatalf("bad ExpiringCache size: %d, expected: %d", + l.Len(), size) + } + } +} + +func TestExpiringLRU_RandomOps(t *testing.T) { + size := 128 + l, err := NewExpiringLRU(size, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.Len() > size { + t.Fatalf("bad ExpiringCache size: %d, expected: %d", + l.Len(), size) + } + } +} + +// Test eviction by least-recently-used (2-queue LRU suuport retaining frequently-used) +func TestExpiring2Q_EvictionByLRU(t *testing.T) { + elru, err := NewExpiring2Q(3, 30*time.Second) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //next add 3,4; verify 2, 3 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-1) || v != (i-1) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-1, i-1, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + //since 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are kept + //and 2,3,4 only touched once (write), so they + //moved thru "recent" list, with 2,3 evicted + for i, v := range []int{0, 1, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) + } + } +} + +//testTimer used to simulate time-elapse for expiration tests +type testTimer struct { + t time.Time +} + +func newTestTimer() *testTimer { return &testTimer{time.Now()} } +func (tt *testTimer) Now() time.Time { return tt.t } +func (tt *testTimer) Advance(d time.Duration) { tt.t = tt.t.Add(d) } + +// Test eviction by ExpireAfterWrite +func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now)) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + //so they should be evicted, although they are more recently retrieved than <2,2> + tt.Advance(15 * time.Second) + //next add 3,4; verify 0,1 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-3) || v != (i-3) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //althoug 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are evicted because expiration + //and 2,3,4 will be kept + for i, v := range []int{2, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterAccess: basically same access sequence as above case +// but different result because of ExpireAfterAccess +func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //also moved them to back in expire list with newer timestamp + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, none expired + //and 2 in recent list + tt.Advance(15 * time.Second) + //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + for i := 3; i < 5; i++ { + elru.Add(i, i) + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //and 0,1,4 will be kept + for i, v := range []int{0, 1, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterWrite +func TestExpiringARC_ExpireAfterWrite(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now)) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + //so they should be evicted, although they are more recently retrieved than <2,2> + tt.Advance(15 * time.Second) + //next add 3,4; verify 0,1 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-3) || v != (i-3) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //althoug 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are evicted because expiration + //and 2,3,4 will be kept + for i, v := range []int{2, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterAccess: basically same access sequence as above case +// but different result because of ExpireAfterAccess +func TestExpiringARC_ExpireAfterAccess(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //also moved them to back in expire list with newer timestamp + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, none expired + //and 2 in recent list + tt.Advance(15 * time.Second) + //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + for i := 3; i < 5; i++ { + elru.Add(i, i) + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //and 0,1,4 will be kept + for i, v := range []int{0, 1, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterWrite +func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now)) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to freq-used list + //2 will remain in recent-used list + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + //so they should be evicted, although they are more recently retrieved than <2,2> + tt.Advance(15 * time.Second) + //next add 3,4; verify 0,1 will be evicted + var ek, ev interface{} + for i := 3; i < 5; i++ { + evicted := elru.Add(i, i, &ek, &ev) + k, v := ek.(int), ev.(int) + if !evicted || k != (i-3) || v != (i-3) { + t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) + } + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //althoug 0, 1 are touched twice (write & read) so + //they are in frequently used list, they are evicted because expiration + //and 2,3,4 will be kept + for i, v := range []int{2, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) + } + } +} + +// Test eviction by ExpireAfterAccess: basically same access sequence as above case +// but different result because of ExpireAfterAccess +func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { + //use test timer for expiration + tt := newTestTimer() + elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) + if err != nil { + t.Fatalf("failed to create expiring LRU") + } + for i := 0; i < 2; i++ { + elru.Add(i, i) + } + //test timer ticks 20 seconds + tt.Advance(20 * time.Second) + //add fresher entry <2,2> to cache + elru.Add(2, 2) + //Get(0),Get(1) will move 0, 1 to back of access list + //also moved them to back in expire list with newer timestamp + //access list will be 2,0,1 + for i := 0; i < 2; i++ { + elru.Get(i) + } + //test timer advance another 15 seconds, none expired + tt.Advance(15 * time.Second) + //next add 3,4; verify 2,0 will be evicted + for i := 3; i < 5; i++ { + elru.Add(i, i) + } + if elru.Len() != 3 { + t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) + } + keys := elru.Keys() + sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) + //and 1,3,4 will be kept + for i, v := range []int{1, 3, 4} { + if v != keys[i] { + t.Fatalf("Expiring LRU eviction failed, expected keys {1,3,4} left, but found %v", elru.Keys()) + } + } +} + +func TestExpiring2Q(t *testing.T) { + l, err := NewExpiring2Q(128, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + _, ok := l.Get(i) + if ok { + t.Fatalf("should be deleted") + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// Test that Contains doesn't update recent-ness +func TestExpiring2Q_Contains(t *testing.T) { + l, err := NewExpiring2Q(2, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func TestExpiring2Q_Peek(t *testing.T) { + l, err := NewExpiring2Q(2, 5*time.Minute) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} diff --git a/lru.go b/lru.go index aa52433..cd7f99b 100644 --- a/lru.go +++ b/lru.go @@ -9,27 +9,44 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { lru simplelru.LRUCache - lock sync.RWMutex + lock RWLocker } +// Option to customize LRUCache +type Option func(*Cache) error + // New creates an LRU of the given size. -func New(size int) (*Cache, error) { - return NewWithEvict(size, nil) +func New(size int, opts ...Option) (*Cache, error) { + return NewWithEvict(size, nil, opts...) } // NewWithEvict constructs a fixed size cache with the given eviction // callback. -func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { +func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), opts ...Option) (*Cache, error) { + //create a cache with default settings lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) if err != nil { return nil, err } c := &Cache{ - lru: lru, + lru: lru, + lock: &sync.RWMutex{}, + } + //apply options for custimization + for _, opt := range opts { + if err = opt(c); err != nil { + return nil, err + } } return c, nil } +// NoLock disables locking for LRUCache +func NoLock(c *Cache) error { + c.lock = NoOpRWLocker{} + return nil +} + // Purge is used to completely clear the cache. func (c *Cache) Purge() { c.lock.Lock() @@ -37,10 +54,10 @@ func (c *Cache) Purge() { c.lock.Unlock() } -// Add adds a value to the cache. Returns true if an eviction occurred. -func (c *Cache) Add(key, value interface{}) (evicted bool) { +// Add adds a value to the cache. Returns true and evicted key/val if an eviction occurred. +func (c *Cache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() - evicted = c.lru.Add(key, value) + evicted = c.lru.Add(key, value, evictedKeyVal...) c.lock.Unlock() return evicted } diff --git a/rwlocker.go b/rwlocker.go new file mode 100644 index 0000000..58012f8 --- /dev/null +++ b/rwlocker.go @@ -0,0 +1,24 @@ +package lru + +// RWLocker define base interface of sync.RWMutex +type RWLocker interface { + Lock() + Unlock() + RLock() + RUnlock() +} + +// NoOpRWLocker is a dummy noop implementation of RWLocker interface +type NoOpRWLocker struct{} + +// Lock perform noop Lock() operation +func (nop NoOpRWLocker) Lock() {} + +// Unlock perform noop Unlock() operation +func (nop NoOpRWLocker) Unlock() {} + +// RLock perform noop RLock() operation +func (nop NoOpRWLocker) RLock() {} + +// RUnlock perform noop RUnlock() operation +func (nop NoOpRWLocker) RUnlock() {} diff --git a/simplelru/lru.go b/simplelru/lru.go index 9233583..259e6b3 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -48,7 +48,7 @@ func (c *LRU) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *LRU) Add(key, value interface{}) (evicted bool) { +func (c *LRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evict bool) { // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) @@ -61,12 +61,18 @@ func (c *LRU) Add(key, value interface{}) (evicted bool) { entry := c.evictList.PushFront(ent) c.items[key] = entry - evict := c.evictList.Len() > c.size + evict = c.evictList.Len() > c.size // Verify size not exceeded if evict { - c.removeOldest() + k, v, _ := c.RemoveOldest() + if len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = k + } + if len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = v + } } - return evict + return } // Get looks up a key's value from the cache. diff --git a/simplelru/lru_interface.go b/simplelru/lru_interface.go index cb7f8ca..33000bd 100644 --- a/simplelru/lru_interface.go +++ b/simplelru/lru_interface.go @@ -3,9 +3,9 @@ package simplelru // LRUCache is the interface for simple LRU cache. type LRUCache interface { - // Adds a value to the cache, returns true if an eviction occurred and - // updates the "recently used"-ness of the key. - Add(key, value interface{}) bool + // Adds a value to the cache, returns true if an eviction occurred + // return evicted key/val and updates the "recently used"-ness of the key. + Add(key, value interface{}, evictedKeyVal ...*interface{}) bool // Returns key's value from the cache and // updates the "recently used"-ness of the key. #value, isFound @@ -36,5 +36,5 @@ type LRUCache interface { Purge() // Resizes cache, returning number evicted - Resize(int) int + Resize(size int) int } From a3c9413ccb51db5a9bdd89f7540f6e0dd8c21794 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 00:53:31 -0800 Subject: [PATCH 02/17] fix lint issues --- 2q.go | 4 +- arc.go | 11 ++-- expiringlru.go | 44 +++++++------- expiringlru_test.go | 140 ++++++++++++++++++++++---------------------- lru.go | 4 +- 5 files changed, 102 insertions(+), 101 deletions(-) diff --git a/2q.go b/2q.go index 72e7d18..6006ffe 100644 --- a/2q.go +++ b/2q.go @@ -85,7 +85,7 @@ func New2QParams(size int, recentRatio, ghostRatio float64, opts ...Option2Q) (* recentEvict: recentEvict, lock: &sync.RWMutex{}, } - //apply options for customization + // Apply options for customization for _, opt := range opts { if err = opt(c); err != nil { return nil, err @@ -160,7 +160,7 @@ func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{ if evicted && len(evictedKeyVal) > 1 { *evictedKeyVal[1] = evictedValue } - return + return evicted } // ensureSpace is used to ensure we have space in the cache diff --git a/arc.go b/arc.go index df15bdc..0770b70 100644 --- a/arc.go +++ b/arc.go @@ -60,7 +60,7 @@ func NewARC(size int, opts ...OptionARC) (*ARCCache, error) { b2: b2, lock: &sync.RWMutex{}, } - //apply option settings + // Apply option settings for _, opt := range opts { if err = opt(c); err != nil { return nil, err @@ -117,7 +117,8 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e } var evictedKey, evictedValue interface{} - if c.b1.Contains(key) { + switch { + case c.b1.Contains(key): // Check if this value was recently evicted as part of the // recently used list // T1 set is too small, increase P appropriately @@ -144,7 +145,7 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e // Add the key to the frequently used list c.t2.Add(key, value) - } else if c.b2.Contains(key) { + case c.b2.Contains(key): // Check if this value was recently evicted as part of the // frequently used list // T2 set is too small, decrease P appropriately @@ -170,7 +171,7 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e // Add the key to the frequently used list c.t2.Add(key, value) - } else { + default: // Brand new entry // Potentially need to make room in the cache if c.t1.Len()+c.t2.Len() >= c.size { @@ -194,7 +195,7 @@ func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (e if evicted && len(evictedKeyVal) > 1 { *evictedKeyVal[1] = evictedValue } - return + return evicted } // replace is used to adaptively evict from either T1 or T2 diff --git a/expiringlru.go b/expiringlru.go index 30ac7de..3708caf 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -7,7 +7,7 @@ import ( "time" ) -//common interface shared by 2q, arc and simple LRU, used as interface of backing LRU +// common interface shared by 2q, arc and simple LRU, used as interface of backing LRU type lruCache interface { // Adds a value to the cache, returns evicted if happened and // updates the "recently used"-ness of the key. @@ -40,7 +40,7 @@ func (e entry) String() string { return fmt.Sprintf("%v,%v %v", e.key, e.val, e.expirationTime) } -//two expiration policies +// two expiration policies type expiringType byte const ( @@ -57,7 +57,7 @@ type ExpiringCache struct { expiration time.Duration expireList *expireList expireType expiringType - //placeholder for time.Now() for easier testing setup + // placeholder for time.Now() for easier testing setup timeNow func() time.Time lock RWLocker } @@ -68,7 +68,7 @@ type OptionExp func(c *ExpiringCache) error // NewExpiring2Q creates an expiring cache with specifized // size and entries lifetime duration, backed by a 2-queue LRU func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - //create a non synced LRU as backing store + // create a non synced LRU as backing store lru, err := New2Q(size, NoLock2Q) if err != nil { return @@ -80,7 +80,7 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi // NewExpiringARC creates an expiring cache with specifized // size and entries lifetime duration, backed by a ARC LRU func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - //create a non synced LRU as backing store + // create a non synced LRU as backing store lru, err := NewARC(size, NoLockARC) if err != nil { return @@ -92,7 +92,7 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // NewExpiringLRU creates an expiring cache with specifized // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - //create a non synced LRU as backing store + // create a non synced LRU as backing store lru, err := New(size, NoLock) if err != nil { return @@ -104,7 +104,7 @@ func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // Expiring will wrap an existing LRU to make its entries // expiring with specified duration func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCache, error) { - //create expiring cache with default settings + // create expiring cache with default settings elru := &ExpiringCache{ lru: lru, expiration: expir, @@ -113,7 +113,7 @@ func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCa timeNow: time.Now, lock: &sync.RWMutex{}, } - //apply options to customize + // apply options to customize for _, opt := range opts { if err := opt(elru); err != nil { return nil, err @@ -165,15 +165,15 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration var ent *entry var expired []*entry if ent0, _ := elru.lru.Peek(k); ent0 != nil { - //update existing cache entry + // update existing cache entry ent = ent0.(*entry) ent.val = v ent.expirationTime = now.Add(expiration) elru.expireList.MoveToFront(ent) } else { - //first remove 1 possible expiration to add space for new entry + // first remove 1 possible expiration to add space for new entry expired = elru.removeExpired(now, false) - //add new entry to expiration list + // add new entry to expiration list ent = &entry{ key: k, val: v, @@ -184,7 +184,7 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration // Add/Update cache entry in backing cache var evictedKey, evictedVal interface{} evicted = elru.lru.Add(k, ent, &evictedKey, &evictedVal) - //remove evicted ent from expireList + // remove evicted ent from expireList if evicted { ent = evictedVal.(*entry) evictedVal = ent.val @@ -200,7 +200,7 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration if evicted && len(evictedKeyVal) > 1 { *evictedKeyVal[1] = evictedVal } - return + return evicted } // Get returns key's value from the cache if found @@ -259,7 +259,7 @@ func (elru *ExpiringCache) Contains(k interface{}) bool { func (elru *ExpiringCache) Keys() []interface{} { elru.lock.Lock() defer elru.lock.Unlock() - //to get accurate key set, remove all expired + // to get accurate key set, remove all expired elru.removeExpired(elru.timeNow(), true) return elru.lru.Keys() } @@ -268,7 +268,7 @@ func (elru *ExpiringCache) Keys() []interface{} { func (elru *ExpiringCache) Len() int { elru.lock.Lock() defer elru.lock.Unlock() - //to get accurate size, remove all expired + // to get accurate size, remove all expired elru.removeExpired(elru.timeNow(), true) return elru.lru.Len() } @@ -281,7 +281,7 @@ func (elru *ExpiringCache) Purge() { elru.lru.Purge() } -//either remove one (the oldest expired), or all expired +// either remove one (the oldest expired), or all expired func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) (res []*entry) { res = elru.expireList.RemoveExpired(now, removeAllExpired) for i := 0; i < len(res); i++ { @@ -311,8 +311,8 @@ func (el *expireList) Init() { } func (el *expireList) PushFront(ent *entry) { - //When all operations use ExpiringCache default expiration, - //PushFront should succeed at first/front entry of list + // When all operations use ExpiringCache default expiration, + // PushFront should succeed at first/front entry of list for e := el.expList.Front(); e != nil; e = e.Next() { if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { ent.elem = el.expList.InsertBefore(ent, e) @@ -323,8 +323,8 @@ func (el *expireList) PushFront(ent *entry) { } func (el *expireList) MoveToFront(ent *entry) { - //When all operations use ExpiringCache default expiration, - //MoveToFront should succeed at first/front entry of list + // When all operations use ExpiringCache default expiration, + // MoveToFront should succeed at first/front entry of list for e := el.expList.Front(); e != nil; e = e.Next() { if !ent.expirationTime.Before(e.Value.(*entry).expirationTime) { el.expList.MoveBefore(ent.elem, e) @@ -338,14 +338,14 @@ func (el *expireList) Remove(ent *entry) interface{} { return el.expList.Remove(ent.elem) } -//either remove one (the oldest expired), or remove all expired +// either remove one (the oldest expired), or remove all expired func (el *expireList) RemoveExpired(now time.Time, removeAllExpired bool) (res []*entry) { for { back := el.expList.Back() if back == nil || back.Value.(*entry).expirationTime.After(now) { break } - //expired + // expired ent := el.expList.Remove(back).(*entry) res = append(res, ent) if !removeAllExpired { diff --git a/expiringlru_test.go b/expiringlru_test.go index 2d0ce57..2540f33 100644 --- a/expiringlru_test.go +++ b/expiringlru_test.go @@ -281,12 +281,12 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { elru.Add(i, i) } elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //next add 3,4; verify 2, 3 will be evicted + // next add 3,4; verify 2, 3 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -299,10 +299,10 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { t.Fatalf("Expiring LRU eviction failed, expected 3 entries left, but found %v", elru.Len()) } keys := elru.Keys() - //since 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are kept - //and 2,3,4 only touched once (write), so they - //moved thru "recent" list, with 2,3 evicted + // since 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are kept + // and 2,3,4 only touched once (write), so they + // moved thru "recent" list, with 2,3 evicted for i, v := range []int{0, 1, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) @@ -310,7 +310,7 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { } } -//testTimer used to simulate time-elapse for expiration tests +// testTimer used to simulate time-elapse for expiration tests type testTimer struct { t time.Time } @@ -321,7 +321,7 @@ func (tt *testTimer) Advance(d time.Duration) { tt.t = tt.t.Add(d) } // Test eviction by ExpireAfterWrite func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now)) if err != nil { @@ -330,19 +330,19 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, - //so they should be evicted, although they are more recently retrieved than <2,2> + // test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) - //next add 3,4; verify 0,1 will be evicted + // next add 3,4; verify 0,1 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -356,9 +356,9 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //althoug 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are evicted because expiration - //and 2,3,4 will be kept + // althoug 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are evicted because expiration + // and 2,3,4 will be kept for i, v := range []int{2, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) @@ -369,7 +369,7 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { // Test eviction by ExpireAfterAccess: basically same access sequence as above case // but different result because of ExpireAfterAccess func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) if err != nil { @@ -378,20 +378,20 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //also moved them to back in expire list with newer timestamp - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // also moved them to back in expire list with newer timestamp + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, none expired - //and 2 in recent list + // test timer advance another 15 seconds, none expired + // and 2 in recent list tt.Advance(15 * time.Second) - //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + // next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired for i := 3; i < 5; i++ { elru.Add(i, i) } @@ -400,7 +400,7 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //and 0,1,4 will be kept + // and 0,1,4 will be kept for i, v := range []int{0, 1, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) @@ -410,7 +410,7 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { // Test eviction by ExpireAfterWrite func TestExpiringARC_ExpireAfterWrite(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now)) if err != nil { @@ -419,19 +419,19 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, - //so they should be evicted, although they are more recently retrieved than <2,2> + // test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) - //next add 3,4; verify 0,1 will be evicted + // next add 3,4; verify 0,1 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -445,9 +445,9 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //althoug 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are evicted because expiration - //and 2,3,4 will be kept + // althoug 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are evicted because expiration + // and 2,3,4 will be kept for i, v := range []int{2, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) @@ -458,7 +458,7 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { // Test eviction by ExpireAfterAccess: basically same access sequence as above case // but different result because of ExpireAfterAccess func TestExpiringARC_ExpireAfterAccess(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) if err != nil { @@ -467,20 +467,20 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //also moved them to back in expire list with newer timestamp - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // also moved them to back in expire list with newer timestamp + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, none expired - //and 2 in recent list + // test timer advance another 15 seconds, none expired + // and 2 in recent list tt.Advance(15 * time.Second) - //next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired + // next add 3,4; verify 2,3 will be evicted, because 0,1 in freq list, not expired for i := 3; i < 5; i++ { elru.Add(i, i) } @@ -489,7 +489,7 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //and 0,1,4 will be kept + // and 0,1,4 will be kept for i, v := range []int{0, 1, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {0,1,4} left, but found %v", elru.Keys()) @@ -499,7 +499,7 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { // Test eviction by ExpireAfterWrite func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now)) if err != nil { @@ -508,19 +508,19 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to freq-used list - //2 will remain in recent-used list + // Get(0),Get(1) will move 0, 1 to freq-used list + // 2 will remain in recent-used list for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, - //so they should be evicted, although they are more recently retrieved than <2,2> + // test timer advance another 15 seconds, entries <0,0>,<1,1> timeout & expire now, + // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) - //next add 3,4; verify 0,1 will be evicted + // next add 3,4; verify 0,1 will be evicted var ek, ev interface{} for i := 3; i < 5; i++ { evicted := elru.Add(i, i, &ek, &ev) @@ -534,9 +534,9 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //althoug 0, 1 are touched twice (write & read) so - //they are in frequently used list, they are evicted because expiration - //and 2,3,4 will be kept + // althoug 0, 1 are touched twice (write & read) so + // they are in frequently used list, they are evicted because expiration + // and 2,3,4 will be kept for i, v := range []int{2, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {2,3,4} left, but found %v", elru.Keys()) @@ -547,7 +547,7 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { // Test eviction by ExpireAfterAccess: basically same access sequence as above case // but different result because of ExpireAfterAccess func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { - //use test timer for expiration + // use test timer for expiration tt := newTestTimer() elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now), ExpireAfterAccess) if err != nil { @@ -556,19 +556,19 @@ func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { for i := 0; i < 2; i++ { elru.Add(i, i) } - //test timer ticks 20 seconds + // test timer ticks 20 seconds tt.Advance(20 * time.Second) - //add fresher entry <2,2> to cache + // add fresher entry <2,2> to cache elru.Add(2, 2) - //Get(0),Get(1) will move 0, 1 to back of access list - //also moved them to back in expire list with newer timestamp - //access list will be 2,0,1 + // Get(0),Get(1) will move 0, 1 to back of access list + // also moved them to back in expire list with newer timestamp + // access list will be 2,0,1 for i := 0; i < 2; i++ { elru.Get(i) } - //test timer advance another 15 seconds, none expired + // test timer advance another 15 seconds, none expired tt.Advance(15 * time.Second) - //next add 3,4; verify 2,0 will be evicted + // next add 3,4; verify 2,0 will be evicted for i := 3; i < 5; i++ { elru.Add(i, i) } @@ -577,7 +577,7 @@ func TestExpiringLRU_ExpireAfterAccess(t *testing.T) { } keys := elru.Keys() sort.Slice(keys, func(i, j int) bool { return keys[i].(int) < keys[j].(int) }) - //and 1,3,4 will be kept + // and 1,3,4 will be kept for i, v := range []int{1, 3, 4} { if v != keys[i] { t.Fatalf("Expiring LRU eviction failed, expected keys {1,3,4} left, but found %v", elru.Keys()) diff --git a/lru.go b/lru.go index cd7f99b..7bbcab0 100644 --- a/lru.go +++ b/lru.go @@ -23,7 +23,7 @@ func New(size int, opts ...Option) (*Cache, error) { // NewWithEvict constructs a fixed size cache with the given eviction // callback. func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), opts ...Option) (*Cache, error) { - //create a cache with default settings + // create a cache with default settings lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) if err != nil { return nil, err @@ -32,7 +32,7 @@ func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), lru: lru, lock: &sync.RWMutex{}, } - //apply options for custimization + // apply options for custimization for _, opt := range opts { if err = opt(c); err != nil { return nil, err From 986e48adc43f6a7868cefe72a945fd8fd80a2933 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 10:13:00 -0800 Subject: [PATCH 03/17] add missing sync in RemoveAllExpired() --- expiringlru.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/expiringlru.go b/expiringlru.go index 3708caf..6eeb9db 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -281,6 +281,13 @@ func (elru *ExpiringCache) Purge() { elru.lru.Purge() } +// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine +func (elru *ExpiringCache) RemoveAllExpired() { + elru.lock.Lock() + defer elru.lock.Unlock() + elru.removeExpired(elru.timeNow(), true) +} + // either remove one (the oldest expired), or all expired func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) (res []*entry) { res = elru.expireList.RemoveExpired(now, removeAllExpired) @@ -290,11 +297,6 @@ func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) ( return } -// RemoveAllExpired remove all expired entries, can be called by cleanup goroutine -func (elru *ExpiringCache) RemoveAllExpired() { - elru.removeExpired(elru.timeNow(), true) -} - // oldest entries are at front of expire list type expireList struct { expList *list.List From c7c985f5f62ddcdac06ff48b4470002c378e3574 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 11:02:02 -0800 Subject: [PATCH 04/17] base expiringlru on simplelru.lru --- expiringlru.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/expiringlru.go b/expiringlru.go index 6eeb9db..f19cd50 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -5,6 +5,8 @@ import ( "fmt" "sync" "time" + + "github.com/hashicorp/golang-lru/simplelru" ) // common interface shared by 2q, arc and simple LRU, used as interface of backing LRU @@ -93,7 +95,7 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { // create a non synced LRU as backing store - lru, err := New(size, NoLock) + lru, err := simplelru.NewLRU(size, nil) if err != nil { return } From 5a93abcf4571c03d48094b8b629588ef9e41f81d Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 12:16:43 -0800 Subject: [PATCH 05/17] following same pattern of simple LRU, separate 2q/arc Cache and LRU, make XXXCache just thread-safe wrapper over XXXLRU, avoid NoLock ugliness --- 2q.go | 180 ++------------------ 2q_test.go | 133 +-------------- arc.go | 217 ++---------------------- arc_test.go | 199 +--------------------- expiringlru.go | 16 +- lru.go | 26 +-- rwlocker.go | 24 --- simplelru/2q.go | 206 +++++++++++++++++++++++ simplelru/2q_test.go | 306 ++++++++++++++++++++++++++++++++++ simplelru/arc.go | 239 ++++++++++++++++++++++++++ simplelru/arc_test.go | 377 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1173 insertions(+), 750 deletions(-) delete mode 100644 rwlocker.go create mode 100644 simplelru/2q.go create mode 100644 simplelru/2q_test.go create mode 100644 simplelru/arc.go create mode 100644 simplelru/arc_test.go diff --git a/2q.go b/2q.go index 6006ffe..fc0912a 100644 --- a/2q.go +++ b/2q.go @@ -1,22 +1,11 @@ package lru import ( - "fmt" "sync" "github.com/hashicorp/golang-lru/simplelru" ) -const ( - // Default2QRecentRatio is the ratio of the 2Q cache dedicated - // to recently added entries that have only been accessed once. - Default2QRecentRatio = 0.25 - - // Default2QGhostEntries is the default ratio of ghost - // entries kept to track entries recently evicted - Default2QGhostEntries = 0.50 -) - // TwoQueueCache is a thread-safe fixed size 2Q cache. // 2Q is an enhancement over the standard LRU cache // in that it tracks both frequently and recently used @@ -27,168 +16,47 @@ const ( // head. The ARCCache is similar, but does not require setting any // parameters. type TwoQueueCache struct { - size int - recentSize int - - recent simplelru.LRUCache - frequent simplelru.LRUCache - recentEvict simplelru.LRUCache - lock RWLocker + lru *simplelru.TwoQueueLRU + lock sync.RWMutex } -// Option2Q define option to customize TwoQueueCache -type Option2Q func(c *TwoQueueCache) error - // New2Q creates a new TwoQueueCache using the default // values for the parameters. -func New2Q(size int, opts ...Option2Q) (*TwoQueueCache, error) { - return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries, opts...) +func New2Q(size int) (*TwoQueueCache, error) { + return New2QParams(size, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) } // New2QParams creates a new TwoQueueCache using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64, opts ...Option2Q) (*TwoQueueCache, error) { - if size <= 0 { - return nil, fmt.Errorf("invalid size") - } - if recentRatio < 0.0 || recentRatio > 1.0 { - return nil, fmt.Errorf("invalid recent ratio") - } - if ghostRatio < 0.0 || ghostRatio > 1.0 { - return nil, fmt.Errorf("invalid ghost ratio") - } - - // Determine the sub-sizes - recentSize := int(float64(size) * recentRatio) - evictSize := int(float64(size) * ghostRatio) - - // Allocate the LRUs - recent, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - frequent, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - recentEvict, err := simplelru.NewLRU(evictSize, nil) +func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) { + lru, err := simplelru.New2QParams(size, recentRatio, ghostRatio) if err != nil { return nil, err } - - // Initialize the cache - c := &TwoQueueCache{ - size: size, - recentSize: recentSize, - recent: recent, - frequent: frequent, - recentEvict: recentEvict, - lock: &sync.RWMutex{}, - } - // Apply options for customization - for _, opt := range opts { - if err = opt(c); err != nil { - return nil, err - } - } - return c, nil -} - -// NoLock2Q disables locking for TwoQueueCache -func NoLock2Q(c *TwoQueueCache) error { - c.lock = NoOpRWLocker{} - return nil + return &TwoQueueCache{ + lru: lru, + }, nil } // Get looks up a key's value from the cache. func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() - - // Check if this is a frequent value - if val, ok := c.frequent.Get(key); ok { - return val, ok - } - - // If the value is contained in recent, then we - // promote it to frequent - if val, ok := c.recent.Peek(key); ok { - c.recent.Remove(key) - c.frequent.Add(key, val) - return val, ok - } - - // No hit - return nil, false + return c.lru.Get(key) } // Add adds a value to the cache, return evicted key/val if eviction happens. func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() - - // Check if the value is frequently used already, - // and just update the value - if c.frequent.Contains(key) { - c.frequent.Add(key, value) - return - } - - // Check if the value is recently used, and promote - // the value into the frequent list - if c.recent.Contains(key) { - c.recent.Remove(key) - c.frequent.Add(key, value) - return - } - - var evictedKey, evictedValue interface{} - // If the value was recently evicted, add it to the - // frequently used list - if c.recentEvict.Contains(key) { - evictedKey, evictedValue, evicted = c.ensureSpace(true) - c.recentEvict.Remove(key) - c.frequent.Add(key, value) - } else { - // Add to the recently seen list - evictedKey, evictedValue, evicted = c.ensureSpace(false) - c.recent.Add(key, value) - } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedValue - } - return evicted -} - -// ensureSpace is used to ensure we have space in the cache -func (c *TwoQueueCache) ensureSpace(recentEvict bool) (key, value interface{}, evicted bool) { - // If we have space, nothing to do - recentLen := c.recent.Len() - freqLen := c.frequent.Len() - if recentLen+freqLen < c.size { - return - } - - // If the recent buffer is larger than - // the target, evict from there - if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { - key, value, evicted = c.recent.RemoveOldest() - c.recentEvict.Add(key, nil) - return - } - - // Remove from the frequent list otherwise - return c.frequent.RemoveOldest() + return c.lru.Add(key, value, evictedKeyVal...) } // Len returns the number of items in the cache. func (c *TwoQueueCache) Len() int { c.lock.RLock() defer c.lock.RUnlock() - return c.recent.Len() + c.frequent.Len() + return c.lru.Len() } // Keys returns a slice of the keys in the cache. @@ -196,32 +64,21 @@ func (c *TwoQueueCache) Len() int { func (c *TwoQueueCache) Keys() []interface{} { c.lock.RLock() defer c.lock.RUnlock() - k1 := c.frequent.Keys() - k2 := c.recent.Keys() - return append(k1, k2...) + return c.lru.Keys() } // Remove removes the provided key from the cache. func (c *TwoQueueCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() - if c.frequent.Remove(key) { - return true - } - if c.recent.Remove(key) { - return true - } - c.recentEvict.Remove(key) - return false + return c.lru.Remove(key) } // Purge is used to completely clear the cache. func (c *TwoQueueCache) Purge() { c.lock.Lock() defer c.lock.Unlock() - c.recent.Purge() - c.frequent.Purge() - c.recentEvict.Purge() + c.lru.Purge() } // Contains is used to check if the cache contains a key @@ -229,7 +86,7 @@ func (c *TwoQueueCache) Purge() { func (c *TwoQueueCache) Contains(key interface{}) bool { c.lock.RLock() defer c.lock.RUnlock() - return c.frequent.Contains(key) || c.recent.Contains(key) + return c.lru.Contains(key) } // Peek is used to inspect the cache value of a key @@ -237,8 +94,5 @@ func (c *TwoQueueCache) Contains(key interface{}) bool { func (c *TwoQueueCache) Peek(key interface{}) (value interface{}, ok bool) { c.lock.RLock() defer c.lock.RUnlock() - if val, ok := c.frequent.Peek(key); ok { - return val, ok - } - return c.recent.Peek(key) + return c.lru.Peek(key) } diff --git a/2q_test.go b/2q_test.go index 1b0f351..32acbf1 100644 --- a/2q_test.go +++ b/2q_test.go @@ -86,140 +86,13 @@ func Test2Q_RandomOps(t *testing.T) { l.Remove(key) } - if l.recent.Len()+l.frequent.Len() > size { - t.Fatalf("bad: recent: %d freq: %d", - l.recent.Len(), l.frequent.Len()) + if l.Len() > size { + t.Fatalf("bad: expected %d, got %d", + size, l.Len()) } } } -func Test2Q_Get_RecentToFrequent(t *testing.T) { - l, err := New2Q(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Touch all the entries, should be in t1 - for i := 0; i < 128; i++ { - l.Add(i, i) - } - if n := l.recent.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Get should upgrade to t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - - // Get be from t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } -} - -func Test2Q_Add_RecentToFrequent(t *testing.T) { - l, err := New2Q(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Add initially to recent - l.Add(1, 1) - if n := l.recent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Add should upgrade to frequent - l.Add(1, 1) - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Add should remain in frequent - l.Add(1, 1) - if n := l.recent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } -} - -func Test2Q_Add_RecentEvict(t *testing.T) { - l, err := New2Q(4) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Add 1,2,3,4,5 -> Evict 1 - l.Add(1, 1) - l.Add(2, 2) - l.Add(3, 3) - l.Add(4, 4) - l.Add(5, 5) - if n := l.recent.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - if n := l.recentEvict.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Pull in the recently evicted - l.Add(1, 1) - if n := l.recent.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - if n := l.recentEvict.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Add 6, should cause another recent evict - l.Add(6, 6) - if n := l.recent.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - if n := l.recentEvict.Len(); n != 2 { - t.Fatalf("bad: %d", n) - } - if n := l.frequent.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } -} - func Test2Q(t *testing.T) { l, err := New2Q(128) if err != nil { diff --git a/arc.go b/arc.go index 0770b70..6cf5bda 100644 --- a/arc.go +++ b/arc.go @@ -15,250 +15,64 @@ import ( // with the size of the cache. ARC has been patented by IBM, but is // similar to the TwoQueueCache (2Q) which requires setting parameters. type ARCCache struct { - size int // Size is the total capacity of the cache - p int // P is the dynamic preference towards T1 or T2 - - t1 simplelru.LRUCache // T1 is the LRU for recently accessed items - b1 simplelru.LRUCache // B1 is the LRU for evictions from t1 - - t2 simplelru.LRUCache // T2 is the LRU for frequently accessed items - b2 simplelru.LRUCache // B2 is the LRU for evictions from t2 - - lock RWLocker + lru *simplelru.ARCLRU + lock sync.RWMutex } -// OptionARC defines option to customize ARCCache -type OptionARC func(*ARCCache) error - // NewARC creates an ARC of the given size -func NewARC(size int, opts ...OptionARC) (*ARCCache, error) { - // Create the sub LRUs - b1, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - b2, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - t1, err := simplelru.NewLRU(size, nil) - if err != nil { - return nil, err - } - t2, err := simplelru.NewLRU(size, nil) +func NewARC(size int) (*ARCCache, error) { + lru, err := simplelru.NewARC(size) if err != nil { return nil, err } - // Initialize the ARC c := &ARCCache{ - size: size, - p: 0, - t1: t1, - b1: b1, - t2: t2, - b2: b2, - lock: &sync.RWMutex{}, - } - // Apply option settings - for _, opt := range opts { - if err = opt(c); err != nil { - return nil, err - } + lru: lru, } - return c, nil -} -// NoLockARC disables locking for ARCCache -func NoLockARC(c *ARCCache) error { - c.lock = NoOpRWLocker{} - return nil + return c, nil } // Get looks up a key's value from the cache. func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() defer c.lock.Unlock() - - // If the value is contained in T1 (recent), then - // promote it to T2 (frequent) - if val, ok := c.t1.Peek(key); ok { - c.t1.Remove(key) - c.t2.Add(key, val) - return val, ok - } - - // Check if the value is contained in T2 (frequent) - if val, ok := c.t2.Get(key); ok { - return val, ok - } - - // No hit - return nil, false + return c.lru.Get(key) } // Add adds a value to the cache, return evicted key/val if it happens. func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { c.lock.Lock() defer c.lock.Unlock() - - // Check if the value is contained in T1 (recent), and potentially - // promote it to frequent T2 - if c.t1.Contains(key) { - c.t1.Remove(key) - c.t2.Add(key, value) - return - } - - // Check if the value is already in T2 (frequent) and update it - if c.t2.Contains(key) { - c.t2.Add(key, value) - return - } - - var evictedKey, evictedValue interface{} - switch { - case c.b1.Contains(key): - // Check if this value was recently evicted as part of the - // recently used list - // T1 set is too small, increase P appropriately - delta := 1 - b1Len := c.b1.Len() - b2Len := c.b2.Len() - if b2Len > b1Len { - delta = b2Len / b1Len - } - if c.p+delta >= c.size { - c.p = c.size - } else { - c.p += delta - } - - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - evictedKey, evictedValue, evicted = c.replace(false) - } - - // Remove from B1 - c.b1.Remove(key) - - // Add the key to the frequently used list - c.t2.Add(key, value) - - case c.b2.Contains(key): - // Check if this value was recently evicted as part of the - // frequently used list - // T2 set is too small, decrease P appropriately - delta := 1 - b1Len := c.b1.Len() - b2Len := c.b2.Len() - if b1Len > b2Len { - delta = b1Len / b2Len - } - if delta >= c.p { - c.p = 0 - } else { - c.p -= delta - } - - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - evictedKey, evictedValue, evicted = c.replace(true) - } - - // Remove from B2 - c.b2.Remove(key) - - // Add the key to the frequently used list - c.t2.Add(key, value) - default: - // Brand new entry - // Potentially need to make room in the cache - if c.t1.Len()+c.t2.Len() >= c.size { - evictedKey, evictedValue, evicted = c.replace(false) - } - - // Keep the size of the ghost buffers trim - if c.b1.Len() > c.size-c.p { - c.b1.RemoveOldest() - } - if c.b2.Len() > c.p { - c.b2.RemoveOldest() - } - - // Add to the recently seen list - c.t1.Add(key, value) - } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedValue - } - return evicted -} - -// replace is used to adaptively evict from either T1 or T2 -// based on the current learned value of P -func (c *ARCCache) replace(b2ContainsKey bool) (k, v interface{}, ok bool) { - t1Len := c.t1.Len() - if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { - k, v, ok = c.t1.RemoveOldest() - if ok { - c.b1.Add(k, nil) - } - } else { - k, v, ok = c.t2.RemoveOldest() - if ok { - c.b2.Add(k, nil) - } - } - return + return c.lru.Add(key, value, evictedKeyVal...) } // Len returns the number of cached entries func (c *ARCCache) Len() int { c.lock.RLock() defer c.lock.RUnlock() - return c.t1.Len() + c.t2.Len() + return c.lru.Len() } // Keys returns all the cached keys func (c *ARCCache) Keys() []interface{} { c.lock.RLock() defer c.lock.RUnlock() - k1 := c.t1.Keys() - k2 := c.t2.Keys() - return append(k1, k2...) + return c.lru.Keys() } // Remove is used to purge a key from the cache func (c *ARCCache) Remove(key interface{}) bool { c.lock.Lock() defer c.lock.Unlock() - if c.t1.Remove(key) { - return true - } - if c.t2.Remove(key) { - return true - } - if c.b1.Remove(key) { - return false - } - if c.b2.Remove(key) { - return false - } - return false + return c.lru.Remove(key) } // Purge is used to clear the cache func (c *ARCCache) Purge() { c.lock.Lock() defer c.lock.Unlock() - c.t1.Purge() - c.t2.Purge() - c.b1.Purge() - c.b2.Purge() + c.lru.Purge() } // Contains is used to check if the cache contains a key @@ -266,7 +80,7 @@ func (c *ARCCache) Purge() { func (c *ARCCache) Contains(key interface{}) bool { c.lock.RLock() defer c.lock.RUnlock() - return c.t1.Contains(key) || c.t2.Contains(key) + return c.lru.Contains(key) } // Peek is used to inspect the cache value of a key @@ -274,8 +88,5 @@ func (c *ARCCache) Contains(key interface{}) bool { func (c *ARCCache) Peek(key interface{}) (value interface{}, ok bool) { c.lock.RLock() defer c.lock.RUnlock() - if val, ok := c.t1.Peek(key); ok { - return val, ok - } - return c.t2.Peek(key) + return c.lru.Peek(key) } diff --git a/arc_test.go b/arc_test.go index e2d9b68..54e8582 100644 --- a/arc_test.go +++ b/arc_test.go @@ -91,204 +91,11 @@ func TestARC_RandomOps(t *testing.T) { l.Remove(key) } - if l.t1.Len()+l.t2.Len() > size { - t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", - l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) + if l.Len() > size { + t.Fatalf("bad: got size %d, expected %d", + l.Len(), size) } - if l.b1.Len()+l.b2.Len() > size { - t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", - l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) - } - } -} - -func TestARC_Get_RecentToFrequent(t *testing.T) { - l, err := NewARC(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Touch all the entries, should be in t1 - for i := 0; i < 128; i++ { - l.Add(i, i) - } - if n := l.t1.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Get should upgrade to t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } - - // Get be from t2 - for i := 0; i < 128; i++ { - _, ok := l.Get(i) - if !ok { - t.Fatalf("missing: %d", i) - } - } - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 128 { - t.Fatalf("bad: %d", n) - } -} - -func TestARC_Add_RecentToFrequent(t *testing.T) { - l, err := NewARC(128) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Add initially to t1 - l.Add(1, 1) - if n := l.t1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - - // Add should upgrade to t2 - l.Add(1, 1) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) } - if n := l.t2.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Add should remain in t2 - l.Add(1, 1) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } -} - -func TestARC_Adaptive(t *testing.T) { - l, err := NewARC(4) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Fill t1 - for i := 0; i < 4; i++ { - l.Add(i, i) - } - if n := l.t1.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - - // Move to t2 - l.Get(0) - l.Get(1) - if n := l.t2.Len(); n != 2 { - t.Fatalf("bad: %d", n) - } - - // Evict from t1 - l.Add(4, 4) - if n := l.b1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [4, 3] (LRU) - // t2 : (MRU) [1, 0] (LRU) - // b1 : (MRU) [2] (LRU) - // b2 : (MRU) [] (LRU) - - // Add 2, should cause hit on b1 - l.Add(2, 2) - if n := l.b1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if l.p != 1 { - t.Fatalf("bad: %d", l.p) - } - if n := l.t2.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [4] (LRU) - // t2 : (MRU) [2, 1, 0] (LRU) - // b1 : (MRU) [3] (LRU) - // b2 : (MRU) [] (LRU) - - // Add 4, should migrate to t2 - l.Add(4, 4) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [] (LRU) - // t2 : (MRU) [4, 2, 1, 0] (LRU) - // b1 : (MRU) [3] (LRU) - // b2 : (MRU) [] (LRU) - - // Add 4, should evict to b2 - l.Add(5, 5) - if n := l.t1.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 3 { - t.Fatalf("bad: %d", n) - } - if n := l.b2.Len(); n != 1 { - t.Fatalf("bad: %d", n) - } - - // Current state - // t1 : (MRU) [5] (LRU) - // t2 : (MRU) [4, 2, 1] (LRU) - // b1 : (MRU) [3] (LRU) - // b2 : (MRU) [0] (LRU) - - // Add 0, should decrease p - l.Add(0, 0) - if n := l.t1.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if n := l.t2.Len(); n != 4 { - t.Fatalf("bad: %d", n) - } - if n := l.b1.Len(); n != 2 { - t.Fatalf("bad: %d", n) - } - if n := l.b2.Len(); n != 0 { - t.Fatalf("bad: %d", n) - } - if l.p != 0 { - t.Fatalf("bad: %d", l.p) - } - - // Current state - // t1 : (MRU) [] (LRU) - // t2 : (MRU) [0, 4, 2, 1] (LRU) - // b1 : (MRU) [5, 3] (LRU) - // b2 : (MRU) [0] (LRU) } func TestARC(t *testing.T) { diff --git a/expiringlru.go b/expiringlru.go index f19cd50..7836709 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -61,7 +61,7 @@ type ExpiringCache struct { expireType expiringType // placeholder for time.Now() for easier testing setup timeNow func() time.Time - lock RWLocker + lock sync.RWMutex } // OptionExp defines option to customize ExpiringCache @@ -70,8 +70,7 @@ type OptionExp func(c *ExpiringCache) error // NewExpiring2Q creates an expiring cache with specifized // size and entries lifetime duration, backed by a 2-queue LRU func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create a non synced LRU as backing store - lru, err := New2Q(size, NoLock2Q) + lru, err := simplelru.New2Q(size) if err != nil { return } @@ -82,8 +81,7 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi // NewExpiringARC creates an expiring cache with specifized // size and entries lifetime duration, backed by a ARC LRU func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create a non synced LRU as backing store - lru, err := NewARC(size, NoLockARC) + lru, err := simplelru.NewARC(size) if err != nil { return } @@ -94,7 +92,6 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp // NewExpiringLRU creates an expiring cache with specifized // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create a non synced LRU as backing store lru, err := simplelru.NewLRU(size, nil) if err != nil { return @@ -113,7 +110,6 @@ func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCa expireList: newExpireList(), expireType: expireAfterWrite, timeNow: time.Now, - lock: &sync.RWMutex{}, } // apply options to customize for _, opt := range opts { @@ -124,12 +120,6 @@ func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCa return elru, nil } -// NoLockExp disables locking for ExpiringCache -func NoLockExp(elru *ExpiringCache) error { - elru.lock = NoOpRWLocker{} - return nil -} - // ExpireAfterWrite sets expiring policy func ExpireAfterWrite(elru *ExpiringCache) error { elru.expireType = expireAfterWrite diff --git a/lru.go b/lru.go index 7bbcab0..ee81bf6 100644 --- a/lru.go +++ b/lru.go @@ -9,44 +9,28 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { lru simplelru.LRUCache - lock RWLocker + lock sync.RWMutex } -// Option to customize LRUCache -type Option func(*Cache) error - // New creates an LRU of the given size. -func New(size int, opts ...Option) (*Cache, error) { - return NewWithEvict(size, nil, opts...) +func New(size int) (*Cache, error) { + return NewWithEvict(size, nil) } // NewWithEvict constructs a fixed size cache with the given eviction // callback. -func NewWithEvict(size int, onEvicted func(key interface{}, value interface{}), opts ...Option) (*Cache, error) { +func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { // create a cache with default settings lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) if err != nil { return nil, err } c := &Cache{ - lru: lru, - lock: &sync.RWMutex{}, - } - // apply options for custimization - for _, opt := range opts { - if err = opt(c); err != nil { - return nil, err - } + lru: lru, } return c, nil } -// NoLock disables locking for LRUCache -func NoLock(c *Cache) error { - c.lock = NoOpRWLocker{} - return nil -} - // Purge is used to completely clear the cache. func (c *Cache) Purge() { c.lock.Lock() diff --git a/rwlocker.go b/rwlocker.go deleted file mode 100644 index 58012f8..0000000 --- a/rwlocker.go +++ /dev/null @@ -1,24 +0,0 @@ -package lru - -// RWLocker define base interface of sync.RWMutex -type RWLocker interface { - Lock() - Unlock() - RLock() - RUnlock() -} - -// NoOpRWLocker is a dummy noop implementation of RWLocker interface -type NoOpRWLocker struct{} - -// Lock perform noop Lock() operation -func (nop NoOpRWLocker) Lock() {} - -// Unlock perform noop Unlock() operation -func (nop NoOpRWLocker) Unlock() {} - -// RLock perform noop RLock() operation -func (nop NoOpRWLocker) RLock() {} - -// RUnlock perform noop RUnlock() operation -func (nop NoOpRWLocker) RUnlock() {} diff --git a/simplelru/2q.go b/simplelru/2q.go new file mode 100644 index 0000000..7f5111b --- /dev/null +++ b/simplelru/2q.go @@ -0,0 +1,206 @@ +package simplelru + +import ( + "fmt" +) + +const ( + // Default2QRecentRatio is the ratio of the 2Q cache dedicated + // to recently added entries that have only been accessed once. + Default2QRecentRatio = 0.25 + + // Default2QGhostEntries is the default ratio of ghost + // entries kept to track entries recently evicted + Default2QGhostEntries = 0.50 +) + +// TwoQueueLRU is a thread-safe fixed size 2Q LRU. +// 2Q is an enhancement over the standard LRU cache +// in that it tracks both frequently and recently used +// entries separately. This avoids a burst in access to new +// entries from evicting frequently used entries. It adds some +// additional tracking overhead to the standard LRU cache, and is +// computationally about 2x the cost, and adds some metadata over +// head. The ARCCache is similar, but does not require setting any +// parameters. +type TwoQueueLRU struct { + size int + recentSize int + + recent LRUCache + frequent LRUCache + recentEvict LRUCache +} + +// New2Q creates a new TwoQueueLRU using the default +// values for the parameters. +func New2Q(size int) (*TwoQueueLRU, error) { + return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries) +} + +// New2QParams creates a new TwoQueueLRU using the provided +// parameter values. +func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueLRU, error) { + if size <= 0 { + return nil, fmt.Errorf("invalid size") + } + if recentRatio < 0.0 || recentRatio > 1.0 { + return nil, fmt.Errorf("invalid recent ratio") + } + if ghostRatio < 0.0 || ghostRatio > 1.0 { + return nil, fmt.Errorf("invalid ghost ratio") + } + + // Determine the sub-sizes + recentSize := int(float64(size) * recentRatio) + evictSize := int(float64(size) * ghostRatio) + + // Allocate the LRUs + recent, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + frequent, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + recentEvict, err := NewLRU(evictSize, nil) + if err != nil { + return nil, err + } + + // Initialize the cache + c := &TwoQueueLRU{ + size: size, + recentSize: recentSize, + recent: recent, + frequent: frequent, + recentEvict: recentEvict, + } + return c, nil +} + +// Get looks up a key's value from the cache. +func (c *TwoQueueLRU) Get(key interface{}) (value interface{}, ok bool) { + // Check if this is a frequent value + if val, ok := c.frequent.Get(key); ok { + return val, ok + } + + // If the value is contained in recent, then we + // promote it to frequent + if val, ok := c.recent.Peek(key); ok { + c.recent.Remove(key) + c.frequent.Add(key, val) + return val, ok + } + + // No hit + return nil, false +} + +// Add adds a value to the cache, return evicted key/val if eviction happens. +func (c *TwoQueueLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { + // Check if the value is frequently used already, + // and just update the value + if c.frequent.Contains(key) { + c.frequent.Add(key, value) + return + } + + // Check if the value is recently used, and promote + // the value into the frequent list + if c.recent.Contains(key) { + c.recent.Remove(key) + c.frequent.Add(key, value) + return + } + + var evictedKey, evictedValue interface{} + // If the value was recently evicted, add it to the + // frequently used list + if c.recentEvict.Contains(key) { + evictedKey, evictedValue, evicted = c.ensureSpace(true) + c.recentEvict.Remove(key) + c.frequent.Add(key, value) + } else { + // Add to the recently seen list + evictedKey, evictedValue, evicted = c.ensureSpace(false) + c.recent.Add(key, value) + } + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return evicted +} + +// ensureSpace is used to ensure we have space in the cache +func (c *TwoQueueLRU) ensureSpace(recentEvict bool) (key, value interface{}, evicted bool) { + // If we have space, nothing to do + recentLen := c.recent.Len() + freqLen := c.frequent.Len() + if recentLen+freqLen < c.size { + return + } + + // If the recent buffer is larger than + // the target, evict from there + if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { + key, value, evicted = c.recent.RemoveOldest() + c.recentEvict.Add(key, nil) + return + } + + // Remove from the frequent list otherwise + return c.frequent.RemoveOldest() +} + +// Len returns the number of items in the cache. +func (c *TwoQueueLRU) Len() int { + return c.recent.Len() + c.frequent.Len() +} + +// Keys returns a slice of the keys in the cache. +// The frequently used keys are first in the returned slice. +func (c *TwoQueueLRU) Keys() []interface{} { + k1 := c.frequent.Keys() + k2 := c.recent.Keys() + return append(k1, k2...) +} + +// Remove removes the provided key from the cache. +func (c *TwoQueueLRU) Remove(key interface{}) bool { + if c.frequent.Remove(key) { + return true + } + if c.recent.Remove(key) { + return true + } + c.recentEvict.Remove(key) + return false +} + +// Purge is used to completely clear the cache. +func (c *TwoQueueLRU) Purge() { + c.recent.Purge() + c.frequent.Purge() + c.recentEvict.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (c *TwoQueueLRU) Contains(key interface{}) bool { + return c.frequent.Contains(key) || c.recent.Contains(key) +} + +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (c *TwoQueueLRU) Peek(key interface{}) (value interface{}, ok bool) { + if val, ok := c.frequent.Peek(key); ok { + return val, ok + } + return c.recent.Peek(key) +} diff --git a/simplelru/2q_test.go b/simplelru/2q_test.go new file mode 100644 index 0000000..6ba575b --- /dev/null +++ b/simplelru/2q_test.go @@ -0,0 +1,306 @@ +package simplelru + +import ( + "math/rand" + "testing" +) + +func Benchmark2Q_Rand(b *testing.B) { + l, err := New2Q(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func Benchmark2Q_Freq(b *testing.B) { + l, err := New2Q(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func Test2Q_RandomOps(t *testing.T) { + size := 128 + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.recent.Len()+l.frequent.Len() > size { + t.Fatalf("bad: recent: %d freq: %d", + l.recent.Len(), l.frequent.Len()) + } + } +} + +func Test2Q_Get_RecentToFrequent(t *testing.T) { + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Touch all the entries, should be in t1 + for i := 0; i < 128; i++ { + l.Add(i, i) + } + if n := l.recent.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Get should upgrade to t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + + // Get be from t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } +} + +func Test2Q_Add_RecentToFrequent(t *testing.T) { + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Add initially to recent + l.Add(1, 1) + if n := l.recent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Add should upgrade to frequent + l.Add(1, 1) + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Add should remain in frequent + l.Add(1, 1) + if n := l.recent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } +} + +func Test2Q_Add_RecentEvict(t *testing.T) { + l, err := New2Q(4) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Add 1,2,3,4,5 -> Evict 1 + l.Add(1, 1) + l.Add(2, 2) + l.Add(3, 3) + l.Add(4, 4) + l.Add(5, 5) + if n := l.recent.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + if n := l.recentEvict.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Pull in the recently evicted + l.Add(1, 1) + if n := l.recent.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + if n := l.recentEvict.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Add 6, should cause another recent evict + l.Add(6, 6) + if n := l.recent.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + if n := l.recentEvict.Len(); n != 2 { + t.Fatalf("bad: %d", n) + } + if n := l.frequent.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } +} + +func Test2Q(t *testing.T) { + l, err := New2Q(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + _, ok := l.Get(i) + if ok { + t.Fatalf("should be deleted") + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// Test that Contains doesn't update recent-ness +func Test2Q_Contains(t *testing.T) { + l, err := New2Q(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func Test2Q_Peek(t *testing.T) { + l, err := New2Q(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} diff --git a/simplelru/arc.go b/simplelru/arc.go new file mode 100644 index 0000000..04e188c --- /dev/null +++ b/simplelru/arc.go @@ -0,0 +1,239 @@ +package simplelru + +// ARCLRU is a thread-safe fixed size Adaptive Replacement Cache LRU (ARC). +// ARC is an enhancement over the standard LRU cache in that tracks both +// frequency and recency of use. This avoids a burst in access to new +// entries from evicting the frequently used older entries. It adds some +// additional tracking overhead to a standard LRU cache, computationally +// it is roughly 2x the cost, and the extra memory overhead is linear +// with the size of the cache. ARC has been patented by IBM, but is +// similar to the TwoQueueCache (2Q) which requires setting parameters. +type ARCLRU struct { + size int // Size is the total capacity of the cache + p int // P is the dynamic preference towards T1 or T2 + + t1 LRUCache // T1 is the LRU for recently accessed items + b1 LRUCache // B1 is the LRU for evictions from t1 + + t2 LRUCache // T2 is the LRU for frequently accessed items + b2 LRUCache // B2 is the LRU for evictions from t2 +} + +// NewARC creates an ARC of the given size +func NewARC(size int) (*ARCLRU, error) { + // Create the sub LRUs + b1, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + b2, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + t1, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + t2, err := NewLRU(size, nil) + if err != nil { + return nil, err + } + + // Initialize the ARC + c := &ARCLRU{ + size: size, + p: 0, + t1: t1, + b1: b1, + t2: t2, + b2: b2, + } + return c, nil +} + +// Get looks up a key's value from the cache. +func (c *ARCLRU) Get(key interface{}) (value interface{}, ok bool) { + // If the value is contained in T1 (recent), then + // promote it to T2 (frequent) + if val, ok := c.t1.Peek(key); ok { + c.t1.Remove(key) + c.t2.Add(key, val) + return val, ok + } + + // Check if the value is contained in T2 (frequent) + if val, ok := c.t2.Get(key); ok { + return val, ok + } + + // No hit + return nil, false +} + +// Add adds a value to the cache, return evicted key/val if it happens. +func (c *ARCLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { + // Check if the value is contained in T1 (recent), and potentially + // promote it to frequent T2 + if c.t1.Contains(key) { + c.t1.Remove(key) + c.t2.Add(key, value) + return + } + + // Check if the value is already in T2 (frequent) and update it + if c.t2.Contains(key) { + c.t2.Add(key, value) + return + } + + var evictedKey, evictedValue interface{} + switch { + case c.b1.Contains(key): + // Check if this value was recently evicted as part of the + // recently used list + // T1 set is too small, increase P appropriately + delta := 1 + b1Len := c.b1.Len() + b2Len := c.b2.Len() + if b2Len > b1Len { + delta = b2Len / b1Len + } + if c.p+delta >= c.size { + c.p = c.size + } else { + c.p += delta + } + + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + evictedKey, evictedValue, evicted = c.replace(false) + } + + // Remove from B1 + c.b1.Remove(key) + + // Add the key to the frequently used list + c.t2.Add(key, value) + + case c.b2.Contains(key): + // Check if this value was recently evicted as part of the + // frequently used list + // T2 set is too small, decrease P appropriately + delta := 1 + b1Len := c.b1.Len() + b2Len := c.b2.Len() + if b1Len > b2Len { + delta = b1Len / b2Len + } + if delta >= c.p { + c.p = 0 + } else { + c.p -= delta + } + + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + evictedKey, evictedValue, evicted = c.replace(true) + } + + // Remove from B2 + c.b2.Remove(key) + + // Add the key to the frequently used list + c.t2.Add(key, value) + default: + // Brand new entry + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + evictedKey, evictedValue, evicted = c.replace(false) + } + + // Keep the size of the ghost buffers trim + if c.b1.Len() > c.size-c.p { + c.b1.RemoveOldest() + } + if c.b2.Len() > c.p { + c.b2.RemoveOldest() + } + + // Add to the recently seen list + c.t1.Add(key, value) + } + if evicted && len(evictedKeyVal) > 0 { + *evictedKeyVal[0] = evictedKey + } + if evicted && len(evictedKeyVal) > 1 { + *evictedKeyVal[1] = evictedValue + } + return evicted +} + +// replace is used to adaptively evict from either T1 or T2 +// based on the current learned value of P +func (c *ARCLRU) replace(b2ContainsKey bool) (k, v interface{}, ok bool) { + t1Len := c.t1.Len() + if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { + k, v, ok = c.t1.RemoveOldest() + if ok { + c.b1.Add(k, nil) + } + } else { + k, v, ok = c.t2.RemoveOldest() + if ok { + c.b2.Add(k, nil) + } + } + return +} + +// Len returns the number of cached entries +func (c *ARCLRU) Len() int { + return c.t1.Len() + c.t2.Len() +} + +// Keys returns all the cached keys +func (c *ARCLRU) Keys() []interface{} { + k1 := c.t1.Keys() + k2 := c.t2.Keys() + return append(k1, k2...) +} + +// Remove is used to purge a key from the cache +func (c *ARCLRU) Remove(key interface{}) bool { + if c.t1.Remove(key) { + return true + } + if c.t2.Remove(key) { + return true + } + if c.b1.Remove(key) { + return false + } + if c.b2.Remove(key) { + return false + } + return false +} + +// Purge is used to clear the cache +func (c *ARCLRU) Purge() { + c.t1.Purge() + c.t2.Purge() + c.b1.Purge() + c.b2.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (c *ARCLRU) Contains(key interface{}) bool { + return c.t1.Contains(key) || c.t2.Contains(key) +} + +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (c *ARCLRU) Peek(key interface{}) (value interface{}, ok bool) { + if val, ok := c.t1.Peek(key); ok { + return val, ok + } + return c.t2.Peek(key) +} diff --git a/simplelru/arc_test.go b/simplelru/arc_test.go new file mode 100644 index 0000000..363b2f8 --- /dev/null +++ b/simplelru/arc_test.go @@ -0,0 +1,377 @@ +package simplelru + +import ( + "math/rand" + "testing" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +func BenchmarkARC_Rand(b *testing.B) { + l, err := NewARC(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = rand.Int63() % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func BenchmarkARC_Freq(b *testing.B) { + l, err := NewARC(8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = rand.Int63() % 16384 + } else { + trace[i] = rand.Int63() % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + _, ok := l.Get(trace[i]) + if ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(miss)) +} + +func TestARC_RandomOps(t *testing.T) { + size := 128 + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + n := 200000 + for i := 0; i < n; i++ { + key := rand.Int63() % 512 + r := rand.Int63() + switch r % 3 { + case 0: + l.Add(key, key) + case 1: + l.Get(key) + case 2: + l.Remove(key) + } + + if l.t1.Len()+l.t2.Len() > size { + t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", + l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) + } + if l.b1.Len()+l.b2.Len() > size { + t.Fatalf("bad: t1: %d t2: %d b1: %d b2: %d p: %d", + l.t1.Len(), l.t2.Len(), l.b1.Len(), l.b2.Len(), l.p) + } + } +} + +func TestARC_Get_RecentToFrequent(t *testing.T) { + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Touch all the entries, should be in t1 + for i := 0; i < 128; i++ { + l.Add(i, i) + } + if n := l.t1.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Get should upgrade to t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } + + // Get be from t2 + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("missing: %d", i) + } + } + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 128 { + t.Fatalf("bad: %d", n) + } +} + +func TestARC_Add_RecentToFrequent(t *testing.T) { + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Add initially to t1 + l.Add(1, 1) + if n := l.t1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + + // Add should upgrade to t2 + l.Add(1, 1) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Add should remain in t2 + l.Add(1, 1) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } +} + +func TestARC_Adaptive(t *testing.T) { + l, err := NewARC(4) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Fill t1 + for i := 0; i < 4; i++ { + l.Add(i, i) + } + if n := l.t1.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + + // Move to t2 + l.Get(0) + l.Get(1) + if n := l.t2.Len(); n != 2 { + t.Fatalf("bad: %d", n) + } + + // Evict from t1 + l.Add(4, 4) + if n := l.b1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [4, 3] (LRU) + // t2 : (MRU) [1, 0] (LRU) + // b1 : (MRU) [2] (LRU) + // b2 : (MRU) [] (LRU) + + // Add 2, should cause hit on b1 + l.Add(2, 2) + if n := l.b1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if l.p != 1 { + t.Fatalf("bad: %d", l.p) + } + if n := l.t2.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [4] (LRU) + // t2 : (MRU) [2, 1, 0] (LRU) + // b1 : (MRU) [3] (LRU) + // b2 : (MRU) [] (LRU) + + // Add 4, should migrate to t2 + l.Add(4, 4) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [] (LRU) + // t2 : (MRU) [4, 2, 1, 0] (LRU) + // b1 : (MRU) [3] (LRU) + // b2 : (MRU) [] (LRU) + + // Add 4, should evict to b2 + l.Add(5, 5) + if n := l.t1.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 3 { + t.Fatalf("bad: %d", n) + } + if n := l.b2.Len(); n != 1 { + t.Fatalf("bad: %d", n) + } + + // Current state + // t1 : (MRU) [5] (LRU) + // t2 : (MRU) [4, 2, 1] (LRU) + // b1 : (MRU) [3] (LRU) + // b2 : (MRU) [0] (LRU) + + // Add 0, should decrease p + l.Add(0, 0) + if n := l.t1.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if n := l.t2.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + if n := l.b1.Len(); n != 2 { + t.Fatalf("bad: %d", n) + } + if n := l.b2.Len(); n != 0 { + t.Fatalf("bad: %d", n) + } + if l.p != 0 { + t.Fatalf("bad: %d", l.p) + } + + // Current state + // t1 : (MRU) [] (LRU) + // t2 : (MRU) [0, 4, 2, 1] (LRU) + // b1 : (MRU) [5, 3] (LRU) + // b2 : (MRU) [0] (LRU) +} + +func TestARC(t *testing.T) { + l, err := NewARC(128) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i := 0; i < 128; i++ { + _, ok := l.Get(i) + if ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + _, ok := l.Get(i) + if !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + _, ok := l.Get(i) + if ok { + t.Fatalf("should be deleted") + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// Test that Contains doesn't update recent-ness +func TestARC_Contains(t *testing.T) { + l, err := NewARC(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func TestARC_Peek(t *testing.T) { + l, err := NewARC(2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} From 410f6d2fc51b59b59592c7436652a6902667ad45 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 12:45:59 -0800 Subject: [PATCH 06/17] remove unnecessary interface redirection --- lru.go | 2 +- simplelru/2q.go | 6 +++--- simplelru/arc.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lru.go b/lru.go index ee81bf6..44394ce 100644 --- a/lru.go +++ b/lru.go @@ -8,7 +8,7 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { - lru simplelru.LRUCache + lru *simplelru.LRU lock sync.RWMutex } diff --git a/simplelru/2q.go b/simplelru/2q.go index 7f5111b..3c0a7f5 100644 --- a/simplelru/2q.go +++ b/simplelru/2q.go @@ -27,9 +27,9 @@ type TwoQueueLRU struct { size int recentSize int - recent LRUCache - frequent LRUCache - recentEvict LRUCache + recent *LRU + frequent *LRU + recentEvict *LRU } // New2Q creates a new TwoQueueLRU using the default diff --git a/simplelru/arc.go b/simplelru/arc.go index 04e188c..72b9057 100644 --- a/simplelru/arc.go +++ b/simplelru/arc.go @@ -12,11 +12,11 @@ type ARCLRU struct { size int // Size is the total capacity of the cache p int // P is the dynamic preference towards T1 or T2 - t1 LRUCache // T1 is the LRU for recently accessed items - b1 LRUCache // B1 is the LRU for evictions from t1 + t1 *LRU // T1 is the LRU for recently accessed items + b1 *LRU // B1 is the LRU for evictions from t1 - t2 LRUCache // T2 is the LRU for frequently accessed items - b2 LRUCache // B2 is the LRU for evictions from t2 + t2 *LRU // T2 is the LRU for frequently accessed items + b2 *LRU // B2 is the LRU for evictions from t2 } // NewARC creates an ARC of the given size From 64aed772274bc683b1590a02ab9c62055d06048e Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 16:46:41 -0800 Subject: [PATCH 07/17] cleanup code and revert 2q/arc to use lru interface --- expiringlru.go | 27 +++++++++++++++++---------- simplelru/2q.go | 6 +++--- simplelru/arc.go | 8 ++++---- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/expiringlru.go b/expiringlru.go index 7836709..d0d6a43 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -55,6 +55,7 @@ const ( // expireAfterAccess and expireAfterWrite (default) // Internally keep a expireList sorted by entries' expirationTime type ExpiringCache struct { + size int lru lruCache expiration time.Duration expireList *expireList @@ -334,16 +335,22 @@ func (el *expireList) Remove(ent *entry) interface{} { // either remove one (the oldest expired), or remove all expired func (el *expireList) RemoveExpired(now time.Time, removeAllExpired bool) (res []*entry) { - for { - back := el.expList.Back() - if back == nil || back.Value.(*entry).expirationTime.After(now) { - break - } - // expired - ent := el.expList.Remove(back).(*entry) - res = append(res, ent) - if !removeAllExpired { - break + back := el.expList.Back() + if back == nil || back.Value.(*entry).expirationTime.After(now) { + return + } + // expired + ent := el.expList.Remove(back).(*entry) + res = append(res, ent) + if removeAllExpired { + for { + back = el.expList.Back() + if back == nil || back.Value.(*entry).expirationTime.After(now) { + break + } + // expired + ent := el.expList.Remove(back).(*entry) + res = append(res, ent) } } return diff --git a/simplelru/2q.go b/simplelru/2q.go index 3c0a7f5..7f5111b 100644 --- a/simplelru/2q.go +++ b/simplelru/2q.go @@ -27,9 +27,9 @@ type TwoQueueLRU struct { size int recentSize int - recent *LRU - frequent *LRU - recentEvict *LRU + recent LRUCache + frequent LRUCache + recentEvict LRUCache } // New2Q creates a new TwoQueueLRU using the default diff --git a/simplelru/arc.go b/simplelru/arc.go index 72b9057..04e188c 100644 --- a/simplelru/arc.go +++ b/simplelru/arc.go @@ -12,11 +12,11 @@ type ARCLRU struct { size int // Size is the total capacity of the cache p int // P is the dynamic preference towards T1 or T2 - t1 *LRU // T1 is the LRU for recently accessed items - b1 *LRU // B1 is the LRU for evictions from t1 + t1 LRUCache // T1 is the LRU for recently accessed items + b1 LRUCache // B1 is the LRU for evictions from t1 - t2 *LRU // T2 is the LRU for frequently accessed items - b2 *LRU // B2 is the LRU for evictions from t2 + t2 LRUCache // T2 is the LRU for frequently accessed items + b2 LRUCache // B2 is the LRU for evictions from t2 } // NewARC creates an ARC of the given size From 3204fcefa37d12c223328ef8a4708cd8023188c5 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 22 Nov 2020 16:49:24 -0800 Subject: [PATCH 08/17] fix lint issue --- expiringlru.go | 1 - 1 file changed, 1 deletion(-) diff --git a/expiringlru.go b/expiringlru.go index d0d6a43..e014ca8 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -55,7 +55,6 @@ const ( // expireAfterAccess and expireAfterWrite (default) // Internally keep a expireList sorted by entries' expirationTime type ExpiringCache struct { - size int lru lruCache expiration time.Duration expireList *expireList From 20c1bf0e3579a8f38c6f2b956d69d384fffd0f42 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Wed, 2 Dec 2020 20:42:37 -0800 Subject: [PATCH 09/17] based on review feedback, change RemoveAllExpired() to return expired key/values --- expiringlru.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/expiringlru.go b/expiringlru.go index e014ca8..2fa402c 100644 --- a/expiringlru.go +++ b/expiringlru.go @@ -274,10 +274,15 @@ func (elru *ExpiringCache) Purge() { } // RemoveAllExpired remove all expired entries, can be called by cleanup goroutine -func (elru *ExpiringCache) RemoveAllExpired() { +func (elru *ExpiringCache) RemoveAllExpired() (keys []interface{}, vals []interface{}) { elru.lock.Lock() defer elru.lock.Unlock() - elru.removeExpired(elru.timeNow(), true) + ents := elru.removeExpired(elru.timeNow(), true) + for _, ent := range ents { + keys = append(keys, ent.key) + vals = append(vals, ent.val) + } + return } // either remove one (the oldest expired), or all expired From f5f33c3f415faf5d98aaffa0242d625636e91bc3 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Wed, 2 Dec 2020 21:41:24 -0800 Subject: [PATCH 10/17] make file names more consistent --- expiringlru.go => expiring.go | 0 expiringlru_test.go => expiring_test.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename expiringlru.go => expiring.go (100%) rename expiringlru_test.go => expiring_test.go (100%) diff --git a/expiringlru.go b/expiring.go similarity index 100% rename from expiringlru.go rename to expiring.go diff --git a/expiringlru_test.go b/expiring_test.go similarity index 100% rename from expiringlru_test.go rename to expiring_test.go From adf322a430d65d1878cab3a4fc8a9072b8815671 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sat, 5 Dec 2020 00:55:09 -0800 Subject: [PATCH 11/17] update code to pass evicted key/val thru registered eviction callback; no api change --- 2q.go | 73 +++++++++++---- arc.go | 68 ++++++++++---- expiring.go | 183 ++++++++++++++++++++++++------------- lru.go | 64 +++++++++---- simplelru/2q.go | 50 +++++++--- simplelru/arc.go | 66 ++++++++----- simplelru/lru.go | 13 +-- simplelru/lru_interface.go | 11 ++- 8 files changed, 358 insertions(+), 170 deletions(-) diff --git a/2q.go b/2q.go index fc0912a..8a0ab2c 100644 --- a/2q.go +++ b/2q.go @@ -16,26 +16,46 @@ import ( // head. The ARCCache is similar, but does not require setting any // parameters. type TwoQueueCache struct { - lru *simplelru.TwoQueueLRU - lock sync.RWMutex + lru *simplelru.TwoQueueLRU + evictedKey, evictedVal interface{} + onEvictedCB func(k, v interface{}) + lock sync.RWMutex } // New2Q creates a new TwoQueueCache using the default // values for the parameters. func New2Q(size int) (*TwoQueueCache, error) { - return New2QParams(size, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) + return New2QParams(size, nil, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) +} + +func New2QWithEvict(size int, onEvict func(k, v interface{})) (*TwoQueueCache, error) { + return New2QParams(size, onEvict, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) } // New2QParams creates a new TwoQueueCache using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) { - lru, err := simplelru.New2QParams(size, recentRatio, ghostRatio) - if err != nil { - return nil, err +func New2QParams(size int, onEvict func(k, v interface{}), recentRatio, ghostRatio float64) (c *TwoQueueCache, err error) { + c = &TwoQueueCache{onEvictedCB: onEvict} + if onEvict != nil { + onEvict = c.onEvicted + } + c.lru, err = simplelru.New2QParams(size, onEvict, recentRatio, ghostRatio) + return +} + +//evicted key/val will be buffered and sent thru callback outside of critical section +func (c *TwoQueueCache) onEvicted(k, v interface{}) { + c.evictedKey = k + c.evictedVal = v +} + +//invoke callback outside of critical section to avoid dead-lock +func (c *TwoQueueCache) sendEvicted() { + if c.onEvictedCB != nil { + c.onEvictedCB(c.evictedKey, c.evictedVal) + c.evictedKey = nil + c.evictedVal = nil } - return &TwoQueueCache{ - lru: lru, - }, nil } // Get looks up a key's value from the cache. @@ -45,11 +65,13 @@ func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { return c.lru.Get(key) } -// Add adds a value to the cache, return evicted key/val if eviction happens. -func (c *TwoQueueCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { +// Add adds a value to the cache, return true if eviction happens. +func (c *TwoQueueCache) Add(key, value interface{}) (evicted bool) { c.lock.Lock() - defer c.lock.Unlock() - return c.lru.Add(key, value, evictedKeyVal...) + evicted = c.lru.Add(key, value) + c.lock.Unlock() + c.sendEvicted() + return } // Len returns the number of items in the cache. @@ -68,17 +90,32 @@ func (c *TwoQueueCache) Keys() []interface{} { } // Remove removes the provided key from the cache. -func (c *TwoQueueCache) Remove(key interface{}) bool { +func (c *TwoQueueCache) Remove(key interface{}) (ok bool) { c.lock.Lock() - defer c.lock.Unlock() - return c.lru.Remove(key) + ok = c.lru.Remove(key) + c.lock.Unlock() + c.sendEvicted() + return } // Purge is used to completely clear the cache. func (c *TwoQueueCache) Purge() { + var keys, vals []interface{} c.lock.Lock() - defer c.lock.Unlock() + if c.onEvicted != nil { + keys = c.lru.Keys() + for _, k := range keys { + val, _ := c.lru.Peek(k) + vals = append(vals, val) + } + } c.lru.Purge() + c.lock.Unlock() + if c.onEvicted != nil { + for i := 0; i < len(keys); i++ { + c.onEvicted(keys[i], vals[i]) + } + } } // Contains is used to check if the cache contains a key diff --git a/arc.go b/arc.go index 6cf5bda..8f3c40d 100644 --- a/arc.go +++ b/arc.go @@ -15,22 +15,38 @@ import ( // with the size of the cache. ARC has been patented by IBM, but is // similar to the TwoQueueCache (2Q) which requires setting parameters. type ARCCache struct { - lru *simplelru.ARCLRU - lock sync.RWMutex + lru *simplelru.ARCLRU + evictedKey, evictedVal interface{} + onEvictedCB func(k, v interface{}) + lock sync.RWMutex } // NewARC creates an ARC of the given size func NewARC(size int) (*ARCCache, error) { - lru, err := simplelru.NewARC(size) - if err != nil { - return nil, err - } - // Initialize the ARC - c := &ARCCache{ - lru: lru, + return NewARCWithEvict(size, nil) +} +func NewARCWithEvict(size int, onEvict func(k, v interface{})) (c *ARCCache, err error) { + c = &ARCCache{onEvictedCB: onEvict} + if onEvict != nil { + onEvict = c.onEvicted } + c.lru, err = simplelru.NewARCWithEvict(size, onEvict) + return +} - return c, nil +//evicted key/val will be buffered and sent thru callback outside of critical section +func (c *ARCCache) onEvicted(k, v interface{}) { + c.evictedKey = k + c.evictedVal = v +} + +//invoke callback outside of critical section to avoid dead-lock +func (c *ARCCache) sendEvicted() { + if c.onEvictedCB != nil { + c.onEvictedCB(c.evictedKey, c.evictedVal) + c.evictedKey = nil + c.evictedVal = nil + } } // Get looks up a key's value from the cache. @@ -41,10 +57,12 @@ func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { } // Add adds a value to the cache, return evicted key/val if it happens. -func (c *ARCCache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { +func (c *ARCCache) Add(key, value interface{}) (evicted bool) { c.lock.Lock() - defer c.lock.Unlock() - return c.lru.Add(key, value, evictedKeyVal...) + evicted = c.lru.Add(key, value) + c.lock.Unlock() + c.sendEvicted() + return } // Len returns the number of cached entries @@ -62,17 +80,33 @@ func (c *ARCCache) Keys() []interface{} { } // Remove is used to purge a key from the cache -func (c *ARCCache) Remove(key interface{}) bool { +func (c *ARCCache) Remove(key interface{}) (ok bool) { c.lock.Lock() - defer c.lock.Unlock() - return c.lru.Remove(key) + ok = c.lru.Remove(key) + c.lock.Unlock() + c.sendEvicted() + return } // Purge is used to clear the cache func (c *ARCCache) Purge() { + var keys, vals []interface{} c.lock.Lock() - defer c.lock.Unlock() + if c.onEvicted != nil { + keys = c.lru.Keys() + for _, k := range keys { + val, _ := c.lru.Peek(k) + vals = append(vals, val) + } + } c.lru.Purge() + c.lock.Unlock() + if c.onEvicted != nil { + for i := 0; i < len(keys); i++ { + c.onEvicted(keys[i], vals[i]) + } + } + } // Contains is used to check if the cache contains a key diff --git a/expiring.go b/expiring.go index 2fa402c..637c7da 100644 --- a/expiring.go +++ b/expiring.go @@ -11,9 +11,9 @@ import ( // common interface shared by 2q, arc and simple LRU, used as interface of backing LRU type lruCache interface { - // Adds a value to the cache, returns evicted if happened and + // Adds a value to the cache, returns true if happened and // updates the "recently used"-ness of the key. - Add(k, v interface{}, evictedKeyVal ...*interface{}) (evicted bool) + Add(k, v interface{}) (evicted bool) // Returns key's value from the cache if found and // updates the "recently used"-ness of the key. Get(k interface{}) (v interface{}, ok bool) @@ -55,10 +55,12 @@ const ( // expireAfterAccess and expireAfterWrite (default) // Internally keep a expireList sorted by entries' expirationTime type ExpiringCache struct { - lru lruCache - expiration time.Duration - expireList *expireList - expireType expiringType + lru lruCache + expiration time.Duration + expireList *expireList + expireType expiringType + evictedEntry *entry + onEvictedCB func(k, v interface{}) // placeholder for time.Now() for easier testing setup timeNow func() time.Time lock sync.RWMutex @@ -70,54 +72,70 @@ type OptionExp func(c *ExpiringCache) error // NewExpiring2Q creates an expiring cache with specifized // size and entries lifetime duration, backed by a 2-queue LRU func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - lru, err := simplelru.New2Q(size) + // create expiring cache with default settings + elru = &ExpiringCache{ + expiration: expir, + expireList: newExpireList(), + expireType: expireAfterWrite, + timeNow: time.Now, + } + elru.lru, err = simplelru.New2QWithEvict(size, elru.onEvicted) if err != nil { return } - elru, err = Expiring(expir, lru, opts...) + // apply options to customize + for _, opt := range opts { + if err = opt(elru); err != nil { + return + } + } return } // NewExpiringARC creates an expiring cache with specifized // size and entries lifetime duration, backed by a ARC LRU func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - lru, err := simplelru.NewARC(size) + // create expiring cache with default settings + elru = &ExpiringCache{ + expiration: expir, + expireList: newExpireList(), + expireType: expireAfterWrite, + timeNow: time.Now, + } + elru.lru, err = simplelru.NewARCWithEvict(size, elru.onEvicted) if err != nil { return } - elru, err = Expiring(expir, lru, opts...) + // apply options to customize + for _, opt := range opts { + if err = opt(elru); err != nil { + return + } + } return } // NewExpiringLRU creates an expiring cache with specifized // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - lru, err := simplelru.NewLRU(size, nil) - if err != nil { - return - } - elru, err = Expiring(expir, lru, opts...) - return -} - -// Expiring will wrap an existing LRU to make its entries -// expiring with specified duration -func Expiring(expir time.Duration, lru lruCache, opts ...OptionExp) (*ExpiringCache, error) { // create expiring cache with default settings - elru := &ExpiringCache{ - lru: lru, + elru = &ExpiringCache{ expiration: expir, expireList: newExpireList(), expireType: expireAfterWrite, timeNow: time.Now, } + elru.lru, err = simplelru.NewLRU(size, elru.onEvicted) + if err != nil { + return + } // apply options to customize for _, opt := range opts { - if err := opt(elru); err != nil { - return nil, err + if err = opt(elru); err != nil { + return } } - return elru, nil + return } // ExpireAfterWrite sets expiring policy @@ -132,6 +150,15 @@ func ExpireAfterAccess(elru *ExpiringCache) error { return nil } +// EvictedCallback register a callback to receive expired/evicted key, values +// Caution: do not do any blocking operations inside callback +func EvictedCallback(cb func(k, v interface{})) OptionExp { + return func(elru *ExpiringCache) error { + elru.onEvictedCB = cb + return nil + } +} + // TimeTicker sets the function used to return current time, for test setup func TimeTicker(tn func() time.Time) OptionExp { return func(elru *ExpiringCache) error { @@ -140,19 +167,22 @@ func TimeTicker(tn func() time.Time) OptionExp { } } +func (elru *ExpiringCache) onEvicted(k, v interface{}) { + elru.evictedEntry = v.(*entry) +} + // Add add a key/val pair to cache with cache's default expiration duration -// return evicted key/val pair if eviction happens. +// return true if eviction happens. // Should be used in most cases for better performance -func (elru *ExpiringCache) Add(k, v interface{}, evictedKeyVal ...*interface{}) (evicted bool) { - return elru.AddWithTTL(k, v, elru.expiration, evictedKeyVal...) +func (elru *ExpiringCache) Add(k, v interface{}) (evicted bool) { + return elru.AddWithTTL(k, v, elru.expiration) } // AddWithTTL add a key/val pair to cache with provided expiration duration -// return evicted key/val pair if eviction happens. +// return true if eviction happens. // Using this with variant expiration durations could cause degraded performance -func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration, evictedKeyVal ...*interface{}) (evicted bool) { +func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration) (evicted bool) { elru.lock.Lock() - defer elru.lock.Unlock() now := elru.timeNow() var ent *entry var expired []*entry @@ -174,23 +204,23 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration elru.expireList.PushFront(ent) } // Add/Update cache entry in backing cache - var evictedKey, evictedVal interface{} - evicted = elru.lru.Add(k, ent, &evictedKey, &evictedVal) + evicted = elru.lru.Add(k, ent) // remove evicted ent from expireList + var ke, ve interface{} if evicted { - ent = evictedVal.(*entry) - evictedVal = ent.val - elru.expireList.Remove(ent) + ke, ve = elru.evictedEntry.key, elru.evictedEntry.val + elru.expireList.Remove(elru.evictedEntry) + elru.evictedEntry = nil } else if len(expired) > 0 { - evictedKey = expired[0].key - evictedVal = expired[0].val evicted = true + ke = expired[0].key + ve = expired[0].val } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedVal + elru.lock.Unlock() + if evicted { + if elru.onEvictedCB != nil { + elru.onEvictedCB(ke, ve) + } } return evicted } @@ -214,14 +244,15 @@ func (elru *ExpiringCache) Get(k interface{}) (v interface{}, ok bool) { } // Remove removes a key from the cache -func (elru *ExpiringCache) Remove(k interface{}) bool { +func (elru *ExpiringCache) Remove(k interface{}) (ok bool) { elru.lock.Lock() defer elru.lock.Unlock() - if ent, _ := elru.lru.Peek(k); ent != nil { - elru.expireList.Remove(ent.(*entry)) - return elru.lru.Remove(k) + if ok = elru.lru.Remove(k); ok { + //there must be a eviction + elru.expireList.Remove(elru.evictedEntry) + elru.evictedEntry = nil } - return false + return } // Peek return key's value without updating the "recently used"-ness of the key. @@ -250,39 +281,59 @@ func (elru *ExpiringCache) Contains(k interface{}) bool { // The frequently used keys are first in the returned slice. func (elru *ExpiringCache) Keys() []interface{} { elru.lock.Lock() - defer elru.lock.Unlock() // to get accurate key set, remove all expired - elru.removeExpired(elru.timeNow(), true) + ents := elru.removeExpired(elru.timeNow(), true) + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } return elru.lru.Keys() } // Len returns the number of items in the cache. func (elru *ExpiringCache) Len() int { elru.lock.Lock() - defer elru.lock.Unlock() // to get accurate size, remove all expired - elru.removeExpired(elru.timeNow(), true) + ents := elru.removeExpired(elru.timeNow(), true) + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } return elru.lru.Len() } // Purge is used to completely clear the cache. func (elru *ExpiringCache) Purge() { + var ents []*entry elru.lock.Lock() - defer elru.lock.Unlock() - elru.expireList.Init() + if elru.onEvictedCB != nil { + ents = elru.expireList.AllEntries() + } elru.lru.Purge() + elru.evictedEntry = nil + elru.expireList.Init() + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } + } } // RemoveAllExpired remove all expired entries, can be called by cleanup goroutine -func (elru *ExpiringCache) RemoveAllExpired() (keys []interface{}, vals []interface{}) { +func (elru *ExpiringCache) RemoveAllExpired() { elru.lock.Lock() - defer elru.lock.Unlock() ents := elru.removeExpired(elru.timeNow(), true) - for _, ent := range ents { - keys = append(keys, ent.key) - vals = append(vals, ent.val) + elru.lock.Unlock() + if elru.onEvictedCB != nil { + for _, ent := range ents { + elru.onEvictedCB(ent.key, ent.val) + } } - return } // either remove one (the oldest expired), or all expired @@ -291,6 +342,9 @@ func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) ( for i := 0; i < len(res); i++ { elru.lru.Remove(res[i].key) } + //now here we already remove them from expireList, + //don't need to do it again + elru.evictedEntry = nil return } @@ -333,6 +387,13 @@ func (el *expireList) MoveToFront(ent *entry) { el.expList.MoveAfter(ent.elem, el.expList.Back()) } +func (el *expireList) AllEntries() (res []*entry) { + for e := el.expList.Front(); e != nil; e = e.Next() { + res = append(res, e.Value.(*entry)) + } + return +} + func (el *expireList) Remove(ent *entry) interface{} { return el.expList.Remove(ent.elem) } diff --git a/lru.go b/lru.go index 44394ce..ac9b3ab 100644 --- a/lru.go +++ b/lru.go @@ -8,8 +8,10 @@ import ( // Cache is a thread-safe fixed size LRU cache. type Cache struct { - lru *simplelru.LRU - lock sync.RWMutex + lru *simplelru.LRU + evictedKeys, evictedVals []interface{} + onEvictedCB func(k, v interface{}) + lock sync.RWMutex } // New creates an LRU of the given size. @@ -19,16 +21,31 @@ func New(size int) (*Cache, error) { // NewWithEvict constructs a fixed size cache with the given eviction // callback. -func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { +func NewWithEvict(size int, onEvicted func(key, value interface{})) (c *Cache, err error) { // create a cache with default settings - lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted)) - if err != nil { - return nil, err + c = &Cache{onEvictedCB: onEvicted} + if onEvicted != nil { + onEvicted = c.onEvicted } - c := &Cache{ - lru: lru, + c.lru, err = simplelru.NewLRU(size, onEvicted) + return +} + +//evicted key/val will be buffered and sent in callback outside of critical section +func (c *Cache) onEvicted(k, v interface{}) { + c.evictedKeys = append(c.evictedKeys, k) + c.evictedVals = append(c.evictedVals, v) +} + +//invoke callback outside of critical section to avoid dead-lock +func (c *Cache) sendEvicted() { + if c.onEvictedCB != nil { + for i := 0; i < len(c.evictedKeys); i++ { + c.onEvictedCB(c.evictedKeys[i], c.evictedVals[i]) + } + c.evictedKeys = nil + c.evictedVals = nil } - return c, nil } // Purge is used to completely clear the cache. @@ -36,14 +53,17 @@ func (c *Cache) Purge() { c.lock.Lock() c.lru.Purge() c.lock.Unlock() + //invoke callback outside of critical section + c.sendEvicted() } -// Add adds a value to the cache. Returns true and evicted key/val if an eviction occurred. -func (c *Cache) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { +// Add adds a value to the cache. Returns true if an eviction occurred. +func (c *Cache) Add(key, value interface{}) (evicted bool) { c.lock.Lock() - evicted = c.lru.Add(key, value, evictedKeyVal...) + evicted = c.lru.Add(key, value) c.lock.Unlock() - return evicted + c.sendEvicted() + return } // Get looks up a key's value from the cache. @@ -77,12 +97,13 @@ func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) { // Returns whether found and whether an eviction occurred. func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { c.lock.Lock() - defer c.lock.Unlock() - if c.lru.Contains(key) { + c.lock.Unlock() return true, false } evicted = c.lru.Add(key, value) + c.lock.Unlock() + c.sendEvicted() return false, evicted } @@ -91,14 +112,14 @@ func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { // Returns whether found and whether an eviction occurred. func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) { c.lock.Lock() - defer c.lock.Unlock() - previous, ok = c.lru.Peek(key) if ok { + c.lock.Unlock() return previous, true, false } - evicted = c.lru.Add(key, value) + c.lock.Unlock() + c.sendEvicted() return nil, false, evicted } @@ -107,6 +128,7 @@ func (c *Cache) Remove(key interface{}) (present bool) { c.lock.Lock() present = c.lru.Remove(key) c.lock.Unlock() + c.sendEvicted() return } @@ -115,6 +137,7 @@ func (c *Cache) Resize(size int) (evicted int) { c.lock.Lock() evicted = c.lru.Resize(size) c.lock.Unlock() + c.sendEvicted() return evicted } @@ -123,14 +146,15 @@ func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) { c.lock.Lock() key, value, ok = c.lru.RemoveOldest() c.lock.Unlock() + c.sendEvicted() return } // GetOldest returns the oldest entry func (c *Cache) GetOldest() (key, value interface{}, ok bool) { - c.lock.Lock() + c.lock.RLock() key, value, ok = c.lru.GetOldest() - c.lock.Unlock() + c.lock.RUnlock() return } diff --git a/simplelru/2q.go b/simplelru/2q.go index 7f5111b..44007f0 100644 --- a/simplelru/2q.go +++ b/simplelru/2q.go @@ -30,17 +30,23 @@ type TwoQueueLRU struct { recent LRUCache frequent LRUCache recentEvict LRUCache + + onEvicted EvictCallback } // New2Q creates a new TwoQueueLRU using the default // values for the parameters. func New2Q(size int) (*TwoQueueLRU, error) { - return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries) + return New2QParams(size, nil, Default2QRecentRatio, Default2QGhostEntries) +} + +func New2QWithEvict(size int, onEvict EvictCallback) (*TwoQueueLRU, error) { + return New2QParams(size, onEvict, Default2QRecentRatio, Default2QGhostEntries) } // New2QParams creates a new TwoQueueLRU using the provided // parameter values. -func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueLRU, error) { +func New2QParams(size int, onEvict EvictCallback, recentRatio, ghostRatio float64) (*TwoQueueLRU, error) { if size <= 0 { return nil, fmt.Errorf("invalid size") } @@ -76,6 +82,7 @@ func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueLRU, error recent: recent, frequent: frequent, recentEvict: recentEvict, + onEvicted: onEvict, } return c, nil } @@ -100,7 +107,7 @@ func (c *TwoQueueLRU) Get(key interface{}) (value interface{}, ok bool) { } // Add adds a value to the cache, return evicted key/val if eviction happens. -func (c *TwoQueueLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { +func (c *TwoQueueLRU) Add(key, value interface{}) (evicted bool) { // Check if the value is frequently used already, // and just update the value if c.frequent.Contains(key) { @@ -128,11 +135,8 @@ func (c *TwoQueueLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) evictedKey, evictedValue, evicted = c.ensureSpace(false) c.recent.Add(key, value) } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedValue + if evicted && c.onEvicted != nil { + c.onEvicted(evictedKey, evictedValue) } return evicted } @@ -172,19 +176,35 @@ func (c *TwoQueueLRU) Keys() []interface{} { } // Remove removes the provided key from the cache. -func (c *TwoQueueLRU) Remove(key interface{}) bool { - if c.frequent.Remove(key) { - return true +func (c *TwoQueueLRU) Remove(key interface{}) (ok bool) { + var val interface{} + if val, ok = c.frequent.Peek(key); ok { + c.frequent.Remove(key) + } else if val, ok = c.recent.Peek(key); ok { + c.recent.Remove(key) } - if c.recent.Remove(key) { - return true + if ok { + if c.onEvicted != nil { + c.onEvicted(key, val) + } + } else { + c.recentEvict.Remove(key) } - c.recentEvict.Remove(key) - return false + return } // Purge is used to completely clear the cache. func (c *TwoQueueLRU) Purge() { + if c.onEvicted != nil { + for _, k := range c.frequent.Keys() { + v, _ := c.frequent.Peek(k) + c.onEvicted(k, v) + } + for _, k := range c.recent.Keys() { + v, _ := c.recent.Peek(k) + c.onEvicted(k, v) + } + } c.recent.Purge() c.frequent.Purge() c.recentEvict.Purge() diff --git a/simplelru/arc.go b/simplelru/arc.go index 04e188c..91c6a51 100644 --- a/simplelru/arc.go +++ b/simplelru/arc.go @@ -17,10 +17,17 @@ type ARCLRU struct { t2 LRUCache // T2 is the LRU for frequently accessed items b2 LRUCache // B2 is the LRU for evictions from t2 + + onEvicted EvictCallback } // NewARC creates an ARC of the given size func NewARC(size int) (*ARCLRU, error) { + return NewARCWithEvict(size, nil) +} + +// NewARC creates an ARC of the given size +func NewARCWithEvict(size int, onEvict EvictCallback) (*ARCLRU, error) { // Create the sub LRUs b1, err := NewLRU(size, nil) if err != nil { @@ -41,12 +48,13 @@ func NewARC(size int) (*ARCLRU, error) { // Initialize the ARC c := &ARCLRU{ - size: size, - p: 0, - t1: t1, - b1: b1, - t2: t2, - b2: b2, + size: size, + p: 0, + t1: t1, + b1: b1, + t2: t2, + b2: b2, + onEvicted: onEvict, } return c, nil } @@ -71,7 +79,7 @@ func (c *ARCLRU) Get(key interface{}) (value interface{}, ok bool) { } // Add adds a value to the cache, return evicted key/val if it happens. -func (c *ARCLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evicted bool) { +func (c *ARCLRU) Add(key, value interface{}) (evicted bool) { // Check if the value is contained in T1 (recent), and potentially // promote it to frequent T2 if c.t1.Contains(key) { @@ -159,11 +167,8 @@ func (c *ARCLRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evi // Add to the recently seen list c.t1.Add(key, value) } - if evicted && len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = evictedKey - } - if evicted && len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = evictedValue + if evicted && c.onEvicted != nil { + c.onEvicted(evictedKey, evictedValue) } return evicted } @@ -199,24 +204,37 @@ func (c *ARCLRU) Keys() []interface{} { } // Remove is used to purge a key from the cache -func (c *ARCLRU) Remove(key interface{}) bool { - if c.t1.Remove(key) { - return true - } - if c.t2.Remove(key) { - return true - } - if c.b1.Remove(key) { - return false +func (c *ARCLRU) Remove(key interface{}) (ok bool) { + var val interface{} + if val, ok = c.t1.Peek(key); ok { + c.t1.Remove(key) + } else if val, ok = c.t2.Peek(key); ok { + c.t2.Remove(key) } - if c.b2.Remove(key) { - return false + if ok { + if c.onEvicted != nil { + c.onEvicted(key, val) + } + } else { + if !c.b1.Remove(key) { + c.b2.Remove(key) + } } - return false + return } // Purge is used to clear the cache func (c *ARCLRU) Purge() { + if c.onEvicted != nil { + for _, k := range c.t1.Keys() { + v, _ := c.t1.Peek(k) + c.onEvicted(k, v) + } + for _, k := range c.t2.Keys() { + v, _ := c.t2.Peek(k) + c.onEvicted(k, v) + } + } c.t1.Purge() c.t2.Purge() c.b1.Purge() diff --git a/simplelru/lru.go b/simplelru/lru.go index 259e6b3..9d3b030 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -5,9 +5,6 @@ import ( "errors" ) -// EvictCallback is used to get a callback when a cache entry is evicted -type EvictCallback func(key interface{}, value interface{}) - // LRU implements a non-thread safe fixed size LRU cache type LRU struct { size int @@ -48,7 +45,7 @@ func (c *LRU) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *LRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evict bool) { +func (c *LRU) Add(key, value interface{}) (evict bool) { // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) @@ -64,13 +61,7 @@ func (c *LRU) Add(key, value interface{}, evictedKeyVal ...*interface{}) (evict evict = c.evictList.Len() > c.size // Verify size not exceeded if evict { - k, v, _ := c.RemoveOldest() - if len(evictedKeyVal) > 0 { - *evictedKeyVal[0] = k - } - if len(evictedKeyVal) > 1 { - *evictedKeyVal[1] = v - } + c.removeOldest() } return } diff --git a/simplelru/lru_interface.go b/simplelru/lru_interface.go index 33000bd..f88df79 100644 --- a/simplelru/lru_interface.go +++ b/simplelru/lru_interface.go @@ -1,11 +1,14 @@ // Package simplelru provides simple LRU implementation based on build-in container/list. package simplelru +// EvictCallback is used to get a callback when a cache entry is evicted +type EvictCallback func(key interface{}, value interface{}) + // LRUCache is the interface for simple LRU cache. type LRUCache interface { - // Adds a value to the cache, returns true if an eviction occurred - // return evicted key/val and updates the "recently used"-ness of the key. - Add(key, value interface{}, evictedKeyVal ...*interface{}) bool + // Adds a value to the cache, returns true if an eviction occurred and + // updates the "recently used"-ness of the key. + Add(key, value interface{}) bool // Returns key's value from the cache and // updates the "recently used"-ness of the key. #value, isFound @@ -36,5 +39,5 @@ type LRUCache interface { Purge() // Resizes cache, returning number evicted - Resize(size int) int + Resize(int) int } From 8b1f11854822d0760d938cde226af8ab9a979c8e Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sat, 5 Dec 2020 10:05:38 -0800 Subject: [PATCH 12/17] clean up code --- 2q.go | 28 +++++++++-------- arc.go | 25 ++++++++------- expiring.go | 87 +++++++++++++++++++++++++++++------------------------ lru.go | 55 +++++++++++++++++++++++++-------- 4 files changed, 120 insertions(+), 75 deletions(-) diff --git a/2q.go b/2q.go index 8a0ab2c..5afcde5 100644 --- a/2q.go +++ b/2q.go @@ -43,21 +43,13 @@ func New2QParams(size int, onEvict func(k, v interface{}), recentRatio, ghostRat return } -//evicted key/val will be buffered and sent thru callback outside of critical section +//evicted key/val will be saved and sent thru registered callback +//outside of critical section later func (c *TwoQueueCache) onEvicted(k, v interface{}) { c.evictedKey = k c.evictedVal = v } -//invoke callback outside of critical section to avoid dead-lock -func (c *TwoQueueCache) sendEvicted() { - if c.onEvictedCB != nil { - c.onEvictedCB(c.evictedKey, c.evictedVal) - c.evictedKey = nil - c.evictedVal = nil - } -} - // Get looks up a key's value from the cache. func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() @@ -67,10 +59,16 @@ func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) { // Add adds a value to the cache, return true if eviction happens. func (c *TwoQueueCache) Add(key, value interface{}) (evicted bool) { + var ke, ve interface{} c.lock.Lock() evicted = c.lru.Add(key, value) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil c.lock.Unlock() - c.sendEvicted() + if evicted && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } return } @@ -91,10 +89,16 @@ func (c *TwoQueueCache) Keys() []interface{} { // Remove removes the provided key from the cache. func (c *TwoQueueCache) Remove(key interface{}) (ok bool) { + var ke, ve interface{} c.lock.Lock() ok = c.lru.Remove(key) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil c.lock.Unlock() - c.sendEvicted() + if ok && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } return } diff --git a/arc.go b/arc.go index 8f3c40d..f76c261 100644 --- a/arc.go +++ b/arc.go @@ -40,15 +40,6 @@ func (c *ARCCache) onEvicted(k, v interface{}) { c.evictedVal = v } -//invoke callback outside of critical section to avoid dead-lock -func (c *ARCCache) sendEvicted() { - if c.onEvictedCB != nil { - c.onEvictedCB(c.evictedKey, c.evictedVal) - c.evictedKey = nil - c.evictedVal = nil - } -} - // Get looks up a key's value from the cache. func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { c.lock.Lock() @@ -58,10 +49,16 @@ func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) { // Add adds a value to the cache, return evicted key/val if it happens. func (c *ARCCache) Add(key, value interface{}) (evicted bool) { + var ke, ve interface{} c.lock.Lock() evicted = c.lru.Add(key, value) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil c.lock.Unlock() - c.sendEvicted() + if evicted && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } return } @@ -81,10 +78,16 @@ func (c *ARCCache) Keys() []interface{} { // Remove is used to purge a key from the cache func (c *ARCCache) Remove(key interface{}) (ok bool) { + var ke, ve interface{} c.lock.Lock() ok = c.lru.Remove(key) + ke, ve = c.evictedKey, c.evictedVal + c.evictedKey = nil + c.evictedVal = nil c.lock.Unlock() - c.sendEvicted() + if ok && c.onEvictedCB != nil { + c.onEvictedCB(ke, ve) + } return } diff --git a/expiring.go b/expiring.go index 637c7da..d903d80 100644 --- a/expiring.go +++ b/expiring.go @@ -66,12 +66,10 @@ type ExpiringCache struct { lock sync.RWMutex } -// OptionExp defines option to customize ExpiringCache +// OptionExp defines options to customize ExpiringCache type OptionExp func(c *ExpiringCache) error -// NewExpiring2Q creates an expiring cache with specifized -// size and entries lifetime duration, backed by a 2-queue LRU -func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { +func newExpiringCacheWithOptions(expir time.Duration, opts []OptionExp) (elru *ExpiringCache, err error) { // create expiring cache with default settings elru = &ExpiringCache{ expiration: expir, @@ -79,10 +77,6 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi expireType: expireAfterWrite, timeNow: time.Now, } - elru.lru, err = simplelru.New2QWithEvict(size, elru.onEvicted) - if err != nil { - return - } // apply options to customize for _, opt := range opts { if err = opt(elru); err != nil { @@ -92,49 +86,54 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi return } +// NewExpiring2Q creates an expiring cache with specifized +// size and entries lifetime duration, backed by a 2-queue LRU +func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { + if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { + return + } + onEvicted := elru.onEvictedCB + if onEvicted != nil { + onEvicted = elru.onEvicted + } + elru.lru, err = simplelru.New2QWithEvict(size, onEvicted) + if err != nil { + return + } + return +} + // NewExpiringARC creates an expiring cache with specifized // size and entries lifetime duration, backed by a ARC LRU func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create expiring cache with default settings - elru = &ExpiringCache{ - expiration: expir, - expireList: newExpireList(), - expireType: expireAfterWrite, - timeNow: time.Now, + if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { + return + } + onEvicted := elru.onEvictedCB + if onEvicted != nil { + onEvicted = elru.onEvicted } - elru.lru, err = simplelru.NewARCWithEvict(size, elru.onEvicted) + elru.lru, err = simplelru.NewARCWithEvict(size, onEvicted) if err != nil { return } - // apply options to customize - for _, opt := range opts { - if err = opt(elru); err != nil { - return - } - } return } // NewExpiringLRU creates an expiring cache with specifized // size and entries lifetime duration, backed by a simple LRU func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *ExpiringCache, err error) { - // create expiring cache with default settings - elru = &ExpiringCache{ - expiration: expir, - expireList: newExpireList(), - expireType: expireAfterWrite, - timeNow: time.Now, + if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { + return + } + onEvicted := elru.onEvictedCB + if onEvicted != nil { + onEvicted = elru.onEvicted } - elru.lru, err = simplelru.NewLRU(size, elru.onEvicted) + elru.lru, err = simplelru.NewLRU(size, onEvicted) if err != nil { return } - // apply options to customize - for _, opt := range opts { - if err = opt(elru); err != nil { - return - } - } return } @@ -167,6 +166,7 @@ func TimeTicker(tn func() time.Time) OptionExp { } } +// buffer evicted key/val to be sent on registered callback func (elru *ExpiringCache) onEvicted(k, v interface{}) { elru.evictedEntry = v.(*entry) } @@ -205,9 +205,9 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration } // Add/Update cache entry in backing cache evicted = elru.lru.Add(k, ent) - // remove evicted ent from expireList var ke, ve interface{} if evicted { + // remove evicted ent from expireList ke, ve = elru.evictedEntry.key, elru.evictedEntry.val elru.expireList.Remove(elru.evictedEntry) elru.evictedEntry = nil @@ -245,13 +245,18 @@ func (elru *ExpiringCache) Get(k interface{}) (v interface{}, ok bool) { // Remove removes a key from the cache func (elru *ExpiringCache) Remove(k interface{}) (ok bool) { + var ke, ve interface{} elru.lock.Lock() - defer elru.lock.Unlock() if ok = elru.lru.Remove(k); ok { //there must be a eviction elru.expireList.Remove(elru.evictedEntry) + ke, ve = elru.evictedEntry.key, elru.evictedEntry.val elru.evictedEntry = nil } + elru.lock.Unlock() + if ok && elru.onEvictedCB != nil { + elru.onEvictedCB(ke, ve) + } return } @@ -279,31 +284,33 @@ func (elru *ExpiringCache) Contains(k interface{}) bool { // Keys returns a slice of the keys in the cache. // The frequently used keys are first in the returned slice. -func (elru *ExpiringCache) Keys() []interface{} { +func (elru *ExpiringCache) Keys() (res []interface{}) { elru.lock.Lock() // to get accurate key set, remove all expired ents := elru.removeExpired(elru.timeNow(), true) + res = elru.lru.Keys() elru.lock.Unlock() if elru.onEvictedCB != nil { for _, ent := range ents { elru.onEvictedCB(ent.key, ent.val) } } - return elru.lru.Keys() + return } // Len returns the number of items in the cache. -func (elru *ExpiringCache) Len() int { +func (elru *ExpiringCache) Len() (sz int) { elru.lock.Lock() // to get accurate size, remove all expired ents := elru.removeExpired(elru.timeNow(), true) + sz = elru.lru.Len() elru.lock.Unlock() if elru.onEvictedCB != nil { for _, ent := range ents { elru.onEvictedCB(ent.key, ent.val) } } - return elru.lru.Len() + return } // Purge is used to completely clear the cache. diff --git a/lru.go b/lru.go index ac9b3ab..dba6d6b 100644 --- a/lru.go +++ b/lru.go @@ -38,31 +38,37 @@ func (c *Cache) onEvicted(k, v interface{}) { } //invoke callback outside of critical section to avoid dead-lock -func (c *Cache) sendEvicted() { +func (c *Cache) sendEvicted(keys, vals []interface{}) { if c.onEvictedCB != nil { - for i := 0; i < len(c.evictedKeys); i++ { - c.onEvictedCB(c.evictedKeys[i], c.evictedVals[i]) + for i := 0; i < len(keys); i++ { + c.onEvictedCB(keys[i], vals[i]) } - c.evictedKeys = nil - c.evictedVals = nil } } // Purge is used to completely clear the cache. func (c *Cache) Purge() { + var ks, vs []interface{} c.lock.Lock() c.lru.Purge() + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() //invoke callback outside of critical section - c.sendEvicted() + c.sendEvicted(ks, vs) } // Add adds a value to the cache. Returns true if an eviction occurred. func (c *Cache) Add(key, value interface{}) (evicted bool) { + var ks, vs []interface{} c.lock.Lock() evicted = c.lru.Add(key, value) + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() - c.sendEvicted() + if evicted { + c.sendEvicted(ks, vs) + } return } @@ -96,14 +102,19 @@ func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) { // recent-ness or deleting it for being stale, and if not, adds the value. // Returns whether found and whether an eviction occurred. func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { + var ks, vs []interface{} c.lock.Lock() if c.lru.Contains(key) { c.lock.Unlock() return true, false } evicted = c.lru.Add(key, value) + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() - c.sendEvicted() + if evicted { + c.sendEvicted(ks, vs) + } return false, evicted } @@ -111,6 +122,7 @@ func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { // recent-ness or deleting it for being stale, and if not, adds the value. // Returns whether found and whether an eviction occurred. func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) { + var ks, vs []interface{} c.lock.Lock() previous, ok = c.lru.Peek(key) if ok { @@ -118,35 +130,54 @@ func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evi return previous, true, false } evicted = c.lru.Add(key, value) + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() - c.sendEvicted() + if evicted { + c.sendEvicted(ks, vs) + } return nil, false, evicted } // Remove removes the provided key from the cache. func (c *Cache) Remove(key interface{}) (present bool) { + var ks, vs []interface{} c.lock.Lock() present = c.lru.Remove(key) + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() - c.sendEvicted() + if present { + c.sendEvicted(ks, vs) + } return } // Resize changes the cache size. func (c *Cache) Resize(size int) (evicted int) { + var ks, vs []interface{} c.lock.Lock() evicted = c.lru.Resize(size) + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() - c.sendEvicted() + if evicted > 0 { + c.sendEvicted(ks, vs) + } return evicted } // RemoveOldest removes the oldest item from the cache. func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) { + var ks, vs []interface{} c.lock.Lock() key, value, ok = c.lru.RemoveOldest() + ks, vs = c.evictedKeys, c.evictedVals + c.evictedKeys, c.evictedVals = nil, nil c.lock.Unlock() - c.sendEvicted() + if ok { + c.sendEvicted(ks, vs) + } return } From 25e60d582884177761d76bcd39ce21ae89c7add3 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sat, 5 Dec 2020 12:00:20 -0800 Subject: [PATCH 13/17] update test code --- 2q.go | 6 +-- arc.go | 6 +-- expiring.go | 18 ++------ expiring_test.go | 40 ++++++++++++------ lru.go | 107 ++++++++++++++++++++++++++++------------------- 5 files changed, 102 insertions(+), 75 deletions(-) diff --git a/2q.go b/2q.go index 5afcde5..8e7e17d 100644 --- a/2q.go +++ b/2q.go @@ -106,7 +106,7 @@ func (c *TwoQueueCache) Remove(key interface{}) (ok bool) { func (c *TwoQueueCache) Purge() { var keys, vals []interface{} c.lock.Lock() - if c.onEvicted != nil { + if c.onEvictedCB != nil { keys = c.lru.Keys() for _, k := range keys { val, _ := c.lru.Peek(k) @@ -115,9 +115,9 @@ func (c *TwoQueueCache) Purge() { } c.lru.Purge() c.lock.Unlock() - if c.onEvicted != nil { + if c.onEvictedCB != nil { for i := 0; i < len(keys); i++ { - c.onEvicted(keys[i], vals[i]) + c.onEvictedCB(keys[i], vals[i]) } } } diff --git a/arc.go b/arc.go index f76c261..a4900d9 100644 --- a/arc.go +++ b/arc.go @@ -95,7 +95,7 @@ func (c *ARCCache) Remove(key interface{}) (ok bool) { func (c *ARCCache) Purge() { var keys, vals []interface{} c.lock.Lock() - if c.onEvicted != nil { + if c.onEvictedCB != nil { keys = c.lru.Keys() for _, k := range keys { val, _ := c.lru.Peek(k) @@ -104,9 +104,9 @@ func (c *ARCCache) Purge() { } c.lru.Purge() c.lock.Unlock() - if c.onEvicted != nil { + if c.onEvictedCB != nil { for i := 0; i < len(keys); i++ { - c.onEvicted(keys[i], vals[i]) + c.onEvictedCB(keys[i], vals[i]) } } diff --git a/expiring.go b/expiring.go index d903d80..72eece0 100644 --- a/expiring.go +++ b/expiring.go @@ -92,11 +92,7 @@ func NewExpiring2Q(size int, expir time.Duration, opts ...OptionExp) (elru *Expi if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { return } - onEvicted := elru.onEvictedCB - if onEvicted != nil { - onEvicted = elru.onEvicted - } - elru.lru, err = simplelru.New2QWithEvict(size, onEvicted) + elru.lru, err = simplelru.New2QWithEvict(size, elru.onEvicted) if err != nil { return } @@ -109,11 +105,7 @@ func NewExpiringARC(size int, expir time.Duration, opts ...OptionExp) (elru *Exp if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { return } - onEvicted := elru.onEvictedCB - if onEvicted != nil { - onEvicted = elru.onEvicted - } - elru.lru, err = simplelru.NewARCWithEvict(size, onEvicted) + elru.lru, err = simplelru.NewARCWithEvict(size, elru.onEvicted) if err != nil { return } @@ -126,11 +118,7 @@ func NewExpiringLRU(size int, expir time.Duration, opts ...OptionExp) (elru *Exp if elru, err = newExpiringCacheWithOptions(expir, opts); err != nil { return } - onEvicted := elru.onEvictedCB - if onEvicted != nil { - onEvicted = elru.onEvicted - } - elru.lru, err = simplelru.NewLRU(size, onEvicted) + elru.lru, err = simplelru.NewLRU(size, elru.onEvicted) if err != nil { return } diff --git a/expiring_test.go b/expiring_test.go index 2540f33..6aa630e 100644 --- a/expiring_test.go +++ b/expiring_test.go @@ -273,7 +273,11 @@ func TestExpiringLRU_RandomOps(t *testing.T) { // Test eviction by least-recently-used (2-queue LRU suuport retaining frequently-used) func TestExpiring2Q_EvictionByLRU(t *testing.T) { - elru, err := NewExpiring2Q(3, 30*time.Second) + var ek, ev interface{} + elru, err := NewExpiring2Q(3, 30*time.Second, EvictedCallback(func(k, v interface{}) { + ek = k + ev = v + })) if err != nil { t.Fatalf("failed to create expiring LRU") } @@ -287,9 +291,8 @@ func TestExpiring2Q_EvictionByLRU(t *testing.T) { elru.Get(i) } // next add 3,4; verify 2, 3 will be evicted - var ek, ev interface{} for i := 3; i < 5; i++ { - evicted := elru.Add(i, i, &ek, &ev) + evicted := elru.Add(i, i) k, v := ek.(int), ev.(int) if !evicted || k != (i-1) || v != (i-1) { t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-1, i-1, k, v) @@ -321,9 +324,15 @@ func (tt *testTimer) Advance(d time.Duration) { tt.t = tt.t.Add(d) } // Test eviction by ExpireAfterWrite func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { + var ek, ev interface{} // use test timer for expiration tt := newTestTimer() - elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now)) + elru, err := NewExpiring2Q(3, 30*time.Second, TimeTicker(tt.Now), EvictedCallback( + func(k, v interface{}) { + ek = k + ev = v + }, + )) if err != nil { t.Fatalf("failed to create expiring LRU") } @@ -343,9 +352,8 @@ func TestExpiring2Q_ExpireAfterWrite(t *testing.T) { // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) // next add 3,4; verify 0,1 will be evicted - var ek, ev interface{} for i := 3; i < 5; i++ { - evicted := elru.Add(i, i, &ek, &ev) + evicted := elru.Add(i, i) k, v := ek.(int), ev.(int) if !evicted || k != (i-3) || v != (i-3) { t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) @@ -410,9 +418,14 @@ func TestExpiring2Q_ExpireAfterAccess(t *testing.T) { // Test eviction by ExpireAfterWrite func TestExpiringARC_ExpireAfterWrite(t *testing.T) { + var ek, ev interface{} // use test timer for expiration tt := newTestTimer() - elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now)) + elru, err := NewExpiringARC(3, 30*time.Second, TimeTicker(tt.Now), EvictedCallback( + func(k, v interface{}) { + ek, ev = k, v + }, + )) if err != nil { t.Fatalf("failed to create expiring LRU") } @@ -432,9 +445,8 @@ func TestExpiringARC_ExpireAfterWrite(t *testing.T) { // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) // next add 3,4; verify 0,1 will be evicted - var ek, ev interface{} for i := 3; i < 5; i++ { - evicted := elru.Add(i, i, &ek, &ev) + evicted := elru.Add(i, i) k, v := ek.(int), ev.(int) if !evicted || k != (i-3) || v != (i-3) { t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) @@ -499,9 +511,14 @@ func TestExpiringARC_ExpireAfterAccess(t *testing.T) { // Test eviction by ExpireAfterWrite func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { + var ek, ev interface{} // use test timer for expiration tt := newTestTimer() - elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now)) + elru, err := NewExpiringLRU(3, 30*time.Second, TimeTicker(tt.Now), EvictedCallback( + func(k, v interface{}) { + ek, ev = k, v + }, + )) if err != nil { t.Fatalf("failed to create expiring LRU") } @@ -521,9 +538,8 @@ func TestExpiringLRU_ExpireAfterWrite(t *testing.T) { // so they should be evicted, although they are more recently retrieved than <2,2> tt.Advance(15 * time.Second) // next add 3,4; verify 0,1 will be evicted - var ek, ev interface{} for i := 3; i < 5; i++ { - evicted := elru.Add(i, i, &ek, &ev) + evicted := elru.Add(i, i) k, v := ek.(int), ev.(int) if !evicted || k != (i-3) || v != (i-3) { t.Fatalf("(%v %v) should be evicted, but got (%v,%v)", i-3, i-3, k, v) diff --git a/lru.go b/lru.go index dba6d6b..75141ed 100644 --- a/lru.go +++ b/lru.go @@ -6,6 +6,10 @@ import ( "github.com/hashicorp/golang-lru/simplelru" ) +const ( + DefaultEvictedBufferSize = 16 +) + // Cache is a thread-safe fixed size LRU cache. type Cache struct { lru *simplelru.LRU @@ -23,51 +27,58 @@ func New(size int) (*Cache, error) { // callback. func NewWithEvict(size int, onEvicted func(key, value interface{})) (c *Cache, err error) { // create a cache with default settings - c = &Cache{onEvictedCB: onEvicted} + c = &Cache{ + onEvictedCB: onEvicted, + } if onEvicted != nil { + c.initEvictBuffers() onEvicted = c.onEvicted } c.lru, err = simplelru.NewLRU(size, onEvicted) return } +func (c *Cache) initEvictBuffers() { + c.evictedKeys = make([]interface{}, 0, DefaultEvictedBufferSize) + c.evictedVals = make([]interface{}, 0, DefaultEvictedBufferSize) +} + //evicted key/val will be buffered and sent in callback outside of critical section func (c *Cache) onEvicted(k, v interface{}) { c.evictedKeys = append(c.evictedKeys, k) c.evictedVals = append(c.evictedVals, v) } -//invoke callback outside of critical section to avoid dead-lock -func (c *Cache) sendEvicted(keys, vals []interface{}) { - if c.onEvictedCB != nil { - for i := 0; i < len(keys); i++ { - c.onEvictedCB(keys[i], vals[i]) - } - } -} - // Purge is used to completely clear the cache. func (c *Cache) Purge() { var ks, vs []interface{} c.lock.Lock() c.lru.Purge() - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && len(c.evictedKeys) > 0 { + ks, vs = c.evictedKeys, c.evictedVals + c.initEvictBuffers() + } c.lock.Unlock() //invoke callback outside of critical section - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil { + for i := 0; i < len(ks); i++ { + c.onEvictedCB(ks[i], vs[i]) + } + } } // Add adds a value to the cache. Returns true if an eviction occurred. func (c *Cache) Add(key, value interface{}) (evicted bool) { - var ks, vs []interface{} + var k, v interface{} c.lock.Lock() evicted = c.lru.Add(key, value) - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } c.lock.Unlock() - if evicted { - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) } return } @@ -102,18 +113,20 @@ func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) { // recent-ness or deleting it for being stale, and if not, adds the value. // Returns whether found and whether an eviction occurred. func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { - var ks, vs []interface{} + var k, v interface{} c.lock.Lock() if c.lru.Contains(key) { c.lock.Unlock() return true, false } evicted = c.lru.Add(key, value) - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } c.lock.Unlock() - if evicted { - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) } return false, evicted } @@ -122,7 +135,7 @@ func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) { // recent-ness or deleting it for being stale, and if not, adds the value. // Returns whether found and whether an eviction occurred. func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) { - var ks, vs []interface{} + var k, v interface{} c.lock.Lock() previous, ok = c.lru.Peek(key) if ok { @@ -130,25 +143,29 @@ func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evi return previous, true, false } evicted = c.lru.Add(key, value) - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } c.lock.Unlock() - if evicted { - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) } return nil, false, evicted } // Remove removes the provided key from the cache. func (c *Cache) Remove(key interface{}) (present bool) { - var ks, vs []interface{} + var k, v interface{} c.lock.Lock() present = c.lru.Remove(key) - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && present { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } c.lock.Unlock() - if present { - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil && present { + c.onEvicted(k, v) } return } @@ -158,25 +175,31 @@ func (c *Cache) Resize(size int) (evicted int) { var ks, vs []interface{} c.lock.Lock() evicted = c.lru.Resize(size) - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && evicted > 0 { + ks, vs = c.evictedKeys, c.evictedVals + c.initEvictBuffers() + } c.lock.Unlock() - if evicted > 0 { - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil && evicted > 0 { + for i := 0; i < len(ks); i++ { + c.onEvictedCB(ks[i], vs[i]) + } } return evicted } // RemoveOldest removes the oldest item from the cache. func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) { - var ks, vs []interface{} + var k, v interface{} c.lock.Lock() key, value, ok = c.lru.RemoveOldest() - ks, vs = c.evictedKeys, c.evictedVals - c.evictedKeys, c.evictedVals = nil, nil + if c.onEvictedCB != nil && ok { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } c.lock.Unlock() - if ok { - c.sendEvicted(ks, vs) + if c.onEvictedCB != nil && ok { + c.onEvictedCB(k, v) } return } From 6fcb4db68b30542f64c6e914932173b7c0be9164 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sat, 5 Dec 2020 12:06:34 -0800 Subject: [PATCH 14/17] clean up --- expiring.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/expiring.go b/expiring.go index 72eece0..fa514ca 100644 --- a/expiring.go +++ b/expiring.go @@ -138,7 +138,6 @@ func ExpireAfterAccess(elru *ExpiringCache) error { } // EvictedCallback register a callback to receive expired/evicted key, values -// Caution: do not do any blocking operations inside callback func EvictedCallback(cb func(k, v interface{})) OptionExp { return func(elru *ExpiringCache) error { elru.onEvictedCB = cb @@ -205,10 +204,8 @@ func (elru *ExpiringCache) AddWithTTL(k, v interface{}, expiration time.Duration ve = expired[0].val } elru.lock.Unlock() - if evicted { - if elru.onEvictedCB != nil { - elru.onEvictedCB(ke, ve) - } + if evicted && elru.onEvictedCB != nil { + elru.onEvictedCB(ke, ve) } return evicted } From d08664607e617708e5ec3ce018caaaf977486df6 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sat, 5 Dec 2020 13:23:36 -0800 Subject: [PATCH 15/17] fix lint issues --- 2q.go | 6 ++++-- arc.go | 4 +++- expiring.go | 6 +++--- lru.go | 5 +++-- simplelru/2q.go | 2 ++ simplelru/arc.go | 2 +- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/2q.go b/2q.go index 8e7e17d..2ab2f7b 100644 --- a/2q.go +++ b/2q.go @@ -28,6 +28,8 @@ func New2Q(size int) (*TwoQueueCache, error) { return New2QParams(size, nil, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) } +// New2QWithEvict creates a new TwoQueueCache using the default +// values for the parameters and a callback to receive evicted values func New2QWithEvict(size int, onEvict func(k, v interface{})) (*TwoQueueCache, error) { return New2QParams(size, onEvict, simplelru.Default2QRecentRatio, simplelru.Default2QGhostEntries) } @@ -43,8 +45,8 @@ func New2QParams(size int, onEvict func(k, v interface{}), recentRatio, ghostRat return } -//evicted key/val will be saved and sent thru registered callback -//outside of critical section later +// evicted key/val will be saved and sent thru registered callback +// outside of critical section later func (c *TwoQueueCache) onEvicted(k, v interface{}) { c.evictedKey = k c.evictedVal = v diff --git a/arc.go b/arc.go index a4900d9..4cdd8dc 100644 --- a/arc.go +++ b/arc.go @@ -25,6 +25,8 @@ type ARCCache struct { func NewARC(size int) (*ARCCache, error) { return NewARCWithEvict(size, nil) } + +// NewARCWithEvict creates an ARC of the given size and a callback to receive evicted values func NewARCWithEvict(size int, onEvict func(k, v interface{})) (c *ARCCache, err error) { c = &ARCCache{onEvictedCB: onEvict} if onEvict != nil { @@ -34,7 +36,7 @@ func NewARCWithEvict(size int, onEvict func(k, v interface{})) (c *ARCCache, err return } -//evicted key/val will be buffered and sent thru callback outside of critical section +// evicted key/val will be buffered and sent thru callback outside of critical section func (c *ARCCache) onEvicted(k, v interface{}) { c.evictedKey = k c.evictedVal = v diff --git a/expiring.go b/expiring.go index fa514ca..af9f0de 100644 --- a/expiring.go +++ b/expiring.go @@ -233,7 +233,7 @@ func (elru *ExpiringCache) Remove(k interface{}) (ok bool) { var ke, ve interface{} elru.lock.Lock() if ok = elru.lru.Remove(k); ok { - //there must be a eviction + // there must be a eviction elru.expireList.Remove(elru.evictedEntry) ke, ve = elru.evictedEntry.key, elru.evictedEntry.val elru.evictedEntry = nil @@ -334,8 +334,8 @@ func (elru *ExpiringCache) removeExpired(now time.Time, removeAllExpired bool) ( for i := 0; i < len(res); i++ { elru.lru.Remove(res[i].key) } - //now here we already remove them from expireList, - //don't need to do it again + // now here we already remove them from expireList, + // don't need to do it again elru.evictedEntry = nil return } diff --git a/lru.go b/lru.go index 75141ed..1e98c92 100644 --- a/lru.go +++ b/lru.go @@ -7,6 +7,7 @@ import ( ) const ( + // DefaultEvictedBufferSize define the default buffer size to store evicted values DefaultEvictedBufferSize = 16 ) @@ -43,7 +44,7 @@ func (c *Cache) initEvictBuffers() { c.evictedVals = make([]interface{}, 0, DefaultEvictedBufferSize) } -//evicted key/val will be buffered and sent in callback outside of critical section +// evicted key/val will be buffered and sent in callback outside of critical section func (c *Cache) onEvicted(k, v interface{}) { c.evictedKeys = append(c.evictedKeys, k) c.evictedVals = append(c.evictedVals, v) @@ -59,7 +60,7 @@ func (c *Cache) Purge() { c.initEvictBuffers() } c.lock.Unlock() - //invoke callback outside of critical section + // invoke callback outside of critical section if c.onEvictedCB != nil { for i := 0; i < len(ks); i++ { c.onEvictedCB(ks[i], vs[i]) diff --git a/simplelru/2q.go b/simplelru/2q.go index 44007f0..c8ee569 100644 --- a/simplelru/2q.go +++ b/simplelru/2q.go @@ -40,6 +40,8 @@ func New2Q(size int) (*TwoQueueLRU, error) { return New2QParams(size, nil, Default2QRecentRatio, Default2QGhostEntries) } +// New2QWithEvict creates a new TwoQueueLRU using the default +// values for the parameters and a callback to receive evicted values func New2QWithEvict(size int, onEvict EvictCallback) (*TwoQueueLRU, error) { return New2QParams(size, onEvict, Default2QRecentRatio, Default2QGhostEntries) } diff --git a/simplelru/arc.go b/simplelru/arc.go index 91c6a51..f4e380f 100644 --- a/simplelru/arc.go +++ b/simplelru/arc.go @@ -26,7 +26,7 @@ func NewARC(size int) (*ARCLRU, error) { return NewARCWithEvict(size, nil) } -// NewARC creates an ARC of the given size +// NewARCWithEvict creates an ARC of the given size and a callback to receive evicted values func NewARCWithEvict(size int, onEvict EvictCallback) (*ARCLRU, error) { // Create the sub LRUs b1, err := NewLRU(size, nil) From aa409b96cb7708297d01f032d580d4a75cba9a54 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 6 Dec 2020 11:36:20 -0800 Subject: [PATCH 16/17] update README with info about expiring cache --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 33e58cf..0b7f8b3 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,25 @@ if l.Len() != 128 { panic(fmt.Sprintf("bad len: %v", l.Len())) } ``` + +Or use expiring caches as following: + +```go +const EntryLifeTime = time.Minute +cache, _ := NewExpiringLRU(128, EntryLifeTime) +for i := 1; i < 256; i++ { + cache.Add(i, nil) +} +// and run a background goroutine to clean up expired entries aggressively +go func() { + LOOP: + for { + select { + case <-shutdown: + break LOOP + case <-time.Tick(EntryLifeTime): + cache.RemoveAllExpired() + } + } +}() +``` From ead1c268cdec2b600f1251dc384fd7e06de53fb8 Mon Sep 17 00:00:00 2001 From: Yigong Liu Date: Sun, 10 Jan 2021 12:08:48 -0800 Subject: [PATCH 17/17] fix bug --- lru.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lru.go b/lru.go index 1e98c92..ae41776 100644 --- a/lru.go +++ b/lru.go @@ -166,7 +166,7 @@ func (c *Cache) Remove(key interface{}) (present bool) { } c.lock.Unlock() if c.onEvictedCB != nil && present { - c.onEvicted(k, v) + c.onEvictedCB(k, v) } return }