lru #3
48
lru/Makefile
Normal file
48
lru/Makefile
Normal 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
|
11
lru/go.mod
Normal file
11
lru/go.mod
Normal file
@ -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
|
||||||
|
)
|
10
lru/go.sum
Normal file
10
lru/go.sum
Normal file
@ -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=
|
106
lru/lru.go
Normal file
106
lru/lru.go
Normal 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
78
lru/lru_test.go
Normal 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()
|
||||||
|
}
|
Reference in New Issue
Block a user