No public description
PiperOrigin-RevId: 800942765
Change-Id: I77c58eafbf998eeafab1d3fd58b46d7294c5992a
diff --git a/2q.go b/2q.go
index fa5780f..2ceae20 100644
--- a/2q.go
+++ b/2q.go
@@ -44,7 +44,7 @@
// New2QParams creates a new TwoQueueCache using the provided
// parameter values.
-func New2QParams(size int, recentRatio float64, ghostRatio float64) (*TwoQueueCache, error) {
+func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) {
if size <= 0 {
return nil, fmt.Errorf("invalid size")
}
@@ -138,7 +138,6 @@
// Add to the recently seen list
c.ensureSpace(false)
c.recent.Add(key, value)
- return
}
// ensureSpace is used to ensure we have space in the cache
diff --git a/2q_test.go b/2q_test.go
index 1b0f351..606384d 100644
--- a/2q_test.go
+++ b/2q_test.go
@@ -1,7 +1,6 @@
package lru
import (
- "math/rand"
"testing"
)
@@ -13,7 +12,7 @@
trace := make([]int64, b.N*2)
for i := 0; i < b.N*2; i++ {
- trace[i] = rand.Int63() % 32768
+ trace[i] = getRand(b) % 32768
}
b.ResetTimer()
@@ -43,9 +42,9 @@
trace := make([]int64, b.N*2)
for i := 0; i < b.N*2; i++ {
if i%2 == 0 {
- trace[i] = rand.Int63() % 16384
+ trace[i] = getRand(b) % 16384
} else {
- trace[i] = rand.Int63() % 32768
+ trace[i] = getRand(b) % 32768
}
}
@@ -75,8 +74,8 @@
n := 200000
for i := 0; i < n; i++ {
- key := rand.Int63() % 512
- r := rand.Int63()
+ key := getRand(t) % 512
+ r := getRand(t)
switch r % 3 {
case 0:
l.Add(key, key)
diff --git a/GOIMPORT/AUTOPATCHES/arc.go.patch b/GOIMPORT/AUTOPATCHES/arc.go.patch
new file mode 100644
index 0000000..063079e
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/arc.go.patch
@@ -0,0 +1,109 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ 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
++ t1 *simplelru.LRU // T1 is the LRU for recently accessed items
++ b1 *simplelru.LRU // 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
++ t2 *simplelru.LRU // T2 is the LRU for frequently accessed items
++ b2 *simplelru.LRU // B2 is the LRU for evictions from t2
+
+ lock sync.RWMutex
+ }
+
+ // NewARC creates an ARC of the given size
+ func NewARC(size int) (*ARCCache, error) {
++ return NewARCWithEvict(size, nil)
++}
++
++// NewARCWithEvict creates an ARC of the given size with onEvict callback.
++func NewARCWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*ARCCache, error) {
++ callback := simplelru.EvictCallback(onEvicted)
+ // Create the sub LRUs
+ b1, err := simplelru.NewLRU(size, nil)
+ if err != nil {
+@@ func NewARC(size int) (*ARCCache, error) {
+ if err != nil {
+ return nil, err
+ }
+- t1, err := simplelru.NewLRU(size, nil)
++ t1, err := simplelru.NewLRU(size, callback)
+ if err != nil {
+ return nil, err
+ }
+- t2, err := simplelru.NewLRU(size, nil)
++ t2, err := simplelru.NewLRU(size, callback)
+ if err != nil {
+ return nil, err
+ }
+@@ func (c *ARCCache) 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.t1.RemoveWithoutEvict(key)
+ c.t2.Add(key, val)
+ return val, ok
+ }
+@@ func (c *ARCCache) Add(key, value interface{}) {
+ // 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.t1.RemoveWithoutEvict(key)
+ c.t2.Add(key, value)
+ return
+ }
+@@ func (c *ARCCache) Keys() []interface{} {
+ return append(k1, k2...)
+ }
+
++func (c *ARCCache) subcaches() [4]simplelru.LRUCache {
++ return [4]simplelru.LRUCache{c.t1, c.t2, c.b1, c.b2}
++}
++
+ // Remove is used to purge a key from the cache
+ func (c *ARCCache) Remove(key interface{}) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+- if c.t1.Remove(key) {
+- return
+- }
+- if c.t2.Remove(key) {
+- return
+- }
+- if c.b1.Remove(key) {
+- return
+- }
+- if c.b2.Remove(key) {
+- return
++ for _, cache := range c.subcaches() {
++ if cache.Remove(key) {
++ return
++ }
+ }
+ }
+
+@@ func (c *ARCCache) Remove(key interface{}) {
+ func (c *ARCCache) Purge() {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+- c.t1.Purge()
+- c.t2.Purge()
+- c.b1.Purge()
+- c.b2.Purge()
++ for _, cache := range c.subcaches() {
++ cache.Purge()
++ }
+ }
+
+ // Contains is used to check if the cache contains a key
diff --git a/GOIMPORT/AUTOPATCHES/arc_test.go.patch b/GOIMPORT/AUTOPATCHES/arc_test.go.patch
new file mode 100644
index 0000000..decf817
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/arc_test.go.patch
@@ -0,0 +1,81 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@ import (
+ "math/rand"
+ "testing"
+ "time"
++
++ "google3/third_party/golang/cmp/cmp"
+ )
+
+ func init() {
+@@ func TestARC_Peek(t *testing.T) {
+ t.Errorf("should not have updated recent-ness of 1")
+ }
+ }
++
++func TestARC_OnEvictedCallback(t *testing.T) {
++ type testCase struct {
++ size int
++ data []int
++ evicted []int
++ }
++
++ testCases := map[string]testCase{
++ "basic-lru-like": {
++ size: 2,
++ data: []int{1, 2, 3, 4},
++ evicted: []int{1, 2},
++ },
++ "t1-to-t2-promotion-simple": {
++ size: 4,
++ data: []int{1, 2, 1, 2, 3, 4, 5, 6},
++ evicted: []int{3, 4},
++ },
++ "adaptive-replacement-simple": {
++ size: 4,
++ data: []int{1, 2, 3, 4, 5, 6, 1, 2, 7, 8},
++ evicted: []int{1, 2, 3, 4, 1, 5},
++ },
++ "MFU-not-evicted-by-new-entries": {
++ size: 4,
++ data: []int{4, 4, 1, 2, 3, 4, 5, 6, 7, 8},
++ evicted: []int{1, 2, 3, 5},
++ },
++ "t2-full-b2-empty-new-items": {
++ size: 4,
++ data: []int{
++ // fill t2, b2 should be empty since its target size is zero
++ 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8,
++ // insert "new" items
++ 1, 2, 3, 4,
++ },
++ evicted: []int{1, 2, 3, 4, 5, 1, 2, 3},
++ },
++ }
++
++ runTest := func(t *testing.T, tc testCase) {
++ evicted := []int{}
++ c, err := NewARCWithEvict(tc.size, func(key interface{}, _ interface{}) {
++ evicted = append(evicted, key.(int))
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++
++ for _, val := range tc.data {
++ c.Add(val, val)
++ }
++
++ if !cmp.Equal(tc.evicted, evicted) {
++ t.Errorf("Expectations mismatch.\nWant: %+v\nGot: %+v", tc.evicted, evicted)
++ }
++ }
++
++ for name, tc := range testCases {
++ t.Run(name, func(t *testing.T) { runTest(t, tc) })
++ }
++}
diff --git a/GOIMPORT/AUTOPATCHES/simplelru/lru.go.patch b/GOIMPORT/AUTOPATCHES/simplelru/lru.go.patch
new file mode 100644
index 0000000..fb0a5bb
--- /dev/null
+++ b/GOIMPORT/AUTOPATCHES/simplelru/lru.go.patch
@@ -0,0 +1,56 @@
+# This file reports differences detected by import.go since the last import.
+# This is only for human review and not machine consumption.
+# Please see go/thirdpartygo for more information.
+# DO NOT EDIT. This must only be generated by //third_party/golang/import.go.
+
+@@
++// Package simplelru contains intefrace and implementation of linked-list based LRU cache.
+ package simplelru
+
+ import (
+@@ func (c *LRU) Peek(key interface{}) (value interface{}, ok bool) {
+ // Remove removes the provided key from the cache, returning if the
+ // key was contained.
+ func (c *LRU) Remove(key interface{}) (present bool) {
+- if ent, ok := c.items[key]; ok {
++ ent, present := c.items[key]
++ if present {
+ c.removeElement(ent)
+- return true
+ }
+- return false
++ return present
++}
++
++// RemoveWithoutEvict removes the provided key from the cache without triggering
++// onEvict callback, returning if the key was contained.
++func (c *LRU) RemoveWithoutEvict(key interface{}) (present bool) {
++ ent, present := c.items[key]
++ if present {
++ c.removeWithoutEvict(ent)
++ }
++ return present
+ }
+
+ // RemoveOldest removes the oldest item from the cache.
+@@ func (c *LRU) removeOldest() {
+
+ // removeElement is used to remove a given list element from the cache
+ func (c *LRU) removeElement(e *list.Element) {
+- c.evictList.Remove(e)
+- kv := e.Value.(*entry)
+- delete(c.items, kv.key)
++ kv := c.removeWithoutEvict(e)
+ if c.onEvict != nil {
+ c.onEvict(kv.key, kv.value)
+ }
+ }
++
++// removeWithoutEvict is used to remove a given list element from the cache
++// without triggering onEvict callback to be called.
++func (c *LRU) removeWithoutEvict(e *list.Element) *entry {
++ c.evictList.Remove(e)
++ kv := e.Value.(*entry)
++ delete(c.items, kv.key)
++ return kv
++}
diff --git a/LICENSE b/LICENSE
index be2cc4d..0e5d580 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,5 @@
+Copyright (c) 2014 HashiCorp, Inc.
+
Mozilla Public License, version 2.0
1. Definitions
diff --git a/README.md b/README.md
index 33e58cf..03bcfb5 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,7 @@
golang-lru
==========
-This provides the `lru` package which implements a fixed-size
-thread safe LRU cache. It is based on the cache in Groupcache.
-
-Documentation
-=============
-
-Full docs are available on [Godoc](http://godoc.org/github.com/hashicorp/golang-lru)
-
-Example
-=======
-
-Using the LRU is very simple:
-
-```go
-l, _ := New(128)
-for i := 0; i < 256; i++ {
- l.Add(i, nil)
-}
-if l.Len() != 128 {
- panic(fmt.Sprintf("bad len: %v", l.Len()))
-}
-```
+Please upgrade to github.com/hashicorp/golang-lru/v2 for all new code as v1 will
+not be updated anymore. The v2 version supports generics and is faster; old code
+can specify a specific tag, e.g. github.com/hashicorp/golang-lru/v1.0.2 for
+backwards compatibility.
diff --git a/arc.go b/arc.go
index a5ed936..b33985d 100644
--- a/arc.go
+++ b/arc.go
@@ -179,7 +179,6 @@
// Add to the recently seen list
c.t1.Add(key, value)
- return
}
// replace is used to adaptively evict from either T1 or T2
diff --git a/arc_test.go b/arc_test.go
index e597eda..d58882a 100644
--- a/arc_test.go
+++ b/arc_test.go
@@ -20,7 +20,7 @@
trace := make([]int64, b.N*2)
for i := 0; i < b.N*2; i++ {
- trace[i] = rand.Int63() % 32768
+ trace[i] = getRand(b) % 32768
}
b.ResetTimer()
@@ -50,9 +50,9 @@
trace := make([]int64, b.N*2)
for i := 0; i < b.N*2; i++ {
if i%2 == 0 {
- trace[i] = rand.Int63() % 16384
+ trace[i] = getRand(b) % 16384
} else {
- trace[i] = rand.Int63() % 32768
+ trace[i] = getRand(b) % 32768
}
}
@@ -82,8 +82,8 @@
n := 200000
for i := 0; i < n; i++ {
- key := rand.Int63() % 512
- r := rand.Int63()
+ key := getRand(t) % 512
+ r := getRand(t)
switch r % 3 {
case 0:
l.Add(key, key)
diff --git a/go.mod b/go.mod
index 824cb97..8ad8826 100644
--- a/go.mod
+++ b/go.mod
@@ -1 +1,3 @@
module github.com/hashicorp/golang-lru
+
+go 1.12
diff --git a/lru.go b/lru.go
index e0d5a59..0600c53 100644
--- a/lru.go
+++ b/lru.go
@@ -6,10 +6,17 @@
"google3/third_party/golang/lru/simplelru/simplelru"
)
+const (
+ // DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
+ DefaultEvictedBufferSize = 16
+)
+
// Cache is a thread-safe fixed size LRU cache.
type Cache struct {
- lru simplelru.LRUCache
- 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,30 +26,63 @@
// NewWithEvict constructs a fixed size cache with the given eviction
// callback.
-func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) {
- lru, err := simplelru.NewLRU(size, simplelru.EvictCallback(onEvicted))
- if err != nil {
- return nil, err
+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{
- lru: lru,
+ if onEvicted != nil {
+ c.initEvictBuffers()
+ onEvicted = c.onEvicted
}
- return c, nil
+ c.lru, err = simplelru.NewLRU(size, onEvicted)
+ return
+}
+
+func (c *Cache) initEvictBuffers() {
+ c.evictedKeys = make([]interface{}, 0, DefaultEvictedBufferSize)
+ c.evictedVals = make([]interface{}, 0, DefaultEvictedBufferSize)
+}
+
+// onEvicted save evicted key/val and sent in externally registered callback
+// outside of critical section
+func (c *Cache) onEvicted(k, v interface{}) {
+ c.evictedKeys = append(c.evictedKeys, k)
+ c.evictedVals = append(c.evictedVals, v)
}
// Purge is used to completely clear the cache.
func (c *Cache) Purge() {
+ var ks, vs []interface{}
c.lock.Lock()
c.lru.Purge()
+ 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
+ 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.
+// Add adds a value to the cache. Returns true if an eviction occurred.
func (c *Cache) Add(key, value interface{}) (evicted bool) {
+ var k, v interface{}
c.lock.Lock()
evicted = c.lru.Add(key, value)
+ 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()
- return evicted
+ if c.onEvictedCB != nil && evicted {
+ c.onEvictedCB(k, v)
+ }
+ return
}
// Get looks up a key's value from the cache.
@@ -71,32 +111,107 @@
return value, ok
}
-// ContainsOrAdd checks if a key is in the cache without updating the
-// recent-ness or deleting it for being stale, and if not, adds the value.
+// ContainsOrAdd checks if a key is in the cache without updating the
+// 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 k, v interface{}
c.lock.Lock()
- defer c.lock.Unlock()
-
if c.lru.Contains(key) {
+ c.lock.Unlock()
return true, false
}
evicted = c.lru.Add(key, value)
+ 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 c.onEvictedCB != nil && evicted {
+ c.onEvictedCB(k, v)
+ }
return false, evicted
}
-// Remove removes the provided key from the cache.
-func (c *Cache) Remove(key interface{}) {
+// PeekOrAdd checks if a key is in the cache without updating the
+// 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 k, v interface{}
c.lock.Lock()
- c.lru.Remove(key)
+ previous, ok = c.lru.Peek(key)
+ if ok {
+ c.lock.Unlock()
+ return previous, true, false
+ }
+ evicted = c.lru.Add(key, value)
+ 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 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 k, v interface{}
+ c.lock.Lock()
+ present = c.lru.Remove(key)
+ 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 c.onEvictedCB != nil && present {
+ c.onEvictedCB(k, v)
+ }
+ 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)
+ if c.onEvictedCB != nil && evicted > 0 {
+ ks, vs = c.evictedKeys, c.evictedVals
+ c.initEvictBuffers()
+ }
+ c.lock.Unlock()
+ 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() {
+func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) {
+ var k, v interface{}
c.lock.Lock()
- c.lru.RemoveOldest()
+ key, value, ok = c.lru.RemoveOldest()
+ 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 c.onEvictedCB != nil && ok {
+ c.onEvictedCB(k, v)
+ }
+ return
+}
+
+// GetOldest returns the oldest entry
+func (c *Cache) GetOldest() (key, value interface{}, ok bool) {
+ c.lock.RLock()
+ key, value, ok = c.lru.GetOldest()
+ c.lock.RUnlock()
+ return
}
// Keys returns a slice of the keys in the cache, from oldest to newest.
diff --git a/lru_test.go b/lru_test.go
index e7e2350..b81705b 100644
--- a/lru_test.go
+++ b/lru_test.go
@@ -1,7 +1,6 @@
package lru
import (
- "math/rand"
"testing"
)
@@ -13,7 +12,7 @@
trace := make([]int64, b.N*2)
for i := 0; i < b.N*2; i++ {
- trace[i] = rand.Int63() % 32768
+ trace[i] = getRand(b) % 32768
}
b.ResetTimer()
@@ -43,9 +42,9 @@
trace := make([]int64, b.N*2)
for i := 0; i < b.N*2; i++ {
if i%2 == 0 {
- trace[i] = rand.Int63() % 16384
+ trace[i] = getRand(b) % 16384
} else {
- trace[i] = rand.Int63() % 32768
+ trace[i] = getRand(b) % 32768
}
}
@@ -171,7 +170,7 @@
}
}
-// test that Contains doesn't update recent-ness
+// test that ContainsOrAdd doesn't update recent-ness
func TestLRUContainsOrAdd(t *testing.T) {
l, err := New(2)
if err != nil {
@@ -201,6 +200,39 @@
}
}
+// test that PeekOrAdd doesn't update recent-ness
+func TestLRUPeekOrAdd(t *testing.T) {
+ l, err := New(2)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ l.Add(1, 1)
+ l.Add(2, 2)
+ previous, contains, evict := l.PeekOrAdd(1, 1)
+ if !contains {
+ t.Errorf("1 should be contained")
+ }
+ if evict {
+ t.Errorf("nothing should be evicted here")
+ }
+ if previous != 1 {
+ t.Errorf("previous is not equal to 1")
+ }
+
+ l.Add(3, 3)
+ contains, evict = l.ContainsOrAdd(1, 1)
+ if contains {
+ t.Errorf("1 should not have been contained")
+ }
+ if !evict {
+ t.Errorf("an eviction should have occurred")
+ }
+ if !l.Contains(1) {
+ t.Errorf("now 1 should be contained")
+ }
+}
+
// test that Peek doesn't update recent-ness
func TestLRUPeek(t *testing.T) {
l, err := New(2)
@@ -219,3 +251,42 @@
t.Errorf("should not have updated recent-ness of 1")
}
}
+
+// test that Resize can upsize and downsize
+func TestLRUResize(t *testing.T) {
+ onEvictCounter := 0
+ onEvicted := func(k interface{}, v interface{}) {
+ onEvictCounter++
+ }
+ l, err := NewWithEvict(2, onEvicted)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ // Downsize
+ l.Add(1, 1)
+ l.Add(2, 2)
+ evicted := l.Resize(1)
+ if evicted != 1 {
+ t.Errorf("1 element should have been evicted: %v", evicted)
+ }
+ if onEvictCounter != 1 {
+ t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter)
+ }
+
+ l.Add(3, 3)
+ if l.Contains(1) {
+ t.Errorf("Element 1 should have been evicted")
+ }
+
+ // Upsize
+ evicted = l.Resize(2)
+ if evicted != 0 {
+ t.Errorf("0 elements should have been evicted: %v", evicted)
+ }
+
+ l.Add(4, 4)
+ if !l.Contains(3) || !l.Contains(4) {
+ t.Errorf("Cache should have contained 2 elements")
+ }
+}
diff --git a/simplelru/lru.go b/simplelru/lru.go
index 0b40413..6714943 100644
--- a/simplelru/lru.go
+++ b/simplelru/lru.go
@@ -26,7 +26,7 @@
// NewLRU constructs an LRU of the given size
func NewLRU(size int, onEvict EvictCallback) (*LRU, error) {
if size <= 0 {
- return nil, errors.New("Must provide a positive size")
+ return nil, errors.New("must provide a positive size")
}
c := &LRU{
size: size,
@@ -74,6 +74,9 @@
func (c *LRU) Get(key interface{}) (value interface{}, ok bool) {
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
+ if ent.Value.(*entry) == nil {
+ return nil, false
+ }
return ent.Value.(*entry).value, true
}
return
@@ -117,7 +120,7 @@
}
// RemoveOldest removes the oldest item from the cache.
-func (c *LRU) RemoveOldest() (key interface{}, value interface{}, ok bool) {
+func (c *LRU) RemoveOldest() (key, value interface{}, ok bool) {
ent := c.evictList.Back()
if ent != nil {
c.removeElement(ent)
@@ -128,7 +131,7 @@
}
// GetOldest returns the oldest entry
-func (c *LRU) GetOldest() (key interface{}, value interface{}, ok bool) {
+func (c *LRU) GetOldest() (key, value interface{}, ok bool) {
ent := c.evictList.Back()
if ent != nil {
kv := ent.Value.(*entry)
@@ -153,6 +156,19 @@
return c.evictList.Len()
}
+// Resize changes the cache size.
+func (c *LRU) Resize(size int) (evicted int) {
+ diff := c.Len() - size
+ if diff < 0 {
+ diff = 0
+ }
+ for i := 0; i < diff; i++ {
+ c.removeOldest()
+ }
+ c.size = size
+ return diff
+}
+
// removeOldest removes the oldest item from the cache.
func (c *LRU) removeOldest() {
ent := c.evictList.Back()
diff --git a/simplelru/lru_interface.go b/simplelru/lru_interface.go
index 74c7077..cb7f8ca 100644
--- a/simplelru/lru_interface.go
+++ b/simplelru/lru_interface.go
@@ -1,3 +1,4 @@
+// Package simplelru provides simple LRU implementation based on build-in container/list.
package simplelru
// LRUCache is the interface for simple LRU cache.
@@ -10,7 +11,7 @@
// updates the "recently used"-ness of the key. #value, isFound
Get(key interface{}) (value interface{}, ok bool)
- // Check if a key exsists in cache without updating the recent-ness.
+ // Checks if a key exists in cache without updating the recent-ness.
Contains(key interface{}) (ok bool)
// Returns key's value without updating the "recently used"-ness of the key.
@@ -31,6 +32,9 @@
// Returns the number of items in the cache.
Len() int
- // Clear all cache entries
+ // Clears all cache entries.
Purge()
+
+ // Resizes cache, returning number evicted
+ Resize(int) int
}
diff --git a/simplelru/lru_test.go b/simplelru/lru_test.go
index ca5676e..a9ffcd5 100644
--- a/simplelru/lru_test.go
+++ b/simplelru/lru_test.go
@@ -165,3 +165,42 @@
t.Errorf("should not have updated recent-ness of 1")
}
}
+
+// Test that Resize can upsize and downsize
+func TestLRU_Resize(t *testing.T) {
+ onEvictCounter := 0
+ onEvicted := func(k interface{}, v interface{}) {
+ onEvictCounter++
+ }
+ l, err := NewLRU(2, onEvicted)
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ // Downsize
+ l.Add(1, 1)
+ l.Add(2, 2)
+ evicted := l.Resize(1)
+ if evicted != 1 {
+ t.Errorf("1 element should have been evicted: %v", evicted)
+ }
+ if onEvictCounter != 1 {
+ t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter)
+ }
+
+ l.Add(3, 3)
+ if l.Contains(1) {
+ t.Errorf("Element 1 should have been evicted")
+ }
+
+ // Upsize
+ evicted = l.Resize(2)
+ if evicted != 0 {
+ t.Errorf("0 elements should have been evicted: %v", evicted)
+ }
+
+ l.Add(4, 4)
+ if !l.Contains(3) || !l.Contains(4) {
+ t.Errorf("Cache should have contained 2 elements")
+ }
+}
diff --git a/testing.go b/testing.go
new file mode 100644
index 0000000..4927607
--- /dev/null
+++ b/testing.go
@@ -0,0 +1,16 @@
+package lru
+
+import (
+ "crypto/rand"
+ "math"
+ "math/big"
+ "testing"
+)
+
+func getRand(tb testing.TB) int64 {
+ out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+ if err != nil {
+ tb.Fatal(err)
+ }
+ return out.Int64()
+}