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/go.mod b/lru/go.mod new file mode 100644 index 0000000..1c7dc42 --- /dev/null +++ b/lru/go.mod @@ -0,0 +1,11 @@ +module lru + +go 1.21.6 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/lru/go.sum b/lru/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/lru/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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() +}