Skip to content

Commit

Permalink
Implement immediate mutex ringlist for LFU and LRU
Browse files Browse the repository at this point in the history
  • Loading branch information
mgnsk committed Apr 9, 2024
1 parent 740d9ab commit 4ee712d
Show file tree
Hide file tree
Showing 10 changed files with 598 additions and 483 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
go-version: 1.21

- name: Run test
run: go test -race
run: go test -race ./...

lint:
runs-on: ubuntu-latest
Expand Down
66 changes: 66 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package evcache_test

import (
"errors"
"sync/atomic"
"testing"

"github.com/mgnsk/evcache/v3"
)

func BenchmarkFetchAndEvictParallel(b *testing.B) {
b.StopTimer()

c := evcache.New[uint64, int](0)
index := uint64(0)
errFetch := errors.New("error fetching")

b.ReportAllocs()
b.StartTimer()

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if idx := atomic.AddUint64(&index, 1); idx%2 == 0 {
_, _ = c.Fetch(0, 0, func() (int, error) {
if idx%4 == 0 {
return 0, errFetch
}
return 0, nil
})
} else {
c.Evict(0)
}
}
})
}

func BenchmarkFetchExists(b *testing.B) {
b.StopTimer()

c := evcache.New[uint64, int](0)
c.LoadOrStore(0, 0, 0)

b.ReportAllocs()
b.StartTimer()

for i := 0; i < b.N; i++ {
_, _ = c.Fetch(0, 0, func() (int, error) {
panic("unexpected fetch callback")
})
}
}

func BenchmarkFetchNotExists(b *testing.B) {
b.StopTimer()

c := evcache.New[int, int](0)

b.ReportAllocs()
b.StartTimer()

for i := 0; i < b.N; i++ {
_, _ = c.Fetch(i, 0, func() (int, error) {
return 0, nil
})
}
}
30 changes: 11 additions & 19 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/mgnsk/evcache/v3/internal/backend"
"github.com/mgnsk/ringlist"
)

// Cache is an in-memory TTL cache with optional capacity.
Expand Down Expand Up @@ -62,7 +63,7 @@ func (c *Cache[K, V]) Exists(key K) bool {
// Get returns the value stored in the cache for key.
func (c *Cache[K, V]) Get(key K) (value V, exists bool) {
if elem, ok := c.backend.Load(key); ok {
return elem.Value, true
return elem.Value.Value, true
}

var zero V
Expand All @@ -74,8 +75,8 @@ func (c *Cache[K, V]) Get(key K) (value V, exists bool) {
//
// Range is allowed to modify the cache.
func (c *Cache[K, V]) Range(f func(key K, value V) bool) {
c.backend.Range(func(key K, elem *backend.Element[V]) bool {
return f(key, elem.Value)
c.backend.Range(func(key K, elem *ringlist.Element[backend.Record[V]]) bool {
return f(key, elem.Value.Value)
})
}

Expand All @@ -86,8 +87,8 @@ func (c *Cache[K, V]) Len() int {

// Evict a key and return its value.
func (c *Cache[K, V]) Evict(key K) (value V, ok bool) {
if elem, ok := c.backend.Evict(key); ok {
return elem.Value, true
if value, ok := c.backend.Evict(key); ok {
return value, true
}

var zero V
Expand Down Expand Up @@ -130,31 +131,22 @@ func (c *Cache[K, V]) Fetch(key K, ttl time.Duration, f func() (V, error)) (valu
func (c *Cache[K, V]) TryFetch(key K, f func() (V, time.Duration, error)) (value V, err error) {
newElem := c.backend.Reserve()

loadOrStore:
if elem, initialized, loaded := c.backend.LoadOrStore(key, newElem); loaded {
if initialized {
c.backend.Release(newElem)
return elem.Value, nil
}

elem.Wait()

goto loadOrStore
if elem, loaded := c.backend.LoadOrStore(key, newElem); loaded {
c.backend.Release(newElem)
return elem.Value.Value, nil
}

defer func() {
if r := recover(); r != nil {
c.backend.Delete(key)
c.backend.Discard(newElem)
c.backend.Discard(key, newElem)

panic(r)
}
}()

value, ttl, err := f()
if err != nil {
c.backend.Delete(key)
c.backend.Discard(newElem)
c.backend.Discard(key, newElem)

var zero V
return zero, err
Expand Down
49 changes: 0 additions & 49 deletions cache_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package evcache_test

import (
"errors"
"fmt"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -229,50 +227,3 @@ func TestExpireEdgeCase(t *testing.T) {
return c.Len() == 0
})
}

func BenchmarkFetchAndEvictParallel(b *testing.B) {
c := evcache.New[uint64, int](0)
index := uint64(0)
errFetch := errors.New("error fetching")
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if idx := atomic.AddUint64(&index, 1); idx%2 == 0 {
_, _ = c.Fetch(0, 0, func() (int, error) {
if idx%4 == 0 {
return 0, errFetch
}
return 0, nil
})
} else {
c.Evict(0)
}
}
})
}

func BenchmarkFetchExists(b *testing.B) {
c := evcache.New[uint64, int](0)
c.LoadOrStore(0, 0, 0)

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Fetch(0, 0, func() (int, error) {
panic("unexpected fetch callback")
})
}
}

func BenchmarkFetchNotExists(b *testing.B) {
c := evcache.New[int, int](0)

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = c.Fetch(i, 0, func() (int, error) {
return 0, nil
})
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/mgnsk/evcache/v3

go 1.21

require github.com/mgnsk/ringlist v0.0.0-20231025190742-1f621b925911
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/mgnsk/ringlist v0.0.0-20231025190742-1f621b925911 h1:/RUn/noOFPYcnnlsXmdmHSvKBp5gwQ5OnlNX9bRgeZE=
github.com/mgnsk/ringlist v0.0.0-20231025190742-1f621b925911/go.mod h1:fyW9CbxFtYtWazjV96eJt4r+B2ABDRu3/rXQOxxTjLE=
Loading

0 comments on commit 4ee712d

Please sign in to comment.