Skip to content

Commit

Permalink
Add lru package
Browse files Browse the repository at this point in the history
This is a simple generic thread safe LRU cache with the minimum
functionality for caching limited number of qcow2 L2 tables.

Signed-off-by: Nir Soffer <[email protected]>
  • Loading branch information
nirs committed Oct 17, 2024
1 parent 6bc97fc commit 6503427
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
70 changes: 70 additions & 0 deletions lru/lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package lru

import (
"container/list"
"sync"
)

// Cache keeps recently used values. Safe for concurrent use by multiple
// goroutines.
type Cache[K comparable, V any] struct {
mutex sync.Mutex
entries map[K]*list.Element
recentlyUsed *list.List
capacity int
}

type cacheEntry[K comparable, V any] struct {
Key K
Value V
}

// New returns a new empty cache that can hold up to capacity items.
func New[K comparable, V any](capacity int) *Cache[K, V] {
return &Cache[K, V]{
entries: make(map[K]*list.Element),
recentlyUsed: list.New(),
capacity: capacity,
}
}

// Get returns the value stored in the cache for a key, or zero value if no
// value is present. The ok result indicates whether value was found in the
// cache.
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mutex.Lock()
defer c.mutex.Unlock()

if elem, ok := c.entries[key]; ok {
c.recentlyUsed.MoveToFront(elem)
entry := elem.Value.(*cacheEntry[K, V])
return entry.Value, true
}

var missing V
return missing, false
}

// Add adds key and value to the cache. If the cache is full, the oldest entry
// is removed. If key is already in the cache, value replaces the cached value.
func (c *Cache[K, V]) Add(key K, value V) {
c.mutex.Lock()
defer c.mutex.Unlock()

if elem, ok := c.entries[key]; ok {
c.recentlyUsed.MoveToFront(elem)
entry := elem.Value.(*cacheEntry[K, V])
entry.Value = value
return
}

if len(c.entries) >= c.capacity {
oldest := c.recentlyUsed.Back()
c.recentlyUsed.Remove(oldest)
entry := oldest.Value.(*cacheEntry[K, V])
delete(c.entries, entry.Key)
}

entry := &cacheEntry[K, V]{Key: key, Value: value}
c.entries[key] = c.recentlyUsed.PushFront(entry)
}
61 changes: 61 additions & 0 deletions lru/lru_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package lru

import (
"testing"
)

type item struct {
key int
value string
}

func TestCache(t *testing.T) {
items := []item{{0, "0"}, {1, "1"}, {2, "2"}, {3, "3"}, {4, "4"}}
cache := New[int, string](5)

// Cache is empty.
for _, i := range items {
if _, ok := cache.Get(i.key); ok {
t.Errorf("key %d in cache", i.key)
}
}

// Add all items to cache.
for _, i := range items {
cache.Add(i.key, i.value)
}

// Verify that all items are cached.
for _, i := range items {
if value, ok := cache.Get(i.key); !ok {
t.Errorf("cached value for %d missing", i.key)
} else if value != i.value {
t.Errorf("expected %q, got %q", value, i.value)
}
}

// Adding next item will remove the least used item (0).
cache.Add(5, "5")

// New item in cache.
if value, ok := cache.Get(5); !ok {
t.Errorf("cached value for 5 missing")
} else if value != "5" {
t.Errorf("expected \"5\", got %q", value)
}

// Removed item not in cache.
if _, ok := cache.Get(0); ok {
t.Error("key 0 in cache")
}

// Rest of items not affected.
for _, i := range items[1:] {
if value, ok := cache.Get(i.key); !ok {
t.Errorf("cached value for %d missing", i.key)
} else if value != i.value {
t.Errorf("expected %q, got %q", value, i.value)
}
}

}

0 comments on commit 6503427

Please sign in to comment.