wip
This commit is contained in:
249
kvmemcache/cache_test.go
Normal file
249
kvmemcache/cache_test.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package kvmemcache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type State[K comparable] struct {
|
||||
Keys []K
|
||||
Stats Stats
|
||||
}
|
||||
|
||||
func (c *Cache[K,V]) assert(state State[K]) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if len(c.cache) != len(state.Keys) {
|
||||
return fmt.Errorf(
|
||||
"Expected %d keys but found %d.",
|
||||
len(state.Keys),
|
||||
len(c.cache))
|
||||
}
|
||||
|
||||
for _, k := range state.Keys {
|
||||
if _, ok := c.cache[k]; !ok {
|
||||
return fmt.Errorf(
|
||||
"Expected key %v not found.",
|
||||
k)
|
||||
}
|
||||
}
|
||||
|
||||
if c.stats.Hits != state.Stats.Hits {
|
||||
return fmt.Errorf(
|
||||
"Expected %d hits, but found %d.",
|
||||
state.Stats.Hits,
|
||||
c.stats.Hits)
|
||||
}
|
||||
|
||||
if c.stats.Misses != state.Stats.Misses {
|
||||
return fmt.Errorf(
|
||||
"Expected %d misses, but found %d.",
|
||||
state.Stats.Misses,
|
||||
c.stats.Misses)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrTest = errors.New("Hello")
|
||||
|
||||
func TestCache_basic(t *testing.T) {
|
||||
c := New(Config[string, string]{
|
||||
MaxSize: 4,
|
||||
TTL: 50 * time.Millisecond,
|
||||
Src: func(key string) (string, error) {
|
||||
if key == "err" {
|
||||
return "", ErrTest
|
||||
}
|
||||
return key, nil
|
||||
},
|
||||
})
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
sleep time.Duration
|
||||
key string
|
||||
evict bool
|
||||
state State[string]
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "get a",
|
||||
key: "a",
|
||||
state: State[string]{
|
||||
Keys: []string{"a"},
|
||||
Stats: Stats{Hits: 0, Misses: 1},
|
||||
},
|
||||
}, {
|
||||
name: "get a again",
|
||||
key: "a",
|
||||
state: State[string]{
|
||||
Keys: []string{"a"},
|
||||
Stats: Stats{Hits: 1, Misses: 1},
|
||||
},
|
||||
}, {
|
||||
name: "sleep, then get a again",
|
||||
sleep: 55 * time.Millisecond,
|
||||
key: "a",
|
||||
state: State[string]{
|
||||
Keys: []string{"a"},
|
||||
Stats: Stats{Hits: 1, Misses: 2},
|
||||
},
|
||||
}, {
|
||||
name: "get b",
|
||||
key: "b",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "b"},
|
||||
Stats: Stats{Hits: 1, Misses: 3},
|
||||
},
|
||||
}, {
|
||||
name: "get c",
|
||||
key: "c",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "b", "c"},
|
||||
Stats: Stats{Hits: 1, Misses: 4},
|
||||
},
|
||||
}, {
|
||||
name: "get d",
|
||||
key: "d",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "b", "c", "d"},
|
||||
Stats: Stats{Hits: 1, Misses: 5},
|
||||
},
|
||||
}, {
|
||||
name: "get e",
|
||||
key: "e",
|
||||
state: State[string]{
|
||||
Keys: []string{"b", "c", "d", "e"},
|
||||
Stats: Stats{Hits: 1, Misses: 6},
|
||||
},
|
||||
}, {
|
||||
name: "get c again",
|
||||
key: "c",
|
||||
state: State[string]{
|
||||
Keys: []string{"b", "c", "d", "e"},
|
||||
Stats: Stats{Hits: 2, Misses: 6},
|
||||
},
|
||||
}, {
|
||||
name: "get err",
|
||||
key: "err",
|
||||
state: State[string]{
|
||||
Keys: []string{"c", "d", "e", "err"},
|
||||
Stats: Stats{Hits: 2, Misses: 7},
|
||||
},
|
||||
}, {
|
||||
name: "get err again",
|
||||
key: "err",
|
||||
state: State[string]{
|
||||
Keys: []string{"c", "d", "e", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 7},
|
||||
},
|
||||
}, {
|
||||
name: "evict c",
|
||||
key: "c",
|
||||
evict: true,
|
||||
state: State[string]{
|
||||
Keys: []string{"d", "e", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 7},
|
||||
},
|
||||
}, {
|
||||
name: "reload-all a",
|
||||
key: "a",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "d", "e", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 8},
|
||||
},
|
||||
}, {
|
||||
name: "reload-all b",
|
||||
key: "b",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "b", "e", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 9},
|
||||
},
|
||||
}, {
|
||||
name: "reload-all c",
|
||||
key: "c",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "b", "c", "err"},
|
||||
Stats: Stats{Hits: 3, Misses: 10},
|
||||
},
|
||||
}, {
|
||||
name: "reload-all d",
|
||||
key: "d",
|
||||
state: State[string]{
|
||||
Keys: []string{"a", "b", "c", "d"},
|
||||
Stats: Stats{Hits: 3, Misses: 11},
|
||||
},
|
||||
}, {
|
||||
name: "read a again",
|
||||
key: "a",
|
||||
state: State[string]{
|
||||
Keys: []string{"b", "c", "d", "a"},
|
||||
Stats: Stats{Hits: 4, Misses: 11},
|
||||
},
|
||||
}, {
|
||||
name: "read e, evicting b",
|
||||
key: "e",
|
||||
state: State[string]{
|
||||
Keys: []string{"c", "d", "a", "e"},
|
||||
Stats: Stats{Hits: 4, Misses: 12},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
time.Sleep(tc.sleep)
|
||||
if !tc.evict {
|
||||
val, err := c.Get(tc.key)
|
||||
if tc.key == "err" && err != ErrTest {
|
||||
t.Fatal(tc.name, val)
|
||||
}
|
||||
if tc.key != "err" && val != tc.key {
|
||||
t.Fatal(tc.name, tc.key, val)
|
||||
}
|
||||
} else {
|
||||
c.Evict(tc.key)
|
||||
}
|
||||
|
||||
if err := c.assert(tc.state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_thunderingHerd(t *testing.T) {
|
||||
c := New(Config[string,string]{
|
||||
MaxSize: 4,
|
||||
Src: func(key string) (string, error) {
|
||||
time.Sleep(time.Second)
|
||||
return key, nil
|
||||
},
|
||||
})
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 16384; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
val, err := c.Get("a")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if val != "a" {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
stats := c.Stats()
|
||||
if stats.Hits != 16383 || stats.Misses != 1 {
|
||||
t.Fatal(stats)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user