diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..964e71d --- /dev/null +++ b/.deepsource.toml @@ -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 \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..68782d7 --- /dev/null +++ b/.github/workflows/go.yml @@ -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 ./... diff --git a/README.md b/README.md index c2981f9..41b348f 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# lru \ No newline at end of file +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") +``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..599d95a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/krakendio/lru + +go 1.22 diff --git a/lru.go b/lru.go new file mode 100644 index 0000000..5af09de --- /dev/null +++ b/lru.go @@ -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() +} diff --git a/lru_test.go b/lru_test.go new file mode 100644 index 0000000..b65aab8 --- /dev/null +++ b/lru_test.go @@ -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") + } +}