317 lines
6.3 KiB
Go
317 lines
6.3 KiB
Go
package ridata
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/dsoprea/go-logging"
|
|
)
|
|
|
|
var (
|
|
// ErrLruEmpty indicates that the LRU is empty..
|
|
ErrLruEmpty = errors.New("lru is empty")
|
|
)
|
|
|
|
// LruKey is the type of an LRU key.
|
|
type LruKey interface{}
|
|
|
|
// LruItem is the interface that any item we add to the LRU must satisfy.
|
|
type LruItem interface {
|
|
Id() LruKey
|
|
}
|
|
|
|
type lruNode struct {
|
|
before *lruNode
|
|
after *lruNode
|
|
item LruItem
|
|
}
|
|
|
|
// String will return a string representation of the node.
|
|
func (ln *lruNode) String() string {
|
|
var beforePhrase string
|
|
if ln.before != nil {
|
|
beforePhrase = fmt.Sprintf("%v", ln.before.item.Id())
|
|
} else {
|
|
beforePhrase = "<NULL>"
|
|
}
|
|
|
|
var afterPhrase string
|
|
if ln.after != nil {
|
|
afterPhrase = fmt.Sprintf("%v", ln.after.item.Id())
|
|
} else {
|
|
afterPhrase = "<NULL>"
|
|
}
|
|
|
|
return fmt.Sprintf("[%v] BEFORE=[%s] AFTER=[%s]", ln.item.Id(), beforePhrase, afterPhrase)
|
|
}
|
|
|
|
type lruEventFunc func(id LruKey) (err error)
|
|
|
|
// Lru establises an LRU of IDs of any type.
|
|
type Lru struct {
|
|
top *lruNode
|
|
bottom *lruNode
|
|
lookup map[LruKey]*lruNode
|
|
maxSize int
|
|
dropCb lruEventFunc
|
|
}
|
|
|
|
// NewLru returns a new instance.
|
|
func NewLru(maxSize int) *Lru {
|
|
return &Lru{
|
|
lookup: make(map[LruKey]*lruNode),
|
|
maxSize: maxSize,
|
|
}
|
|
}
|
|
|
|
// SetDropCb sets a callback that will be triggered whenever an item ages out
|
|
// or is manually dropped.
|
|
func (lru *Lru) SetDropCb(cb lruEventFunc) {
|
|
lru.dropCb = cb
|
|
}
|
|
|
|
// Count returns the number of items in the LRU.
|
|
func (lru *Lru) Count() int {
|
|
return len(lru.lookup)
|
|
}
|
|
|
|
// MaxCount returns the maximum number of items the LRU can contain.
|
|
func (lru *Lru) MaxCount() int {
|
|
return lru.maxSize
|
|
}
|
|
|
|
// IsFull will return true if at capacity.
|
|
func (lru *Lru) IsFull() bool {
|
|
return lru.Count() == lru.maxSize
|
|
}
|
|
|
|
// Exists will do a membership check for the given key.
|
|
func (lru *Lru) Exists(id LruKey) bool {
|
|
_, found := lru.lookup[id]
|
|
return found
|
|
}
|
|
|
|
// FindPosition will return the numerical position in the list. Since the LRU
|
|
// will never be very large, this call is not expensive, per se. But, it *is*
|
|
// O(n) and any call to us will compound with any loops you happen to wrap us
|
|
// into.
|
|
func (lru *Lru) FindPosition(id LruKey) int {
|
|
node, found := lru.lookup[id]
|
|
if found == false {
|
|
return -1
|
|
}
|
|
|
|
position := 0
|
|
for ; node.before != nil; node = node.before {
|
|
position++
|
|
}
|
|
|
|
return position
|
|
}
|
|
|
|
// Get touches the cache and returns the data.
|
|
func (lru *Lru) Get(id LruKey) (found bool, item LruItem, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
if node, found := lru.lookup[id]; found == true {
|
|
_, _, err := lru.Set(node.item)
|
|
log.PanicIf(err)
|
|
|
|
return true, node.item, nil
|
|
}
|
|
|
|
return false, nil, nil
|
|
}
|
|
|
|
// Set bumps an item to the front of the LRU. It will be added if it doesn't
|
|
// already exist. If as a result of adding an item the LRU exceeds the maximum
|
|
// size, the least recently used item will be discarded.
|
|
//
|
|
// If it was not previously in the LRU, `added` will be `true`.
|
|
func (lru *Lru) Set(item LruItem) (added bool, droppedItem LruItem, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): !! Add tests for added/droppedItem returns.
|
|
|
|
id := item.Id()
|
|
|
|
node, found := lru.lookup[id]
|
|
|
|
added = (found == false)
|
|
|
|
if found == true {
|
|
// It's already at the front.
|
|
if node.before == nil {
|
|
return added, nil, nil
|
|
}
|
|
|
|
// If we were at the bottom, the bottom is now whatever was upstream of
|
|
// us.
|
|
if lru.bottom == node {
|
|
lru.bottom = lru.bottom.before
|
|
}
|
|
|
|
// Prune.
|
|
if node.before != nil {
|
|
node.before.after = node.after
|
|
node.before = nil
|
|
}
|
|
|
|
// Insert at the front.
|
|
node.after = lru.top
|
|
|
|
// Point the head of the list to us.
|
|
lru.top = node
|
|
} else {
|
|
node = &lruNode{
|
|
after: lru.top,
|
|
item: item,
|
|
}
|
|
|
|
lru.lookup[id] = node
|
|
|
|
// Point the head of the list to us.
|
|
lru.top = node
|
|
}
|
|
|
|
// Update the link from the downstream node.
|
|
if node.after != nil {
|
|
node.after.before = node
|
|
}
|
|
|
|
if lru.bottom == nil {
|
|
lru.bottom = node
|
|
}
|
|
|
|
if len(lru.lookup) > lru.maxSize {
|
|
lastItemId := lru.Oldest()
|
|
lastNode := lru.lookup[lastItemId]
|
|
|
|
found, err := lru.Drop(lastItemId)
|
|
log.PanicIf(err)
|
|
|
|
if found == false {
|
|
log.Panicf("drop of old item was ineffectual")
|
|
}
|
|
|
|
droppedItem = lastNode.item
|
|
}
|
|
|
|
return added, droppedItem, nil
|
|
}
|
|
|
|
// Drop discards the given item.
|
|
func (lru *Lru) Drop(id LruKey) (found bool, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
node, found := lru.lookup[id]
|
|
if found == false {
|
|
return false, nil
|
|
}
|
|
|
|
// Keep the `top` node up-to-date.
|
|
if node.before == nil {
|
|
lru.top = node.after
|
|
}
|
|
|
|
// Keep the `bottom` node up-to-date.
|
|
if node.after == nil {
|
|
lru.bottom = node.before
|
|
}
|
|
|
|
// Detach us from the previous node and link that node to the one after us.
|
|
if node.before != nil {
|
|
node.before.after = node.after
|
|
}
|
|
|
|
delete(lru.lookup, id)
|
|
|
|
if lru.dropCb != nil {
|
|
err := lru.dropCb(id)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// Newest returns the most recently used ID.
|
|
func (lru *Lru) Newest() LruKey {
|
|
if lru.top != nil {
|
|
return lru.top.item.Id()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Oldest returns the least recently used ID.
|
|
func (lru *Lru) Oldest() LruKey {
|
|
if lru.bottom != nil {
|
|
return lru.bottom.item.Id()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// All returns a list of all IDs.
|
|
func (lru *Lru) All() []LruKey {
|
|
collected := make([]LruKey, len(lru.lookup))
|
|
i := 0
|
|
for value := range lru.lookup {
|
|
collected[i] = value
|
|
i++
|
|
}
|
|
|
|
return collected
|
|
}
|
|
|
|
// PopOldest will pop the oldest entry out of the LRU and return it. It will
|
|
// return ErrLruEmpty when empty.
|
|
func (lru *Lru) PopOldest() (item LruItem, err error) {
|
|
lk := lru.Oldest()
|
|
if lk == nil {
|
|
return nil, ErrLruEmpty
|
|
}
|
|
|
|
node := lru.lookup[lk]
|
|
if node == nil {
|
|
log.Panicf("something went wrong resolving the oldest item")
|
|
}
|
|
|
|
found, err := lru.Drop(lk)
|
|
log.PanicIf(err)
|
|
|
|
if found == false {
|
|
log.Panicf("something went wrong dropping the oldest item")
|
|
}
|
|
|
|
return node.item, nil
|
|
}
|
|
|
|
// Dump returns a list of all IDs.
|
|
func (lru *Lru) Dump() {
|
|
fmt.Printf("Count: (%d)\n", len(lru.lookup))
|
|
fmt.Printf("\n")
|
|
|
|
fmt.Printf("Top: %v\n", lru.top)
|
|
fmt.Printf("Bottom: %v\n", lru.bottom)
|
|
fmt.Printf("\n")
|
|
|
|
i := 0
|
|
for ptr := lru.top; ptr != nil; ptr = ptr.after {
|
|
fmt.Printf("%03d: %s\n", i, ptr)
|
|
i++
|
|
}
|
|
}
|