From 35c1824ba85ee8988cfa5a978cdd2e4b3fbeca47 Mon Sep 17 00:00:00 2001 From: Konstantin Grachev Date: Sat, 27 Jan 2024 17:50:56 +0300 Subject: [PATCH] lru: init --- lru/Makefile | 48 ++++++++++++++++++++++ lru/lru.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ lru/lru_test.go | 78 +++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 lru/Makefile create mode 100644 lru/lru.go create mode 100644 lru/lru_test.go diff --git a/lru/Makefile b/lru/Makefile new file mode 100644 index 0000000..4543fa9 --- /dev/null +++ b/lru/Makefile @@ -0,0 +1,48 @@ +.DEFAULT_GOAL := help + +# ==================================================================================== # +# HELPERS +# ==================================================================================== # + +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + + +# ==================================================================================== # +# QUALITY CONTROL +# ==================================================================================== # + +## tidy: format code and tidy modfile +.PHONY: tidy +tidy: + go fmt ./... + go mod tidy -v + +## audit: run quality control checks +.PHONY: audit +audit: + go mod verify + go vet ./... + go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + go test -race -buildvcs -vet=off ./... + go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run -v ./... + + +# ==================================================================================== # +# DEVELOPMENT +# ==================================================================================== # + +## test: run all tests +.PHONY: test +test: + go test -v -race -buildvcs ./... + +## test/cover: run all tests and display coverage +.PHONY: test/cover +test/cover: + go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... + go tool cover -html=/tmp/coverage.out diff --git a/lru/lru.go b/lru/lru.go new file mode 100644 index 0000000..2218a4a --- /dev/null +++ b/lru/lru.go @@ -0,0 +1,106 @@ +package lru + +import ( + "container/list" + "time" +) + +type LRU[T any] struct { + queue *list.List + items map[string]*list.Element + capacity int +} +type item[T any] struct { + key string + value *T + eol time.Time +} + +func New[T any](capacity int) *LRU[T] { + return &LRU[T]{ + queue: list.New(), + items: make(map[string]*list.Element, capacity), + capacity: capacity, + } +} + +func (lru *LRU[T]) Set(key string, value T, ttl time.Duration) (evicted bool) { + eol := time.Now().Add(ttl) + + if element, ok := lru.items[key]; ok { + lru.queue.MoveToFront(element) + item := element.Value.(*item[T]) + item.value = &value + item.eol = eol + + return + } + + if lru.queue.Len() == lru.capacity { + lru.evict() + evicted = true + } + + item := &item[T]{ + key: key, + value: &value, + eol: eol, + } + + element := lru.queue.PushFront(item) + lru.items[item.key] = element + + return +} + +func (lru *LRU[T]) Get(name string) (value T, ok bool) { + element, ok := lru.items[name] + if !ok { + return value, false + } + + item := element.Value.(*item[T]) + if item.expired() { + lru.delete(element) + + return value, false + } + + lru.queue.MoveToFront(element) + + return *item.value, true +} + +func (lru *LRU[T]) evict() { + element := lru.queue.Back() + if element == nil { + return + } + + for { // find first expired + item := element.Value.(*item[T]) + + if item.expired() { + lru.delete(element) + + return + } + + element = element.Prev() + + if element == nil { + break // probably expired not found + } + } + + lru.delete(lru.queue.Back()) // delete oldest if no one expired +} + +func (lru *LRU[T]) delete(element *list.Element) { + lru.queue.Remove(element) + delete(lru.items, element.Value.(*item[T]).key) +} + +func (i *item[T]) expired() bool { + return time.Now().After(i.eol) +} diff --git a/lru/lru_test.go b/lru/lru_test.go new file mode 100644 index 0000000..6403db6 --- /dev/null +++ b/lru/lru_test.go @@ -0,0 +1,78 @@ +package lru + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestTtl(t *testing.T) { + cache := New[int](1) + + cache.Set("first", 1, 500*time.Millisecond) + + value, ok := cache.Get("first") + assert.True(t, ok) + assert.Equal(t, 1, value) + + time.Sleep(600 * time.Millisecond) + + value, ok = cache.Get("first") + assert.False(t, ok) + assert.Equal(t, 0, value) +} + +func TestEvictFirst(t *testing.T) { + cache := New[int](2) + + assert.False(t, cache.Set("first", 1, 500*time.Millisecond)) + assert.False(t, cache.Set("second", 2, 500*time.Millisecond)) + assert.True(t, cache.Set("third", 3, 500*time.Millisecond)) + + _, ok := cache.Get("first") + assert.False(t, ok) +} + +func TestEvictExpired(t *testing.T) { + cache := New[int](2) + + assert.False(t, cache.Set("first", 1, 1000*time.Millisecond)) + assert.False(t, cache.Set("second", 2, 100*time.Millisecond)) + + time.Sleep(200*time.Millisecond) + + assert.True(t, cache.Set("third", 3, 1000*time.Millisecond)) + + var ok bool + _, ok = cache.Get("first") + assert.True(t, ok) + _, ok = cache.Get("third") + assert.True(t, ok) + _, ok = cache.Get("second") + assert.False(t, ok) +} + +func TestSetExisted(t *testing.T) { + cache := New[int](2) + + assert.False(t, cache.Set("first", 1, 200*time.Millisecond)) + assert.False(t, cache.Set("second", 2, 500*time.Millisecond)) + + time.Sleep(100*time.Millisecond) + + assert.False(t, cache.Set("first", 11, 1000*time.Millisecond)) + assert.True(t, cache.Set("third", 3, 1000*time.Millisecond)) + + value, ok := cache.Get("first") // exists with updated ttl + assert.True(t, ok) + assert.Equal(t, 11, value) + _, ok = cache.Get("third") + assert.True(t, ok) + _, ok = cache.Get("second") // evicted + assert.False(t, ok) +} + +func TestDummy(t *testing.T) { // for coverage 100% coverage + cache := New[int](0) + cache.evict() +}