WIP: kvmemcache

This commit is contained in:
jdl 2021-04-06 08:58:17 +02:00
parent bd37278092
commit a6290db03b
4 changed files with 179 additions and 133 deletions

View File

@ -34,39 +34,34 @@ type Config struct {
Src func(string) (interface{}, error) Src func(string) (interface{}, error)
} }
type Stats struct {
Hits uint64
Misses uint64
Expired uint64
}
func New(conf Config) *Cache { func New(conf Config) *Cache {
return &Cache{ return &Cache{
lock: sync.Mutex{},
updateLock: keyedmutex.New(), updateLock: keyedmutex.New(),
src: conf.Src, src: conf.Src,
ttl: conf.TTL, ttl: conf.TTL,
maxSize: conf.MaxSize, maxSize: conf.MaxSize,
cache: make(map[string]*list.Element), lock: sync.Mutex{},
cache: make(map[string]*list.Element, conf.MaxSize+1),
ll: list.New(), ll: list.New(),
} }
} }
func (c *Cache) Get(key string) (interface{}, error) { func (c *Cache) Get(key string) (interface{}, error) {
item := c.getItem(key) val, err, ok := c.get(key)
if item == nil { if ok {
item = c.loadItemFromSource(key) return val, err
} }
return item.value, item.err
return c.load(key)
} }
func (c *Cache) Evict(key string) { func (c *Cache) Evict(key string) {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
c.evictKey(key) c.evict(key)
} }
func (c Cache) Stats() Stats { func (c *Cache) Stats() Stats {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
return c.stats return c.stats

View File

@ -1,98 +1,149 @@
package kvmemcache package kvmemcache
import ( import (
"errors"
"fmt" "fmt"
"math/rand"
"testing" "testing"
"time" "time"
) )
func testRunner(c *Cache, t *testing.T, done chan bool) { type State struct {
rand.Seed(time.Now().UnixNano()) Keys []string
for i := 0; i < 200001; i++ { Stats Stats
x := rand.Int31n(50)
if rand.Float64() < 0.01 {
x = rand.Int31n(9999)
} }
key := fmt.Sprintf("key-%v", x) func (c *Cache) assert(state State) error {
expectedVal := "value for " + key c.lock.Lock()
defer c.lock.Unlock()
val, err := c.Get(key) if len(c.cache) != len(state.Keys) {
return fmt.Errorf(
if err != nil { "Expected %d keys but found %d.",
t.Fatal(err) len(state.Keys),
len(c.cache))
} }
if val.(string) != expectedVal { for _, k := range state.Keys {
t.Fatal(val) if _, ok := c.cache[k]; !ok {
return fmt.Errorf(
"Expected key %s not found.",
k)
} }
} }
done <- true
if c.stats.Hits != state.Stats.Hits {
return fmt.Errorf(
"Expected %d hits, but found %d.",
state.Stats.Hits,
c.stats.Hits)
} }
func testCache(name string, c *Cache, t *testing.T) { if c.stats.Misses != state.Stats.Misses {
N := 8 return fmt.Errorf(
done := make(chan bool, N) "Expected %d misses, but found %d.",
state.Stats.Misses,
for i := 0; i < N; i++ { c.stats.Misses)
go testRunner(c, t, done)
} }
for i := 0; i < N; i++ { return nil
<-done
} }
stats := c.Stats() var TestError = errors.New("Hello")
fmt.Println(name) func TestCache_Basic(t *testing.T) {
fmt.Printf(" Hits: %d\n", stats.Hits)
fmt.Printf(" Misses: %d\n", stats.Misses)
fmt.Printf(" Expired: %d\n", stats.Expired)
fmt.Printf(" Hit-rate: %.2f%%\n",
100*float64(stats.Hits)/float64(stats.Hits+stats.Misses))
}
func TestCache(t *testing.T) {
c := New(Config{ c := New(Config{
MaxSize: 2048, MaxSize: 4,
TTL: 100 * time.Millisecond, TTL: 50 * time.Millisecond,
Src: func(key string) (interface{}, error) { Src: func(key string) (interface{}, error) {
return fmt.Sprintf("value for %s", key), nil if key == "err" {
}, return nil, TestError
})
testCache("LRUTimeout", c, t)
} }
func TestCacheTTL(t *testing.T) {
c := New(Config{
MaxSize: 2048,
TTL: 100 * time.Millisecond,
Src: func(key string) (interface{}, error) {
return key, nil return key, nil
}, },
}) })
c.Get("a") type testCase struct {
c.Get("b") name string
time.Sleep(50 * time.Millisecond) sleep time.Duration
c.Get("a") key string
c.Get("b") state State
c.Get("c")
stats := c.Stats()
if stats.Expired != 0 {
t.Fatal(stats.Expired)
} }
time.Sleep(50 * time.Millisecond) cases := []testCase{
c.Get("a") {
c.Get("b") name: "get a",
c.Get("c") key: "a",
state: State{
stats = c.Stats() Keys: []string{"a"},
if stats.Expired != 2 { Stats: Stats{Hits: 0, Misses: 1},
t.Fatal(stats.Expired) },
}, {
name: "get a again",
key: "a",
state: State{
Keys: []string{"a"},
Stats: Stats{Hits: 1, Misses: 1},
},
}, {
name: "sleep, then get a again",
sleep: 55 * time.Millisecond,
key: "a",
state: State{
Keys: []string{"a"},
Stats: Stats{Hits: 1, Misses: 2},
},
}, {
name: "get b",
key: "b",
state: State{
Keys: []string{"a", "b"},
Stats: Stats{Hits: 1, Misses: 3},
},
}, {
name: "get c",
key: "c",
state: State{
Keys: []string{"a", "b", "c"},
Stats: Stats{Hits: 1, Misses: 4},
},
}, {
name: "get d",
key: "d",
state: State{
Keys: []string{"a", "b", "c", "d"},
Stats: Stats{Hits: 1, Misses: 5},
},
}, {
name: "get e",
key: "e",
state: State{
Keys: []string{"b", "c", "d", "e"},
Stats: Stats{Hits: 1, Misses: 6},
},
}, {
name: "get c again",
key: "c",
state: State{
Keys: []string{"b", "c", "d", "e"},
Stats: Stats{Hits: 2, Misses: 6},
},
},
} }
for _, tc := range cases {
time.Sleep(tc.sleep)
val, err := c.Get(tc.key)
if tc.key == "err" && err != TestError {
t.Fatal(tc.name, val)
} }
if tc.key != "err" && val.(string) != tc.key {
t.Fatal(tc.name, tc.key, val)
}
if err := c.assert(tc.state); err != nil {
t.Fatal(err)
}
}
}
// TODO: Test thundering herd mitigation.

View File

@ -4,70 +4,64 @@ import (
"time" "time"
) )
func (c *Cache) getItem(key string) *lruItem { func (c *Cache) put(key string, value interface{}, err error) {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
elem, ok := c.cache[key]
if !ok {
c.stats.Misses++ c.stats.Misses++
return nil c.cache[key] = c.ll.PushFront(lruItem{
}
item := elem.Value.(*lruItem)
if c.ttl != 0 && time.Since(item.createdAt) > c.ttl {
c.stats.Expired++
c.evictKey(key)
return nil
}
c.stats.Hits++
c.ll.MoveToFront(elem)
return item
}
func (c *Cache) loadItemFromSource(key string) *lruItem {
c.updateLock.Lock(key)
defer c.updateLock.Unlock(key)
// May have lost update race.
if item := c.getItem(key); item != nil {
return item
}
val, err := c.src(key)
item := &lruItem{
key: key, key: key,
value: val, createdAt: time.Now(),
value: value,
err: err, err: err,
} })
if c.ttl != 0 { if c.maxSize != 0 && len(c.cache) > c.maxSize {
item.createdAt = time.Now() li := c.ll.Back()
} c.ll.Remove(li)
delete(c.cache, li.Value.(lruItem).key)
c.putItem(key, item)
return item
}
func (c *Cache) putItem(key string, item *lruItem) {
c.lock.Lock()
defer c.lock.Unlock()
c.cache[key] = c.ll.PushFront(item)
if c.maxSize > 0 && len(c.cache) > c.maxSize {
elem := c.ll.Back()
c.ll.Remove(elem)
delete(c.cache, elem.Value.(*lruItem).key)
} }
} }
func (c *Cache) evictKey(key string) { func (c *Cache) evict(key string) {
elem, ok := c.cache[key] elem := c.cache[key]
if !ok { if elem != nil {
return
}
delete(c.cache, key) delete(c.cache, key)
c.ll.Remove(elem) c.ll.Remove(elem)
} }
}
func (c *Cache) get(key string) (val interface{}, err error, ok bool) {
c.lock.Lock()
defer c.lock.Unlock()
li := c.cache[key]
if li == nil {
return nil, nil, false
}
item := li.Value.(lruItem)
// Maybe evict.
if c.ttl != 0 && time.Since(item.createdAt) > c.ttl {
c.evict(key)
return nil, nil, false
}
c.stats.Hits++
return item.value, item.err, true
}
func (c *Cache) load(key string) (interface{}, error) {
c.updateLock.Lock(key)
defer c.updateLock.Unlock(key)
// Check again in case we lost the update race.
val, err, ok := c.get(key)
if ok {
return val, err
}
// Won the update race.
val, err = c.src(key)
c.put(key, val, err)
return val, err
}

6
kvmemcache/stats.go Normal file
View File

@ -0,0 +1,6 @@
package kvmemcache
type Stats struct {
Hits uint64
Misses uint64
}