From e0fa0d9553f17bf757942b47dd361fbc1f5ca661 Mon Sep 17 00:00:00 2001 From: Kanishka Date: Thu, 3 Aug 2023 23:51:01 -0600 Subject: [PATCH] feat/utils: lru cache (#3420) --- lib/utils/lru-cache/lru_cache.go | 95 +++++++++++++++++++++++++++ lib/utils/lru-cache/lru_cache_test.go | 59 +++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 lib/utils/lru-cache/lru_cache.go create mode 100644 lib/utils/lru-cache/lru_cache_test.go diff --git a/lib/utils/lru-cache/lru_cache.go b/lib/utils/lru-cache/lru_cache.go new file mode 100644 index 0000000000..2737015f69 --- /dev/null +++ b/lib/utils/lru-cache/lru_cache.go @@ -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 +} diff --git a/lib/utils/lru-cache/lru_cache_test.go b/lib/utils/lru-cache/lru_cache_test.go new file mode 100644 index 0000000000..0a378a6820 --- /dev/null +++ b/lib/utils/lru-cache/lru_cache_test.go @@ -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) + }) +}