lru: init

This commit is contained in:
2024-01-27 17:50:56 +03:00
parent 29fea2773f
commit 35c1824ba8
3 changed files with 232 additions and 0 deletions

48
lru/Makefile Normal file
View File

@ -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

106
lru/lru.go Normal file
View File

@ -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)
}

78
lru/lru_test.go Normal file
View File

@ -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()
}