Skip to content

Commit

Permalink
Merge pull request #57 from ElrondNetwork/lru-cache-from-elrond-go
Browse files Browse the repository at this point in the history
copied LRU cache from elrond-go
  • Loading branch information
sasurobert authored Jan 12, 2022
2 parents b7a054d + 5ad56d3 commit db36097
Show file tree
Hide file tree
Showing 10 changed files with 1,505 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
11 changes: 11 additions & 0 deletions storage/errors.go
Original file line number Diff line number Diff line change
@@ -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")
69 changes: 69 additions & 0 deletions storage/interface.go
Original file line number Diff line number Diff line change
@@ -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()
}
284 changes: 284 additions & 0 deletions storage/lrucache/capacity/capacityLRUCache.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit db36097

Please sign in to comment.