feat: implement lfu based cache strategy
This commit is contained in:
41
pkg/storage/driver/cache/lfu/testsuite/main.go
vendored
Normal file
41
pkg/storage/driver/cache/lfu/testsuite/main.go
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type StoreFactory func(testName string) lfu.Store[string, string]
|
||||
type testCase func(t *testing.T, store lfu.Store[string, string]) error
|
||||
|
||||
var testCases = []testCase{
|
||||
testSetGetDelete,
|
||||
testEviction,
|
||||
testConcurrent,
|
||||
testMultipleSet,
|
||||
testTTL,
|
||||
}
|
||||
|
||||
func TestCacheWithStore(t *testing.T, factory StoreFactory) {
|
||||
for _, tc := range testCases {
|
||||
funcName := runtime.FuncForPC(reflect.ValueOf(tc).Pointer()).Name()
|
||||
funcNameParts := strings.Split(funcName, "/")
|
||||
testName := funcNameParts[len(funcNameParts)-1]
|
||||
func(tc testCase) {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store := factory(testName)
|
||||
|
||||
if err := tc(t, store); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
67
pkg/storage/driver/cache/lfu/testsuite/test_concurrent.go
vendored
Normal file
67
pkg/storage/driver/cache/lfu/testsuite/test_concurrent.go
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func testConcurrent(t *testing.T, store lfu.Store[string, string]) error {
|
||||
const value = "foobar"
|
||||
totalKeys := 25
|
||||
totalSize := len(value) * totalKeys
|
||||
capacity := totalSize / 2
|
||||
|
||||
cache := lfu.NewCache[string, string](store,
|
||||
lfu.WithCapacity[string, string](capacity),
|
||||
lfu.WithLog[string, string](t.Logf),
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(totalKeys)
|
||||
|
||||
loops := totalKeys * 10
|
||||
|
||||
for i := 0; i < totalKeys; i++ {
|
||||
key := fmt.Sprintf("key%d", i)
|
||||
func(key string) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < loops; i++ {
|
||||
if err := cache.Set(key, value); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(key)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("cache before final evict [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
|
||||
|
||||
if err := cache.Evict(); err != nil {
|
||||
t.Errorf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
t.Logf("cache after final evict [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
|
||||
|
||||
expectedLength := capacity / len(value)
|
||||
if e, g := expectedLength, cache.Len(); e < g {
|
||||
t.Errorf("cache.Len(): expected <= %d, got %d", e, g)
|
||||
}
|
||||
|
||||
if cache.Size() > capacity {
|
||||
t.Errorf("cache.Size(): expected <= %d, got %d", capacity, cache.Size())
|
||||
}
|
||||
|
||||
if e, g := expectedLength*len(value), cache.Size(); e < g {
|
||||
t.Errorf("cache.Size(): expected <= %d, got %d", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
70
pkg/storage/driver/cache/lfu/testsuite/test_eviction.go
vendored
Normal file
70
pkg/storage/driver/cache/lfu/testsuite/test_eviction.go
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func testEviction(t *testing.T, store lfu.Store[string, string]) error {
|
||||
cache := lfu.NewCache[string, string](store,
|
||||
lfu.WithCapacity[string, string](10),
|
||||
lfu.WithLog[string, string](t.Logf),
|
||||
)
|
||||
|
||||
if err := cache.Set("key1", "key1"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := cache.Set("key2", "key2"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Increment frequency of key2
|
||||
if _, err := cache.Get("key2"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := 8, cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if err := cache.Set("key3", "key3"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
t.Logf("cache [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
|
||||
|
||||
_, err := cache.Get("key1")
|
||||
if err == nil {
|
||||
t.Errorf("expected 'key1' to be evicted")
|
||||
}
|
||||
|
||||
if !errors.Is(err, lfu.ErrNotFound) {
|
||||
t.Errorf("expected err to be 'ErrNotFound'")
|
||||
}
|
||||
|
||||
value, err := cache.Get("key2")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := "key2", value; e < g {
|
||||
t.Errorf("cache.Get(\"key2\"): expected %v, got %v", e, g)
|
||||
}
|
||||
|
||||
if e, g := cache.Capacity(), cache.Size(); e < g {
|
||||
t.Errorf("cache.Size(): expected <= %d, got %d", e, g)
|
||||
}
|
||||
|
||||
if e, g := 2, cache.Len(); e != g {
|
||||
t.Errorf("cache.Len(): expected %d, got %d", e, g)
|
||||
}
|
||||
|
||||
if cache.Size() < 0 {
|
||||
t.Errorf("cache.Size(): expected value >= 0, got %d", cache.Size())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
80
pkg/storage/driver/cache/lfu/testsuite/test_multiple_set.go
vendored
Normal file
80
pkg/storage/driver/cache/lfu/testsuite/test_multiple_set.go
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func testMultipleSet(t *testing.T, store lfu.Store[string, string]) error {
|
||||
const (
|
||||
key = "mykey"
|
||||
firstValue = "foo"
|
||||
secondValue = "bar"
|
||||
thirdValue = "foobar"
|
||||
)
|
||||
|
||||
cache := lfu.NewCache[string, string](store)
|
||||
|
||||
if e, g := 0, cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if err := cache.Set(key, firstValue); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := len(firstValue), cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
retrieved, err := cache.Get(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := firstValue, retrieved; e != g {
|
||||
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if err := cache.Set(key, secondValue); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := len(secondValue), cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
retrieved, err = cache.Get(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := secondValue, retrieved; e != g {
|
||||
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if err := cache.Set(key, thirdValue); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := len(thirdValue), cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
retrieved, err = cache.Get(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := thirdValue, retrieved; e != g {
|
||||
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := len(thirdValue), cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
66
pkg/storage/driver/cache/lfu/testsuite/test_set_get_delete.go
vendored
Normal file
66
pkg/storage/driver/cache/lfu/testsuite/test_set_get_delete.go
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func testSetGetDelete(t *testing.T, store lfu.Store[string, string]) error {
|
||||
const (
|
||||
key = "mykey"
|
||||
value = "foobar"
|
||||
)
|
||||
|
||||
cache := lfu.NewCache[string, string](store, lfu.WithCapacity[string, string](10))
|
||||
|
||||
if e, g := 0, cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if err := cache.Set(key, value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := len(value), cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := 1, cache.Len(); e != g {
|
||||
t.Errorf("cache.Len(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
retrieved, err := cache.Get(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := value, retrieved; e != g {
|
||||
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if err := cache.Delete(key); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := cache.Get(key); err == nil || !errors.Is(err, lfu.ErrNotFound) {
|
||||
t.Errorf("cache.Get(key): err should be lfu.ErrNotFound, got '%v'", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if e, g := value, retrieved; e != g {
|
||||
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := 0, cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := 0, cache.Len(); e != g {
|
||||
t.Errorf("cache.Len(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
t.Logf("cache [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
|
||||
|
||||
return nil
|
||||
}
|
54
pkg/storage/driver/cache/lfu/testsuite/test_ttl.go
vendored
Normal file
54
pkg/storage/driver/cache/lfu/testsuite/test_ttl.go
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func testTTL(t *testing.T, store lfu.Store[string, string]) error {
|
||||
const (
|
||||
key = "mykey"
|
||||
value = "foobar"
|
||||
)
|
||||
|
||||
ttl := time.Second
|
||||
|
||||
cache := lfu.NewCache[string, string](store,
|
||||
lfu.WithTTL[string, string](ttl),
|
||||
lfu.WithCapacity[string, string](10),
|
||||
)
|
||||
|
||||
if err := cache.Set(key, value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
retrieved, err := cache.Get(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := value, retrieved; e != g {
|
||||
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
|
||||
if _, err := cache.Get(key); !errors.Is(err, lfu.ErrNotFound) {
|
||||
t.Errorf("cache.Get(key): expected err == lfu.ErrNotFound, got '%v'", err)
|
||||
}
|
||||
|
||||
t.Logf("cache [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
|
||||
|
||||
if e, g := 0, cache.Size(); e != g {
|
||||
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
if e, g := 0, cache.Len(); e != g {
|
||||
t.Errorf("cache.Len(): expected '%v', got '%v'", e, g)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user