Skip to content

Commit

Permalink
feat/utils: lru cache (#3420)
Browse files Browse the repository at this point in the history
kanishkatn authored Aug 4, 2023
1 parent 2eb6313 commit e0fa0d9
Showing 2 changed files with 154 additions and 0 deletions.
95 changes: 95 additions & 0 deletions lib/utils/lru-cache/lru_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

// Package lrucache provides a generic LRU (Least Recently Used) cache implementation in Go,
// capable of storing key-value pairs of any comparable key type and any value type. It supports
// concurrent read and write operations and automatically evicts the least recently used item
// when the cache reaches its capacity. The cache is backed by a doubly linked list and a map
// for fast access and eviction.
//
// Example usage:
// cache := NewLRUCache[string, string](100) // Create a cache with a capacity of 100
// cache.Put("key1", value1)
// val1 := cache.Get("key1")
//
// Note: The LRUCache does not automatically resize, and values must be comparable.
//

package lrucache

import (
"container/list"
"sync"
)

// DefaultLRUCapacity is the default capacity of the LRU cache.
const DefaultLRUCapacity = 20

// LRUCache represents the LRU cache.
type LRUCache[K comparable, V any] struct {
sync.RWMutex
capacity uint
cache map[K]*list.Element
lruList *list.List
}

// Entry represents an item in the cache.
type Entry[K comparable, V any] struct {
key K
value V
}

// NewLRUCache creates a new LRU cache with the specified capacity.
func NewLRUCache[K comparable, V any](capacity uint) *LRUCache[K, V] {
if capacity < 1 {
capacity = DefaultLRUCapacity
}

return &LRUCache[K, V]{
capacity: capacity,
cache: make(map[K]*list.Element),
lruList: list.New(),
}
}

// Get retrieves the value associated with the given key from the cache.
func (c *LRUCache[K, V]) Get(key K) V {
c.RLock()
defer c.RUnlock()

if elem, exists := c.cache[key]; exists {
c.lruList.MoveToFront(elem)
return elem.Value.(*Entry[K, V]).value
}

var zeroV V
return zeroV
}

// Put adds a key-value pair to the cache.
func (c *LRUCache[K, V]) Put(key K, value V) {
c.Lock()
defer c.Unlock()

// If the key already exists in the cache, update its value and move it to the front.
if elem, exists := c.cache[key]; exists {
elem.Value.(*Entry[K, V]).value = value
c.lruList.MoveToFront(elem)
return
}

// If the cache is full, remove the least recently used item (from the back of the list).
if len(c.cache) >= int(c.capacity) {
// Get the least recently used item (back of the list).
lastElem := c.lruList.Back()
if lastElem != nil {
delete(c.cache, lastElem.Value.(*Entry[K, V]).key)
c.lruList.Remove(lastElem)
}
}

// Add the new key-value pair to the cache (at the front of the list).
newEntry := &Entry[K, V]{key: key, value: value}
newElem := c.lruList.PushFront(newEntry)
c.cache[key] = newElem
}
59 changes: 59 additions & 0 deletions lib/utils/lru-cache/lru_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2023 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package lrucache

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestLRUCache(t *testing.T) {
cache := NewLRUCache[int, string](2)

t.Run("TestBasicOperations", func(t *testing.T) {
cache.Put(1, "Alice")
cache.Put(2, "Bob")

v := cache.Get(1)
require.Equal(t, "Alice", v)

v = cache.Get(2)
require.Equal(t, "Bob", v)
})

t.Run("TestUpdateExistingKey", func(t *testing.T) {
cache.Put(1, "Alice")

// Update the value of an existing key.
cache.Put(1, "Alice Smith")

v := cache.Get(1)
require.Equal(t, "Alice Smith", v)
})

t.Run("TestCacheEviction", func(t *testing.T) {
cache.Put(1, "Alice")
cache.Put(2, "Bob")

// This will evict 1 (least recently used).
cache.Put(4, "Dave")

v := cache.Get(1)
require.Equal(t, "", v)

v = cache.Get(2)
require.Equal(t, "Bob", v)

v = cache.Get(4)
require.Equal(t, "Dave", v)
})

t.Run("TestRetrieveNonExistingKey", func(t *testing.T) {
cache.Put(1, "Alice")

v := cache.Get(999)
require.Equal(t, "", v)
})
}

0 comments on commit e0fa0d9

Please sign in to comment.