From 5ad56d3f2305e7729c0b0144e2bc4f6a56aaeed8 Mon Sep 17 00:00:00 2001 From: Robert Sasu Date: Wed, 12 Jan 2022 13:26:00 +0200 Subject: [PATCH] copied LRU cache from elrond-go --- go.mod | 1 + go.sum | 2 + storage/errors.go | 11 + storage/interface.go | 69 +++ storage/lrucache/capacity/capacityLRUCache.go | 284 ++++++++++ .../capacity/capacityLRUCache_test.go | 499 ++++++++++++++++++ storage/lrucache/export_test.go | 5 + storage/lrucache/lrucache.go | 191 +++++++ storage/lrucache/lrucache_test.go | 420 +++++++++++++++ storage/lrucache/simpleLRUCacheAdapter.go | 23 + 10 files changed, 1505 insertions(+) create mode 100644 storage/errors.go create mode 100644 storage/interface.go create mode 100644 storage/lrucache/capacity/capacityLRUCache.go create mode 100644 storage/lrucache/capacity/capacityLRUCache_test.go create mode 100644 storage/lrucache/export_test.go create mode 100644 storage/lrucache/lrucache.go create mode 100644 storage/lrucache/lrucache_test.go create mode 100644 storage/lrucache/simpleLRUCacheAdapter.go diff --git a/go.mod b/go.mod index 82061dc05..a3d50b95f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 + github.com/hashicorp/golang-lru v0.5.4 github.com/mr-tron/base58 v1.2.0 github.com/pelletier/go-toml v1.9.3 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index e89e5e79d..598c86962 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= diff --git a/storage/errors.go b/storage/errors.go new file mode 100644 index 000000000..7671c8818 --- /dev/null +++ b/storage/errors.go @@ -0,0 +1,11 @@ +package storage + +import ( + "errors" +) + +// ErrCacheSizeInvalid signals that size of cache is less than 1 +var ErrCacheSizeInvalid = errors.New("cache size is less than 1") + +// ErrCacheCapacityInvalid signals that capacity of cache is less than 1 +var ErrCacheCapacityInvalid = errors.New("cache capacity is less than 1") diff --git a/storage/interface.go b/storage/interface.go new file mode 100644 index 000000000..2cb05c422 --- /dev/null +++ b/storage/interface.go @@ -0,0 +1,69 @@ +package storage + +// Cacher provides caching services +type Cacher interface { + // Clear is used to completely clear the cache. + Clear() + // Put adds a value to the cache. Returns true if an eviction occurred. + Put(key []byte, value interface{}, sizeInBytes int) (evicted bool) + // Get looks up a key's value from the cache. + Get(key []byte) (value interface{}, ok bool) + // Has checks if a key is in the cache, without updating the + // recent-ness or deleting it for being stale. + Has(key []byte) bool + // Peek returns the key value (or undefined if not found) without updating + // the "recently used"-ness of the key. + Peek(key []byte) (value interface{}, ok bool) + // HasOrAdd 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. + HasOrAdd(key []byte, value interface{}, sizeInBytes int) (has, added bool) + // Remove removes the provided key from the cache. + Remove(key []byte) + // Keys returns a slice of the keys in the cache, from oldest to newest. + Keys() [][]byte + // Len returns the number of items in the cache. + Len() int + // SizeInBytesContained returns the size in bytes of all contained elements + SizeInBytesContained() uint64 + // MaxSize returns the maximum number of items which can be stored in the cache. + MaxSize() int + // RegisterHandler registers a new handler to be called when a new data is added + RegisterHandler(handler func(key []byte, value interface{}), id string) + // UnRegisterHandler deletes the handler from the list + UnRegisterHandler(id string) + // Close closes the underlying temporary db if the cacher implementation has one, + // otherwise it does nothing + Close() error + // IsInterfaceNil returns true if there is no value under the interface + IsInterfaceNil() bool +} + +// ForEachItem is an iterator callback +type ForEachItem func(key []byte, value interface{}) + +// LRUCacheHandler is the interface for LRU cache. +type LRUCacheHandler interface { + Add(key, value interface{}) bool + Get(key interface{}) (value interface{}, ok bool) + Contains(key interface{}) (ok bool) + ContainsOrAdd(key, value interface{}) (ok, evicted bool) + Peek(key interface{}) (value interface{}, ok bool) + Remove(key interface{}) bool + Keys() []interface{} + Len() int + Purge() +} + +// SizedLRUCacheHandler is the interface for size capable LRU cache. +type SizedLRUCacheHandler interface { + AddSized(key, value interface{}, sizeInBytes int64) bool + Get(key interface{}) (value interface{}, ok bool) + Contains(key interface{}) (ok bool) + AddSizedIfMissing(key, value interface{}, sizeInBytes int64) (ok, evicted bool) + Peek(key interface{}) (value interface{}, ok bool) + Remove(key interface{}) bool + Keys() []interface{} + Len() int + SizeInBytesContained() uint64 + Purge() +} diff --git a/storage/lrucache/capacity/capacityLRUCache.go b/storage/lrucache/capacity/capacityLRUCache.go new file mode 100644 index 000000000..cbb5f476e --- /dev/null +++ b/storage/lrucache/capacity/capacityLRUCache.go @@ -0,0 +1,284 @@ +package capacity + +import ( + "container/list" + "sync" + + "github.com/ElrondNetwork/elrond-go-core/storage" +) + +// capacityLRU implements a non thread safe LRU Cache with a max capacity size +type capacityLRU struct { + lock sync.Mutex + size int + maxCapacityInBytes int64 + currentCapacityInBytes int64 + //TODO investigate if we can replace this list with a binary tree. Check also the other implementation lruCache + evictList *list.List + items map[interface{}]*list.Element +} + +// entry is used to hold a value in the evictList +type entry struct { + key interface{} + value interface{} + size int64 +} + +// NewCapacityLRU constructs an CapacityLRU of the given size with a byte size capacity +func NewCapacityLRU(size int, byteCapacity int64) (*capacityLRU, error) { + if size < 1 { + return nil, storage.ErrCacheSizeInvalid + } + if byteCapacity < 1 { + return nil, storage.ErrCacheCapacityInvalid + } + c := &capacityLRU{ + size: size, + maxCapacityInBytes: byteCapacity, + evictList: list.New(), + items: make(map[interface{}]*list.Element), + } + return c, nil +} + +// Purge is used to completely clear the cache. +func (c *capacityLRU) Purge() { + c.lock.Lock() + defer c.lock.Unlock() + + c.items = make(map[interface{}]*list.Element) + c.evictList.Init() + c.currentCapacityInBytes = 0 +} + +// AddSized adds a value to the cache. Returns true if an eviction occurred. +func (c *capacityLRU) AddSized(key, value interface{}, sizeInBytes int64) bool { + c.lock.Lock() + defer c.lock.Unlock() + + c.addSized(key, value, sizeInBytes) + + return c.evictIfNeeded() +} + +func (c *capacityLRU) addSized(key interface{}, value interface{}, sizeInBytes int64) { + if sizeInBytes < 0 { + return + } + + // Check for existing item + if ent, ok := c.items[key]; ok { + c.update(key, value, sizeInBytes, ent) + } else { + c.addNew(key, value, sizeInBytes) + } +} + +// AddSizedAndReturnEvicted adds the given key-value pair to the cache, and returns the evicted values +func (c *capacityLRU) AddSizedAndReturnEvicted(key, value interface{}, sizeInBytes int64) map[interface{}]interface{} { + c.lock.Lock() + defer c.lock.Unlock() + + c.addSized(key, value, sizeInBytes) + + evictedValues := make(map[interface{}]interface{}) + for c.shouldEvict() { + evicted := c.evictList.Back() + if evicted == nil { + continue + } + + c.removeElement(evicted) + evictedEntry, ok := evicted.Value.(*entry) + if !ok { + continue + } + + evictedValues[evictedEntry.key] = evictedEntry.value + } + + return evictedValues +} + +func (c *capacityLRU) addNew(key interface{}, value interface{}, sizeInBytes int64) { + ent := &entry{ + key: key, + value: value, + size: sizeInBytes, + } + e := c.evictList.PushFront(ent) + c.items[key] = e + c.currentCapacityInBytes += sizeInBytes +} + +func (c *capacityLRU) update(key interface{}, value interface{}, sizeInBytes int64, ent *list.Element) { + c.evictList.MoveToFront(ent) + + e := ent.Value.(*entry) + sizeDiff := sizeInBytes - e.size + e.value = value + e.size = sizeInBytes + c.currentCapacityInBytes += sizeDiff + + c.adjustSize(key, sizeInBytes) +} + +// Get looks up a key's value from the cache. +func (c *capacityLRU) Get(key interface{}) (interface{}, bool) { + c.lock.Lock() + defer c.lock.Unlock() + + 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 nil, false +} + +// Contains checks if a key is in the cache, without updating the recent-ness +// or deleting it for being stale. +func (c *capacityLRU) Contains(key interface{}) bool { + c.lock.Lock() + defer c.lock.Unlock() + + _, ok := c.items[key] + + return ok +} + +// AddSizedIfMissing 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 *capacityLRU) AddSizedIfMissing(key, value interface{}, sizeInBytes int64) (bool, bool) { + if sizeInBytes < 0 { + return false, false + } + + c.lock.Lock() + defer c.lock.Unlock() + + _, ok := c.items[key] + if ok { + return true, false + } + c.addNew(key, value, sizeInBytes) + evicted := c.evictIfNeeded() + + return false, evicted +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *capacityLRU) Peek(key interface{}) (interface{}, bool) { + c.lock.Lock() + defer c.lock.Unlock() + + ent, ok := c.items[key] + if ok { + return ent.Value.(*entry).value, true + } + return nil, ok +} + +// Remove removes the provided key from the cache, returning if the +// key was contained. +func (c *capacityLRU) Remove(key interface{}) bool { + c.lock.Lock() + defer c.lock.Unlock() + + if ent, ok := c.items[key]; ok { + c.removeElement(ent) + return true + } + return false +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *capacityLRU) Keys() []interface{} { + c.lock.Lock() + defer c.lock.Unlock() + + keys := make([]interface{}, len(c.items)) + i := 0 + for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() { + keys[i] = ent.Value.(*entry).key + i++ + } + return keys +} + +// Len returns the number of items in the cache. +func (c *capacityLRU) Len() int { + c.lock.Lock() + defer c.lock.Unlock() + + return c.evictList.Len() +} + +// SizeInBytesContained returns the size in bytes of all contained elements +func (c *capacityLRU) SizeInBytesContained() uint64 { + c.lock.Lock() + defer c.lock.Unlock() + + return uint64(c.currentCapacityInBytes) +} + +// removeOldest removes the oldest item from the cache. +func (c *capacityLRU) removeOldest() { + ent := c.evictList.Back() + if ent != nil { + c.removeElement(ent) + } +} + +// removeElement is used to remove a given list element from the cache +func (c *capacityLRU) removeElement(e *list.Element) { + c.evictList.Remove(e) + kv := e.Value.(*entry) + delete(c.items, kv.key) + c.currentCapacityInBytes -= kv.size +} + +func (c *capacityLRU) adjustSize(key interface{}, sizeInBytes int64) { + element := c.items[key] + if element == nil || element.Value == nil || element.Value.(*entry) == nil { + return + } + + v := element.Value.(*entry) + c.currentCapacityInBytes -= v.size + v.size = sizeInBytes + element.Value = v + c.currentCapacityInBytes += sizeInBytes + c.evictIfNeeded() +} + +func (c *capacityLRU) shouldEvict() bool { + if c.evictList.Len() == 1 { + // keep at least one element, no matter how large it is + return false + } + + return c.evictList.Len() > c.size || c.currentCapacityInBytes > c.maxCapacityInBytes +} + +func (c *capacityLRU) evictIfNeeded() bool { + evicted := false + for c.shouldEvict() { + c.removeOldest() + evicted = true + } + + return evicted +} + +// IsInterfaceNil returns true if there is no value under the interface +func (c *capacityLRU) IsInterfaceNil() bool { + return c == nil +} diff --git a/storage/lrucache/capacity/capacityLRUCache_test.go b/storage/lrucache/capacity/capacityLRUCache_test.go new file mode 100644 index 000000000..b9a6bf070 --- /dev/null +++ b/storage/lrucache/capacity/capacityLRUCache_test.go @@ -0,0 +1,499 @@ +package capacity + +import ( + "testing" + + "github.com/ElrondNetwork/elrond-go-core/core/check" + "github.com/ElrondNetwork/elrond-go-core/storage" + "github.com/stretchr/testify/assert" +) + +func createDefaultCache() *capacityLRU { + cache, _ := NewCapacityLRU(100, 100) + return cache +} + +//------- NewCapacityLRU + +func TestNewCapacityLRU_WithInvalidSize(t *testing.T) { + t.Parallel() + + size := 0 + capacity := int64(1) + cache, err := NewCapacityLRU(size, capacity) + assert.True(t, check.IfNil(cache)) + assert.Equal(t, storage.ErrCacheSizeInvalid, err) +} + +func TestNewCapacityLRU_WithInvalidCapacity(t *testing.T) { + t.Parallel() + + size := 1 + capacity := int64(0) + cache, err := NewCapacityLRU(size, capacity) + assert.Nil(t, cache) + assert.Equal(t, storage.ErrCacheCapacityInvalid, err) +} + +func TestNewCapacityLRU(t *testing.T) { + t.Parallel() + + size := 1 + capacity := int64(5) + + cache, err := NewCapacityLRU(size, capacity) + assert.False(t, check.IfNil(cache)) + assert.Nil(t, err) + assert.Equal(t, size, cache.size) + assert.Equal(t, capacity, cache.maxCapacityInBytes) + assert.Equal(t, int64(0), cache.currentCapacityInBytes) + assert.NotNil(t, cache.evictList) + assert.NotNil(t, cache.items) +} + +//------- AddSized + +func TestCapacityLRUCache_AddSizedNegativeSizeInBytesShouldReturn(t *testing.T) { + t.Parallel() + + c := createDefaultCache() + data := []byte("test") + key := "key" + c.AddSized(key, data, -1) + + assert.Equal(t, 0, c.Len()) +} + +func TestCapacityLRUCache_AddSizedSimpleTestShouldWork(t *testing.T) { + t.Parallel() + + c := createDefaultCache() + data := []byte("test") + key := "key" + capacity := int64(5) + c.AddSized(key, data, capacity) + + v, ok := c.Get(key) + assert.True(t, ok) + assert.NotNil(t, v) + assert.Equal(t, data, v) + + keys := c.Keys() + assert.Equal(t, 1, len(keys)) + assert.Equal(t, key, keys[0]) +} + +func TestCapacityLRUCache_AddSizedEvictionByCacheSizeShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(3, 100000) + + keys := []string{"key1", "key2", "key3", "key4", "key5"} + + c.AddSized(keys[0], struct{}{}, 0) + assert.Equal(t, 1, c.Len()) + + c.AddSized(keys[1], struct{}{}, 0) + assert.Equal(t, 2, c.Len()) + + c.AddSized(keys[2], struct{}{}, 0) + assert.Equal(t, 3, c.Len()) + + c.AddSized(keys[3], struct{}{}, 0) + assert.Equal(t, 3, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[3])) + + c.AddSized(keys[4], struct{}{}, 0) + assert.Equal(t, 3, c.Len()) + assert.False(t, c.Contains(keys[1])) + assert.True(t, c.Contains(keys[4])) +} + +func TestCapacityLRUCache_AddSizedEvictionBySizeInBytesShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2", "key3", "key4"} + + c.AddSized(keys[0], struct{}{}, 500) + assert.Equal(t, 1, c.Len()) + + c.AddSized(keys[1], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + + c.AddSized(keys[2], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[2])) + + c.AddSized(keys[3], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[1])) + assert.True(t, c.Contains(keys[3])) +} + +func TestCapacityLRUCache_AddSizedEvictionBySizeInBytesOneLargeElementShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2", "key3", "key4"} + + c.AddSized(keys[0], struct{}{}, 500) + assert.Equal(t, 1, c.Len()) + + c.AddSized(keys[1], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + + c.AddSized(keys[2], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[2])) + + c.AddSized(keys[3], struct{}{}, 500000) + assert.Equal(t, 1, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.False(t, c.Contains(keys[1])) + assert.False(t, c.Contains(keys[2])) + assert.True(t, c.Contains(keys[3])) +} + +func TestCapacityLRUCache_AddSizedEvictionBySizeInBytesOneLargeElementEvictedBySmallElementsShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2", "key3"} + + c.AddSized(keys[0], struct{}{}, 500000) + assert.Equal(t, 1, c.Len()) + + c.AddSized(keys[1], struct{}{}, 500) + assert.Equal(t, 1, c.Len()) + + c.AddSized(keys[2], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[1])) + assert.True(t, c.Contains(keys[2])) +} + +func TestCapacityLRUCache_AddSizedEvictionBySizeInBytesExistingOneLargeElementShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2"} + + c.AddSized(keys[0], struct{}{}, 500) + assert.Equal(t, 1, c.Len()) + + c.AddSized(keys[1], struct{}{}, 500) + assert.Equal(t, 2, c.Len()) + + c.AddSized(keys[0], struct{}{}, 500000) + assert.Equal(t, 1, c.Len()) + assert.True(t, c.Contains(keys[0])) + assert.False(t, c.Contains(keys[1])) +} + +//------- AddSizedIfMissing + +func TestCapacityLRUCache_AddSizedIfMissing(t *testing.T) { + t.Parallel() + + c := createDefaultCache() + data := []byte("data1") + key := "key" + + found, evicted := c.AddSizedIfMissing(key, data, 1) + assert.False(t, found) + assert.False(t, evicted) + + v, ok := c.Get(key) + assert.True(t, ok) + assert.NotNil(t, v) + assert.Equal(t, data, v) + + data2 := []byte("data2") + found, evicted = c.AddSizedIfMissing(key, data2, 1) + assert.True(t, found) + assert.False(t, evicted) + + v, ok = c.Get(key) + assert.True(t, ok) + assert.NotNil(t, v) + assert.Equal(t, data, v) +} + +func TestCapacityLRUCache_AddSizedIfMissingNegativeSizeInBytesShouldReturnFalse(t *testing.T) { + t.Parallel() + + c := createDefaultCache() + data := []byte("data1") + key := "key" + + has, evicted := c.AddSizedIfMissing(key, data, -1) + assert.False(t, has) + assert.False(t, evicted) + assert.Equal(t, 0, c.Len()) +} + +//------- Get + +func TestCapacityLRUCache_GetShouldWork(t *testing.T) { + t.Parallel() + + key := "key" + value := &struct{ A int }{A: 10} + + c := createDefaultCache() + c.AddSized(key, value, 0) + + recovered, exists := c.Get(key) + assert.True(t, value == recovered) //pointer testing + assert.True(t, exists) + + recovered, exists = c.Get("key not found") + assert.Nil(t, recovered) + assert.False(t, exists) +} + +//------- Purge + +func TestCapacityLRUCache_PurgeShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2"} + c.AddSized(keys[0], struct{}{}, 500) + c.AddSized(keys[1], struct{}{}, 500) + + c.Purge() + + assert.Equal(t, 0, c.Len()) + assert.Equal(t, int64(0), c.currentCapacityInBytes) +} + +//------- Peek + +func TestCapacityLRUCache_PeekNotFoundShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + val, found := c.Peek("key not found") + + assert.Nil(t, val) + assert.False(t, found) +} + +func TestCapacityLRUCache_PeekShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + key1 := "key1" + key2 := "key2" + val1 := &struct{}{} + + c.AddSized(key1, val1, 0) + c.AddSized(key2, struct{}{}, 0) + + //at this point key2 is more "recent" than key1 + assert.True(t, c.evictList.Front().Value.(*entry).key == key2) + + val, found := c.Peek(key1) + assert.True(t, val == val1) //pointer testing + assert.True(t, found) + + //recentness should not have been altered + assert.True(t, c.evictList.Front().Value.(*entry).key == key2) +} + +//------- Remove + +func TestCapacityLRUCache_RemoveNotFoundShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + removed := c.Remove("key not found") + + assert.False(t, removed) +} + +func TestCapacityLRUCache_RemovedShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + key1 := "key1" + key2 := "key2" + + c.AddSized(key1, struct{}{}, 0) + c.AddSized(key2, struct{}{}, 0) + + assert.Equal(t, 2, c.Len()) + + c.Remove(key1) + + assert.Equal(t, 1, c.Len()) + assert.True(t, c.Contains(key2)) +} + +// ---------- AddSizedAndReturnEvicted + +func TestCapacityLRUCache_AddSizedAndReturnEvictedNegativeSizeInBytesShouldReturn(t *testing.T) { + t.Parallel() + + c := createDefaultCache() + data := []byte("test") + key := "key" + c.AddSizedAndReturnEvicted(key, data, -1) + + assert.Equal(t, 0, c.Len()) +} + +func TestCapacityLRUCache_AddSizedAndReturnEvictedSimpleTestShouldWork(t *testing.T) { + t.Parallel() + + c := createDefaultCache() + data := []byte("test") + key := "key" + capacity := int64(5) + c.AddSizedAndReturnEvicted(key, data, capacity) + + v, ok := c.Get(key) + assert.True(t, ok) + assert.NotNil(t, v) + assert.Equal(t, data, v) + + keys := c.Keys() + assert.Equal(t, 1, len(keys)) + assert.Equal(t, key, keys[0]) +} + +func TestCapacityLRUCache_AddSizedAndReturnEvictedEvictionByCacheSizeShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(3, 100000) + + keys := []string{"key1", "key2", "key3", "key4", "key5"} + values := []string{"val1", "val2", "val3", "val4", "val5"} + + evicted := c.AddSizedAndReturnEvicted(keys[0], values[0], int64(len(values[0]))) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 1, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[1], values[1], int64(len(values[1]))) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 2, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[2], values[2], int64(len(values[2]))) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 3, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[3], values[3], int64(len(values[3]))) + assert.Equal(t, 3, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[3])) + assert.Equal(t, 1, len(evicted)) + assert.Equal(t, values[0], evicted[keys[0]]) + + evicted = c.AddSizedAndReturnEvicted(keys[4], values[4], int64(len(values[4]))) + assert.Equal(t, 3, c.Len()) + assert.False(t, c.Contains(keys[1])) + assert.True(t, c.Contains(keys[4])) + assert.Equal(t, 1, len(evicted)) + assert.Equal(t, values[1], evicted[keys[1]]) +} + +func TestCapacityLRUCache_AddSizedAndReturnEvictedEvictionBySizeInBytesShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2", "key3", "key4"} + values := []string{"val1", "val2", "val3", "val4"} + + evicted := c.AddSizedAndReturnEvicted(keys[0], values[0], 500) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 1, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[1], values[1], 500) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 2, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[2], values[2], 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[2])) + assert.Equal(t, 1, len(evicted)) + assert.Equal(t, values[0], evicted[keys[0]]) + + evicted = c.AddSizedAndReturnEvicted(keys[3], values[3], 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[1])) + assert.True(t, c.Contains(keys[3])) + assert.Equal(t, 1, len(evicted)) + assert.Equal(t, values[1], evicted[keys[1]]) +} + +func TestCapacityLRUCache_AddSizedAndReturnEvictedEvictionBySizeInBytesOneLargeElementShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2", "key3", "key4"} + values := []string{"val1", "val2", "val3", "val4"} + + evicted := c.AddSizedAndReturnEvicted(keys[0], values[0], 500) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 1, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[1], values[1], 500) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 2, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[2], values[2], 500) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[2])) + assert.Equal(t, 1, len(evicted)) + assert.Equal(t, values[0], evicted[keys[0]]) + + evicted = c.AddSizedAndReturnEvicted(keys[3], values[3], 500000) + assert.Equal(t, 1, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.False(t, c.Contains(keys[1])) + assert.False(t, c.Contains(keys[2])) + assert.True(t, c.Contains(keys[3])) + assert.Equal(t, 2, len(evicted)) + assert.Equal(t, values[1], evicted[keys[1]]) + assert.Equal(t, values[2], evicted[keys[2]]) +} + +func TestCapacityLRUCache_AddSizedAndReturnEvictedEvictionBySizeInBytesOneLargeElementEvictedBySmallElementsShouldWork(t *testing.T) { + t.Parallel() + + c, _ := NewCapacityLRU(100000, 1000) + + keys := []string{"key1", "key2", "key3"} + values := []string{"val1", "val2", "val3"} + + evicted := c.AddSizedAndReturnEvicted(keys[0], values[0], 500000) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 1, c.Len()) + + evicted = c.AddSizedAndReturnEvicted(keys[1], values[1], 500) + assert.Equal(t, 1, c.Len()) + assert.Equal(t, 1, len(evicted)) + assert.Equal(t, values[0], evicted[keys[0]]) + + evicted = c.AddSizedAndReturnEvicted(keys[2], values[2], 500) + assert.Equal(t, 0, len(evicted)) + assert.Equal(t, 2, c.Len()) + assert.False(t, c.Contains(keys[0])) + assert.True(t, c.Contains(keys[1])) + assert.True(t, c.Contains(keys[2])) +} diff --git a/storage/lrucache/export_test.go b/storage/lrucache/export_test.go new file mode 100644 index 000000000..92889ed26 --- /dev/null +++ b/storage/lrucache/export_test.go @@ -0,0 +1,5 @@ +package lrucache + +func (c *lruCache) AddedDataHandlers() map[string]func(key []byte, value interface{}) { + return c.mapDataHandlers +} diff --git a/storage/lrucache/lrucache.go b/storage/lrucache/lrucache.go new file mode 100644 index 000000000..b8d0c44ba --- /dev/null +++ b/storage/lrucache/lrucache.go @@ -0,0 +1,191 @@ +package lrucache + +import ( + "sync" + + "github.com/ElrondNetwork/elrond-go-core/storage" + "github.com/ElrondNetwork/elrond-go-core/storage/lrucache/capacity" + lru "github.com/hashicorp/golang-lru" +) + +var _ storage.Cacher = (*lruCache)(nil) + +// LRUCache implements a Least Recently Used eviction cache +type lruCache struct { + cache storage.SizedLRUCacheHandler + maxsize int + + mutAddedDataHandlers sync.RWMutex + mapDataHandlers map[string]func(key []byte, value interface{}) +} + +// NewCache creates a new LRU cache instance +func NewCache(size int) (*lruCache, error) { + cache, err := lru.New(size) + if err != nil { + return nil, err + } + + c := createLRUCache(size, cache) + + return c, nil +} + +// NewCacheWithEviction creates a new sized LRU cache instance with eviction function +func NewCacheWithEviction(size int, onEvicted func(key interface{}, value interface{})) (*lruCache, error) { + cache, err := lru.NewWithEvict(size, onEvicted) + if err != nil { + return nil, err + } + + c := createLRUCache(size, cache) + + return c, nil +} + +func createLRUCache(size int, cache *lru.Cache) *lruCache { + c := &lruCache{ + cache: &simpleLRUCacheAdapter{ + LRUCacheHandler: cache, + }, + maxsize: size, + mutAddedDataHandlers: sync.RWMutex{}, + mapDataHandlers: make(map[string]func(key []byte, value interface{})), + } + return c +} + +// NewCacheWithSizeInBytes creates a new sized LRU cache instance +func NewCacheWithSizeInBytes(size int, sizeInBytes int64) (*lruCache, error) { + cache, err := capacity.NewCapacityLRU(size, sizeInBytes) + if err != nil { + return nil, err + } + + c := &lruCache{ + cache: cache, + maxsize: size, + mutAddedDataHandlers: sync.RWMutex{}, + mapDataHandlers: make(map[string]func(key []byte, value interface{})), + } + + return c, nil +} + +// Clear is used to completely clear the cache. +func (c *lruCache) Clear() { + c.cache.Purge() +} + +// Put adds a value to the cache. Returns true if an eviction occurred. +func (c *lruCache) Put(key []byte, value interface{}, sizeInBytes int) (evicted bool) { + evicted = c.cache.AddSized(string(key), value, int64(sizeInBytes)) + + c.callAddedDataHandlers(key, value) + + return evicted +} + +// RegisterHandler registers a new handler to be called when a new data is added +func (c *lruCache) RegisterHandler(handler func(key []byte, value interface{}), id string) { + if handler == nil { + return + } + + c.mutAddedDataHandlers.Lock() + c.mapDataHandlers[id] = handler + c.mutAddedDataHandlers.Unlock() +} + +// UnRegisterHandler removes the handler from the list +func (c *lruCache) UnRegisterHandler(id string) { + c.mutAddedDataHandlers.Lock() + delete(c.mapDataHandlers, id) + c.mutAddedDataHandlers.Unlock() +} + +// Get looks up a key's value from the cache. +func (c *lruCache) Get(key []byte) (value interface{}, ok bool) { + return c.cache.Get(string(key)) +} + +// Has checks if a key is in the cache, without updating the +// recent-ness or deleting it for being stale. +func (c *lruCache) Has(key []byte) bool { + return c.cache.Contains(string(key)) +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *lruCache) Peek(key []byte) (value interface{}, ok bool) { + v, ok := c.cache.Peek(string(key)) + + if !ok { + return nil, ok + } + + return v, ok +} + +// HasOrAdd 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 *lruCache) HasOrAdd(key []byte, value interface{}, sizeInBytes int) (has, added bool) { + has, _ = c.cache.AddSizedIfMissing(string(key), value, int64(sizeInBytes)) + + if !has { + c.callAddedDataHandlers(key, value) + } + + return has, !has +} + +func (c *lruCache) callAddedDataHandlers(key []byte, value interface{}) { + c.mutAddedDataHandlers.RLock() + for _, handler := range c.mapDataHandlers { + go handler(key, value) + } + c.mutAddedDataHandlers.RUnlock() +} + +// Remove removes the provided key from the cache. +func (c *lruCache) Remove(key []byte) { + c.cache.Remove(string(key)) +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *lruCache) Keys() [][]byte { + res := c.cache.Keys() + r := make([][]byte, len(res)) + + for i := 0; i < len(res); i++ { + r[i] = []byte(res[i].(string)) + } + + return r +} + +// Len returns the number of items in the cache. +func (c *lruCache) Len() int { + return c.cache.Len() +} + +// SizeInBytesContained returns the size in bytes of all contained elements +func (c *lruCache) SizeInBytesContained() uint64 { + return c.cache.SizeInBytesContained() +} + +// MaxSize returns the maximum number of items which can be stored in cache. +func (c *lruCache) MaxSize() int { + return c.maxsize +} + +// Close does nothing for this cacher implementation +func (c *lruCache) Close() error { + return nil +} + +// IsInterfaceNil returns true if there is no value under the interface +func (c *lruCache) IsInterfaceNil() bool { + return c == nil +} diff --git a/storage/lrucache/lrucache_test.go b/storage/lrucache/lrucache_test.go new file mode 100644 index 000000000..7e34bdcaa --- /dev/null +++ b/storage/lrucache/lrucache_test.go @@ -0,0 +1,420 @@ +package lrucache_test + +import ( + "bytes" + "fmt" + "sync" + "testing" + "time" + + "github.com/ElrondNetwork/elrond-go-core/core/check" + "github.com/ElrondNetwork/elrond-go-core/storage" + "github.com/ElrondNetwork/elrond-go-core/storage/lrucache" + "github.com/stretchr/testify/assert" +) + +var timeoutWaitForWaitGroups = time.Second * 2 + +//------- NewCache + +func TestNewCache_BadSizeShouldErr(t *testing.T) { + t.Parallel() + + c, err := lrucache.NewCache(0) + + assert.True(t, check.IfNil(c)) + assert.NotNil(t, err) +} + +func TestNewCache_ShouldWork(t *testing.T) { + t.Parallel() + + c, err := lrucache.NewCache(1) + + assert.False(t, check.IfNil(c)) + assert.Nil(t, err) +} + +//------- NewCacheWithSizeInBytes + +func TestNewCacheWithSizeInBytes_BadSizeShouldErr(t *testing.T) { + t.Parallel() + + c, err := lrucache.NewCacheWithSizeInBytes(0, 100000) + + assert.True(t, check.IfNil(c)) + assert.Equal(t, storage.ErrCacheSizeInvalid, err) +} + +func TestNewCacheWithSizeInBytes_BadSizeInBytesShouldErr(t *testing.T) { + t.Parallel() + + c, err := lrucache.NewCacheWithSizeInBytes(1, 0) + + assert.True(t, check.IfNil(c)) + assert.Equal(t, storage.ErrCacheCapacityInvalid, err) +} + +func TestNewCacheWithSizeInBytes_ShouldWork(t *testing.T) { + t.Parallel() + + c, err := lrucache.NewCacheWithSizeInBytes(1, 100000) + + assert.False(t, check.IfNil(c)) + assert.Nil(t, err) +} + +func TestLRUCache_PutNotPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key"), []byte("value") + c, _ := lrucache.NewCache(10) + + l := c.Len() + + assert.Zero(t, l, "cache expected to be empty") + + c.Put(key, val, 0) + l = c.Len() + + assert.Equal(t, l, 1, "cache size expected 1 but found %d", l) +} + +func TestLRUCache_PutPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key"), []byte("value") + c, _ := lrucache.NewCache(10) + + c.Put(key, val, 0) + c.Put(key, val, 0) + + l := c.Len() + assert.Equal(t, l, 1, "cache size expected 1 but found %d", l) +} + +func TestLRUCache_PutPresentRewrite(t *testing.T) { + t.Parallel() + + key := []byte("key") + val1 := []byte("value1") + val2 := []byte("value2") + c, _ := lrucache.NewCache(10) + + c.Put(key, val1, 0) + c.Put(key, val2, 0) + + l := c.Len() + assert.Equal(t, l, 1, "cache size expected 1 but found %d", l) + recoveredVal, has := c.Get(key) + assert.True(t, has) + assert.Equal(t, val2, recoveredVal) +} + +func TestLRUCache_GetNotPresent(t *testing.T) { + t.Parallel() + + key := []byte("key1") + c, _ := lrucache.NewCache(10) + + v, ok := c.Get(key) + + assert.False(t, ok, "value %s not expected to be found", v) +} + +func TestLRUCache_GetPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key2"), []byte("value2") + c, _ := lrucache.NewCache(10) + + c.Put(key, val, 0) + + v, ok := c.Get(key) + + assert.True(t, ok, "value expected but not found") + assert.Equal(t, val, v) +} + +func TestLRUCache_HasNotPresent(t *testing.T) { + t.Parallel() + + key := []byte("key3") + c, _ := lrucache.NewCache(10) + + found := c.Has(key) + + assert.False(t, found, "key %s not expected to be found", key) +} + +func TestLRUCache_HasPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key4"), []byte("value4") + c, _ := lrucache.NewCache(10) + + c.Put(key, val, 0) + + found := c.Has(key) + + assert.True(t, found, "value expected but not found") +} + +func TestLRUCache_PeekNotPresent(t *testing.T) { + t.Parallel() + + key := []byte("key5") + c, _ := lrucache.NewCache(10) + + _, ok := c.Peek(key) + + assert.False(t, ok, "not expected to find key %s", key) +} + +func TestLRUCache_PeekPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key6"), []byte("value6") + c, _ := lrucache.NewCache(10) + + c.Put(key, val, 0) + v, ok := c.Peek(key) + + assert.True(t, ok, "value expected but not found") + assert.Equal(t, val, v, "expected to find %s but found %s", val, v) +} + +func TestLRUCache_HasOrAddNotPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key7"), []byte("value7") + c, _ := lrucache.NewCache(10) + + _, ok := c.Peek(key) + assert.False(t, ok, "not expected to find key %s", key) + + c.HasOrAdd(key, val, 0) + v, ok := c.Peek(key) + assert.True(t, ok, "value expected but not found") + assert.Equal(t, val, v, "expected to find %s but found %s", val, v) +} + +func TestLRUCache_HasOrAddPresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key8"), []byte("value8") + c, _ := lrucache.NewCache(10) + + _, ok := c.Peek(key) + + assert.False(t, ok, "not expected to find key %s", key) + + c.HasOrAdd(key, val, 0) + v, ok := c.Peek(key) + + assert.True(t, ok, "value expected but not found") + assert.Equal(t, val, v, "expected to find %s but found %s", val, v) +} + +func TestLRUCache_RemoveNotPresent(t *testing.T) { + t.Parallel() + + key := []byte("key9") + c, _ := lrucache.NewCache(10) + + found := c.Has(key) + + assert.False(t, found, "not expected to find key %s", key) + + c.Remove(key) + found = c.Has(key) + + assert.False(t, found, "not expected to find key %s", key) +} + +func TestLRUCache_RemovePresent(t *testing.T) { + t.Parallel() + + key, val := []byte("key10"), []byte("value10") + c, _ := lrucache.NewCache(10) + + c.Put(key, val, 0) + found := c.Has(key) + + assert.True(t, found, "expected to find key %s", key) + + c.Remove(key) + found = c.Has(key) + + assert.False(t, found, "not expected to find key %s", key) +} + +func TestLRUCache_Keys(t *testing.T) { + t.Parallel() + + c, _ := lrucache.NewCache(10) + + for i := 0; i < 20; i++ { + key, val := []byte(fmt.Sprintf("key%d", i)), []byte(fmt.Sprintf("value%d", i)) + c.Put(key, val, 0) + } + + keys := c.Keys() + + // check also that cache size does not grow over the capacity + assert.Equal(t, 10, len(keys), "expected cache size 10 but current size %d", len(keys)) +} + +func TestLRUCache_Len(t *testing.T) { + t.Parallel() + + c, _ := lrucache.NewCache(10) + + for i := 0; i < 20; i++ { + key, val := []byte(fmt.Sprintf("key%d", i)), []byte(fmt.Sprintf("value%d", i)) + c.Put(key, val, 0) + } + + l := c.Len() + + assert.Equal(t, 10, l, "expected cache size 10 but current size %d", l) +} + +func TestLRUCache_Clear(t *testing.T) { + t.Parallel() + + c, _ := lrucache.NewCache(10) + + for i := 0; i < 5; i++ { + key, val := []byte(fmt.Sprintf("key%d", i)), []byte(fmt.Sprintf("value%d", i)) + c.Put(key, val, 0) + } + + l := c.Len() + + assert.Equal(t, 5, l, "expected size 5, got %d", l) + + c.Clear() + l = c.Len() + + assert.Zero(t, l, "expected size 0, got %d", l) +} + +func TestLRUCache_CacherRegisterAddedDataHandlerNilHandlerShouldIgnore(t *testing.T) { + t.Parallel() + + c, _ := lrucache.NewCache(100) + c.RegisterHandler(nil, "") + + assert.Equal(t, 0, len(c.AddedDataHandlers())) +} + +func TestLRUCache_CacherRegisterPutAddedDataHandlerShouldWork(t *testing.T) { + t.Parallel() + + wg := sync.WaitGroup{} + wg.Add(1) + chDone := make(chan bool) + + f := func(key []byte, value interface{}) { + if !bytes.Equal([]byte("aaaa"), key) { + return + } + + wg.Done() + } + + go func() { + wg.Wait() + chDone <- true + }() + + c, _ := lrucache.NewCache(100) + c.RegisterHandler(f, "") + c.Put([]byte("aaaa"), "bbbb", 0) + + select { + case <-chDone: + case <-time.After(timeoutWaitForWaitGroups): + assert.Fail(t, "should have been called") + return + } + + assert.Equal(t, 1, len(c.AddedDataHandlers())) +} + +func TestLRUCache_CacherRegisterHasOrAddAddedDataHandlerShouldWork(t *testing.T) { + t.Parallel() + + wg := sync.WaitGroup{} + wg.Add(1) + chDone := make(chan bool) + + f := func(key []byte, value interface{}) { + if !bytes.Equal([]byte("aaaa"), key) { + return + } + + wg.Done() + } + + go func() { + wg.Wait() + chDone <- true + }() + + c, _ := lrucache.NewCache(100) + c.RegisterHandler(f, "") + c.HasOrAdd([]byte("aaaa"), "bbbb", 0) + + select { + case <-chDone: + case <-time.After(timeoutWaitForWaitGroups): + assert.Fail(t, "should have been called") + return + } + + assert.Equal(t, 1, len(c.AddedDataHandlers())) +} + +func TestLRUCache_CacherRegisterHasOrAddAddedDataHandlerNotAddedShouldNotCall(t *testing.T) { + t.Parallel() + + wg := sync.WaitGroup{} + wg.Add(1) + chDone := make(chan bool) + + f := func(key []byte, value interface{}) { + wg.Done() + } + + go func() { + wg.Wait() + chDone <- true + }() + + c, _ := lrucache.NewCache(100) + //first add, no call + c.HasOrAdd([]byte("aaaa"), "bbbb", 0) + c.RegisterHandler(f, "") + //second add, should not call as the data was found + c.HasOrAdd([]byte("aaaa"), "bbbb", 0) + + select { + case <-chDone: + assert.Fail(t, "should have not been called") + return + case <-time.After(timeoutWaitForWaitGroups): + } + + assert.Equal(t, 1, len(c.AddedDataHandlers())) +} + +func TestLRUCache_CloseShouldNotErr(t *testing.T) { + t.Parallel() + + c, _ := lrucache.NewCache(1) + + err := c.Close() + assert.Nil(t, err) +} diff --git a/storage/lrucache/simpleLRUCacheAdapter.go b/storage/lrucache/simpleLRUCacheAdapter.go new file mode 100644 index 000000000..6affa266b --- /dev/null +++ b/storage/lrucache/simpleLRUCacheAdapter.go @@ -0,0 +1,23 @@ +package lrucache + +import "github.com/ElrondNetwork/elrond-go-core/storage" + +// simpleLRUCacheAdapter provides an adapter between LRUCacheHandler and SizeLRUCacheHandler +type simpleLRUCacheAdapter struct { + storage.LRUCacheHandler +} + +// AddSized calls the Add method without the size in bytes parameter +func (slca *simpleLRUCacheAdapter) AddSized(key, value interface{}, _ int64) bool { + return slca.Add(key, value) +} + +// AddSizedIfMissing calls ContainsOrAdd without the size in bytes parameter +func (slca *simpleLRUCacheAdapter) AddSizedIfMissing(key, value interface{}, _ int64) (ok, evicted bool) { + return slca.ContainsOrAdd(key, value) +} + +// SizeInBytesContained returns 0 +func (slca *simpleLRUCacheAdapter) SizeInBytesContained() uint64 { + return 0 +}