Skip to content

Commit

Permalink
First version of LRU cache client
Browse files Browse the repository at this point in the history
  • Loading branch information
thedae committed Jan 21, 2025
1 parent 4c3cc2f commit 041eefe
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 1 deletion.
16 changes: 16 additions & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version = 1

[[analyzers]]
name = "go"
enabled = true

[analyzers.meta]
import_root = "github.com/krakendio/lru"

[[transformers]]
name = "gofmt"
enabled = true

[[transformers]]
name = "gofumpt"
enabled = true
22 changes: 22 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Go

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.22

- name: Test
run: go test -v ./...
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# lru
lru
=========

A simple LRU cache implementation that also evicts elements when an specific size threshold is met

### Usage

```
go get github.com/krakendio/lru
```

```go
maxSize := (1024 * 1024 * 100) // 100 MB
maxItems := 100000

cache, err := lru.NewLruCache(maxSize, maxItems)

cache.Set("key1", "value")

value, exists := cache.Get("key1")

cache.Delete("key1")
```
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/krakendio/lru

go 1.22
99 changes: 99 additions & 0 deletions lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package lru

import (
"container/list"
"errors"
"sync"
)

type LruCache struct {
maxItems uint64
maxSize uint64
current uint64

mu sync.RWMutex

evictList *list.List
items map[string]*list.Element
}

type entry struct {
key string
value []byte
}

var (
ErrInvalidMaxSize = errors.New("invalid max size, must be greater than 0")
ErrInvalidMaxItems = errors.New("invalid max items, must be greater than 0")
)

func NewLruCache(maxBytes, maxItems uint64) (*LruCache, error) {
if maxBytes <= 0 {
return nil, ErrInvalidMaxSize
}
if maxItems <= 0 {
return nil, ErrInvalidMaxItems
}

return &LruCache{
maxItems: maxItems,
maxSize: maxBytes,
items: make(map[string]*list.Element),
evictList: list.New(),
}, nil
}

func (c *LruCache) Get(key string) (res []byte, status bool) {
c.mu.Lock()

var ent *list.Element
if ent, status = c.items[key]; status {
c.evictList.MoveToFront(ent)
res = ent.Value.(*entry).value
}

c.mu.Unlock()
return
}

func (c *LruCache) Set(key string, resp []byte) {
if len(resp) >= int(c.maxSize) {
return
}

c.mu.Lock()

// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
ent.Value.(*entry).value = resp
c.mu.Unlock()
return
}

// Add new item
c.items[key] = c.evictList.PushFront(&entry{key, resp})

c.current += uint64(len(resp))
// Verify size not exceeded and evict if necessary
for c.current > c.maxSize || uint64(len(c.items)) > c.maxItems {
if e := c.evictList.Back(); e != nil {
delete(c.items, e.Value.(*entry).key)
c.current -= uint64(len(e.Value.(*entry).value))
c.evictList.Remove(e)
}
}
c.mu.Unlock()
}

func (c *LruCache) Delete(key string) {
c.mu.Lock()

if ent, ok := c.items[key]; ok {
delete(c.items, ent.Value.(*entry).key)
c.current -= uint64(len(ent.Value.(*entry).value))
c.evictList.Remove(ent)
}

c.mu.Unlock()
}
123 changes: 123 additions & 0 deletions lru_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package lru

import (
"strconv"
"testing"
)

func TestNewLruCache(t *testing.T) {
_, err := NewLruCache(0, 0)
if err != ErrInvalidMaxSize {
t.Errorf("expecting ErrInvalidMaxSize, got %v", err)
}

_, err = NewLruCache(1, 0)
if err != ErrInvalidMaxItems {
t.Errorf("expecting ErrInvalidMaxItems, got %v", err)
}
}

func TestDelete(t *testing.T) {
c, _ := NewLruCache(10000, 3)
c.Set("a", []byte("A"))
c.Set("b", []byte("B"))
c.Set("c", []byte("C"))

c.Delete("a")
if _, ok := c.Get("a"); ok {
t.Errorf("expecting key a to be deleted")
}
}

func TestGet(t *testing.T) {
c, _ := NewLruCache(10000, 3)

for i := 1; i <= 3; i++ {
v := strconv.Itoa(i)
c.Set(v, []byte(v))
}

for i := 1; i <= 3; i++ {
v := strconv.Itoa(i)
if _, ok := c.Get(v); !ok {
t.Errorf("expecting key %s to exist", v)
}
}

if _, ok := c.Get("4"); ok {
t.Errorf("expecting key 4 to not exist")
}
}

func TestSet_invalidSize(t *testing.T) {
c, _ := NewLruCache(10, 10)
c.Set("too big", []byte("too big to fit the cache size"))

if _, ok := c.Get("too big"); ok {
t.Errorf("expecting key a to not exist")
}
}

func TestSet_evictDueToItems(t *testing.T) {
c, _ := NewLruCache(10000, 10)

for i := 1; i <= 10; i++ {
v := strconv.Itoa(i)
c.Set(v, []byte(v))
}

c.Set("11", []byte("11"))

if _, ok := c.Get("1"); ok {
t.Errorf("expecting key 1 to be evicted")
}

c.Get("2")

c.Set("12", []byte("12"))
if _, ok := c.Get("2"); !ok {
t.Errorf("expecting key 2 to exist")
}
}

func TestSet_evictDueToSize(t *testing.T) {
c, _ := NewLruCache(10, 100)

for i := 1; i <= 10; i++ {
v := strconv.Itoa(i)
c.Set(v, []byte("v"))
}

c.Set("11", []byte("v"))

if _, ok := c.Get("1"); ok {
t.Errorf("expecting key 1 to be evicted")
}

c.Set("12", []byte("vvv"))
for i := 2; i <= 4; i++ {
v := strconv.Itoa(i)
if _, ok := c.Get(v); ok {
t.Errorf("expecting key %s to be evicted", v)
}
}
}

func TestSet_moveToFront(t *testing.T) {
c, _ := NewLruCache(10000, 10)

for i := 1; i <= 10; i++ {
v := strconv.Itoa(i)
c.Set(v, []byte(v))
}

c.Set("1", []byte("1"))
c.Set("11", []byte("11"))

if _, ok := c.Get("1"); !ok {
t.Errorf("expecting key 1 to exist")
}
if _, ok := c.Get("2"); ok {
t.Errorf("expecting key 2 to be evicted")
}
}

0 comments on commit 041eefe

Please sign in to comment.