From 222358b146029565fd4e48446ccc4584644eb286 Mon Sep 17 00:00:00 2001 From: Houdini Date: Fri, 2 Aug 2024 18:40:19 +0100 Subject: [PATCH] feature: add lru implementation add tests to run the lru implementation concurrently to ensure the appropriate locking mechanism are used --- .github/workflows/main.yaml | 4 +- README.md | 4 +- tests/lru_test.go | 48 +++++++++++++++++ zwis/lru.go | 101 ++++++++++++++++++++++++++++++++++++ zwis/memory.go | 8 +-- zwis/{cache.go => zwis.go} | 0 6 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 tests/lru_test.go rename zwis/{cache.go => zwis.go} (100%) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7582646..f39afce 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -17,9 +17,9 @@ jobs: uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v3 with: - go-version: 1.20 + go-version: 1.21 - name: Install dependencies run: go mod tidy diff --git a/README.md b/README.md index 49834c1..2dc73f7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# zwis +# Zwis An in-memory cache system in Go that supports expirable cache entries and various cache eviction policies including Least Frequently Used (LFU), Least Recently Used (LRU), and Adaptive Replacement Cache (ARC). + +[![zwis library workflow](https://github.com/NonsoAmadi10/zwis/actions/workflows/main.yaml/badge.svg)](https://github.com/NonsoAmadi10/zwis/actions/workflows/main.yaml) diff --git a/tests/lru_test.go b/tests/lru_test.go new file mode 100644 index 0000000..5809670 --- /dev/null +++ b/tests/lru_test.go @@ -0,0 +1,48 @@ +package zwis_test + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/NonsoAmadi10/zwis/zwis" +) + +func TestLRUCacheConcurrency(t *testing.T) { + cache := zwis.NewLRUCache(100) + ctx := context.Background() + + var wg sync.WaitGroup + wg.Add(2) + + // Concurrent writes + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + cache.Set(ctx, fmt.Sprintf("key%d", i), i, 0) + } + }() + + // Concurrent reads + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + cache.Get(ctx, fmt.Sprintf("key%d", i)) + } + }() + + // Wait for both goroutines to finish + wg.Wait() + + // Additional verification + for i := 900; i < 1000; i++ { + key := fmt.Sprintf("key%d", i) + value, ok := cache.Get(ctx, key) + if !ok { + t.Errorf("Expected key %s to be in cache, but it wasn't", key) + } else if value != i { + t.Errorf("Expected value for key %s to be %d, but got %v", key, i, value) + } + } +} diff --git a/zwis/lru.go b/zwis/lru.go index 35d465d..150a0b0 100644 --- a/zwis/lru.go +++ b/zwis/lru.go @@ -1 +1,102 @@ package zwis + +import ( + "container/list" + "context" + "sync" + "time" +) + +type LRUCache struct { + capacity int + cache map[interface{}]*list.Element + list *list.List + mutex sync.RWMutex +} + +type entry struct { + key interface{} + value interface{} + expiration time.Time +} + +func NewLRUCache(capacity int) *LRUCache { + return &LRUCache{ + capacity: capacity, + cache: make(map[interface{}]*list.Element), + list: list.New(), + } +} + +func (lru *LRUCache) Get(ctx context.Context, key interface{}) (interface{}, bool) { + lru.mutex.RLock() + elem, ok := lru.cache[key] + lru.mutex.RUnlock() + + if !ok { + return nil, false + } + + lru.mutex.Lock() + defer lru.mutex.Unlock() + + entry := elem.Value.(*entry) + if !entry.expiration.IsZero() && entry.expiration.Before(time.Now()) { + lru.removeElement(elem) + return nil, false + } + + lru.list.MoveToFront(elem) + return entry.value, true +} + +func (lru *LRUCache) Set(ctx context.Context, key, value interface{}, ttl time.Duration) { + lru.mutex.Lock() + defer lru.mutex.Unlock() + + var expiration time.Time + if ttl > 0 { + expiration = time.Now().Add(ttl) + } + + if elem, ok := lru.cache[key]; ok { + lru.list.MoveToFront(elem) + elem.Value.(*entry).value = value + elem.Value.(*entry).expiration = expiration + } else { + if lru.list.Len() >= lru.capacity { + lru.removeOldest() + } + elem := lru.list.PushFront(&entry{key, value, expiration}) + lru.cache[key] = elem + } +} + +func (lru *LRUCache) Delete(ctx context.Context, key interface{}) { + lru.mutex.Lock() + defer lru.mutex.Unlock() + + if elem, ok := lru.cache[key]; ok { + lru.removeElement(elem) + } +} + +func (lru *LRUCache) Clear(ctx context.Context) { + lru.mutex.Lock() + defer lru.mutex.Unlock() + + lru.list.Init() + lru.cache = make(map[interface{}]*list.Element) +} + +func (lru *LRUCache) removeOldest() { + oldest := lru.list.Back() + if oldest != nil { + lru.removeElement(oldest) + } +} + +func (lru *LRUCache) removeElement(elem *list.Element) { + lru.list.Remove(elem) + delete(lru.cache, elem.Value.(*entry).key) +} diff --git a/zwis/memory.go b/zwis/memory.go index 84784c9..dfd3324 100644 --- a/zwis/memory.go +++ b/zwis/memory.go @@ -8,7 +8,7 @@ import ( type item struct { value interface{} - expiration int64 + expiration time.Time } type MemoryCache struct { @@ -31,7 +31,7 @@ func (c *MemoryCache) Get(ctx context.Context, key string) (interface{}, bool) { return nil, false } - if item.expiration > 0 && item.expiration < time.Now().UnixNano() { + if !item.expiration.IsZero() && item.expiration.Before(time.Now()) { return nil, false } @@ -42,9 +42,9 @@ func (c *MemoryCache) Set(ctx context.Context, key string, value interface{}, tt c.mu.Lock() defer c.mu.Unlock() - var expiration int64 + var expiration time.Time if ttl > 0 { - expiration = time.Now().Add(ttl).UnixNano() + expiration = time.Now().Add(ttl) } c.items[key] = item{ diff --git a/zwis/cache.go b/zwis/zwis.go similarity index 100% rename from zwis/cache.go rename to zwis/zwis.go