Skip to content

Commit

Permalink
implemented lru and lfu cache
Browse files Browse the repository at this point in the history
  • Loading branch information
alserov committed May 19, 2024
0 parents commit 702730f
Show file tree
Hide file tree
Showing 13 changed files with 1,063 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/on-push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
on:
push:
branches:
- main
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
env:
env: CI
steps:
- name: setup
uses: actions/setup-go@v2
with:
go-version: 1.22.0

- name: checkout
uses: actions/checkout@v3

- name: testing
run: go test -v ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/alserov/goche

go 1.22

require github.com/stretchr/testify v1.9.0

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 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
35 changes: 35 additions & 0 deletions instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package goche

import (
"context"
)

type Cache interface {
Get(ctx context.Context, key string) (any, bool)
Set(ctx context.Context, key string, val any)

Close()
}

// New - cache constructor, requires cache type, additionally modifiers
func New(t Type, mods ...ModifierFunc) Cache {
switch t {
case LFU:
return NewLFU(mods...)
case LRU:
return NewLRU(mods...)
default:
panic("invalid constructor parameter")
}
}

type Type int

const (
LRU Type = iota
LFU

DefaultLimit = 1000
)

type ModifierFunc func(cache any)
269 changes: 269 additions & 0 deletions lfu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
package goche

import (
"context"
"sync"
"time"
)

func NewLFU(mods ...ModifierFunc) Cache {
ctx, cancel := context.WithCancel(context.Background())

c := lfu{
limit: DefaultLimit,
vals: make(map[string]any),
pushCh: make(chan *lfuNode, DefaultLimit),
popCh: make(chan struct{}, 10),
updatePosCh: make(chan string, 10),
stopCtx: ctx,
stopCtxCancel: cancel,
}

for _, mod := range mods {
mod(&c)
}

go c.pop()
go c.push()
go c.updatePos()
go c.clearAfterPeriod()

return &c
}

type lfuNode struct {
val any
key string

new bool

freq uint64

lastUsedAt time.Time

prev *lfuNode
next *lfuNode
}

type lfu struct {
head *lfuNode
tail *lfuNode

mu sync.RWMutex

vals map[string]any

stopCtx context.Context
stopCtxCancel context.CancelFunc

popCh chan struct{}
pushCh chan *lfuNode
updatePosCh chan string

len uint64
limit uint64

clearPeriod *time.Duration
}

func (c *lfu) Close() {
c.stopCtxCancel()
}

func (c *lfu) Get(ctx context.Context, key string) (any, bool) {
c.mu.RLock()
val, ok := c.vals[key]
c.mu.RUnlock()

if ok {
select {
case c.updatePosCh <- key:
case <-ctx.Done():
close(c.updatePosCh)
}
}

return val, ok
}

func (c *lfu) Set(ctx context.Context, key string, val any) {
c.mu.Lock()
_, ok := c.vals[key]
if ok {
c.mu.Unlock()
return
}
c.vals[key] = val
c.mu.Unlock()

select {
case c.pushCh <- newLFUNode(key, val):
case <-c.stopCtx.Done():
close(c.pushCh)
}
}

func (c *lfu) updatePos() {
upsert := func(key string) {
c.mu.Lock()
defer c.mu.Unlock()

var n *lfuNode
for n = c.head; n != nil && n.key != key; n = n.next {
}

if n != nil {
n.lastUsedAt = time.Now()
n.freq++

currentNode := n.prev
for currentNode != nil && currentNode.freq <= n.freq {
currentNode = currentNode.prev
}

if currentNode == nil {
for n.prev != nil && n.prev.freq <= n.freq {
c.swap(n.prev, n)
}
} else {
if currentNode.next == n {
c.swap(currentNode, n)
} else {
if currentNode.next != nil {
currentNode.next.prev = n
}
n.next = currentNode.next
n.prev = currentNode
currentNode.next = n
}
}
}
}

for key := range c.updatePosCh {
upsert(key)
}
}

func (c *lfu) push() {
defer func() {
close(c.popCh)
}()

insert := func(n *lfuNode) {
c.mu.Lock()
defer c.mu.Unlock()

if c.head == nil {
c.head = n
c.tail = n
} else {
if c.len >= c.limit {
c.popCh <- struct{}{}
}

currentNode := c.tail
for currentNode != nil && currentNode.freq <= n.freq {
currentNode = currentNode.prev
}

if currentNode == nil {
c.head.prev = n
n.next = c.head
c.head = n
} else {
if currentNode.next != nil {
currentNode.next.prev = n
}
n.next = currentNode.next
n.prev = currentNode
currentNode.next = n
}
}

c.len++
}

for node := range c.pushCh {
insert(node)
}
}

func (c *lfu) pop() {
extrude := func() {
c.mu.Lock()
defer c.mu.Unlock()

delete(c.vals, c.tail.key)

if c.tail != nil {
c.tail = c.tail.prev
if c.tail != nil {
c.tail.next = nil
}
}
}

for range c.popCh {
extrude()
}
}

func (c *lfu) swap(prev, curr *lfuNode) bool {
if prev == nil || curr == nil {
return false
}

prev.next = curr.next
if curr.next != nil {
curr.next.prev = prev
} else {
c.tail = prev
}

curr.prev = prev.prev
if prev.prev != nil {
prev.prev.next = curr
} else {
c.head = curr
}

curr.next = prev
prev.prev = curr

return true
}

func (c *lfu) clearAfterPeriod() {
if c.clearPeriod != nil {
for range time.Tick(*c.clearPeriod) {
c.mu.Lock()
if c.head != nil {
curr := c.head
for curr != nil {
if time.Now().UnixMilli()-curr.lastUsedAt.UnixMilli() > c.clearPeriod.Milliseconds() {
delete(c.vals, curr.key)

if curr.prev == nil {
c.head = c.head.next
} else {
curr.prev.next = curr.next
}

if curr.next == nil {
c.tail = c.tail.prev
} else {
curr.next.prev = curr.prev
}
}
curr = curr.next
}
}
c.mu.Unlock()
}
}
}

func newLFUNode(key string, val any) *lfuNode {
return &lfuNode{val: val, key: key, lastUsedAt: time.Now()}
}
22 changes: 22 additions & 0 deletions lfu_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package goche

import (
"context"
"testing"
)

func BenchmarkLFUSet(b *testing.B) {
c := New(LFU)

for i := 0; i < b.N; i++ {
c.Set(context.Background(), "key", "value")
}
}

func BenchmarkLFUGet(b *testing.B) {
c := New(LFU)

for i := 0; i < b.N; i++ {
c.Get(context.Background(), "key")
}
}
Loading

0 comments on commit 702730f

Please sign in to comment.