Skip to content

Commit

Permalink
feature: add lru implementation
Browse files Browse the repository at this point in the history
add tests to run the lru implementation concurrently to ensure the appropriate locking mechanism are used
  • Loading branch information
NonsoAmadi10 committed Aug 2, 2024
1 parent 3ebe5d9 commit 222358b
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 7 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)
48 changes: 48 additions & 0 deletions tests/lru_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
101 changes: 101 additions & 0 deletions zwis/lru.go
Original file line number Diff line number Diff line change
@@ -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)
}
8 changes: 4 additions & 4 deletions zwis/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type item struct {
value interface{}
expiration int64
expiration time.Time
}

type MemoryCache struct {
Expand All @@ -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
}

Expand All @@ -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{
Expand Down
File renamed without changes.

0 comments on commit 222358b

Please sign in to comment.