WIP: kvmemcache
This commit is contained in:
parent
bd37278092
commit
a6290db03b
@ -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
|
||||||
|
@ -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)
|
|
||||||
expectedVal := "value for " + key
|
|
||||||
|
|
||||||
val, err := c.Get(key)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val.(string) != expectedVal {
|
|
||||||
t.Fatal(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCache(name string, c *Cache, t *testing.T) {
|
func (c *Cache) assert(state State) error {
|
||||||
N := 8
|
c.lock.Lock()
|
||||||
done := make(chan bool, N)
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
for i := 0; i < N; i++ {
|
if len(c.cache) != len(state.Keys) {
|
||||||
go testRunner(c, t, done)
|
return fmt.Errorf(
|
||||||
|
"Expected %d keys but found %d.",
|
||||||
|
len(state.Keys),
|
||||||
|
len(c.cache))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < N; i++ {
|
for _, k := range state.Keys {
|
||||||
<-done
|
if _, ok := c.cache[k]; !ok {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Expected key %s not found.",
|
||||||
|
k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := c.Stats()
|
if c.stats.Hits != state.Stats.Hits {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Expected %d hits, but found %d.",
|
||||||
|
state.Stats.Hits,
|
||||||
|
c.stats.Hits)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(name)
|
if c.stats.Misses != state.Stats.Misses {
|
||||||
fmt.Printf(" Hits: %d\n", stats.Hits)
|
return fmt.Errorf(
|
||||||
fmt.Printf(" Misses: %d\n", stats.Misses)
|
"Expected %d misses, but found %d.",
|
||||||
fmt.Printf(" Expired: %d\n", stats.Expired)
|
state.Stats.Misses,
|
||||||
fmt.Printf(" Hit-rate: %.2f%%\n",
|
c.stats.Misses)
|
||||||
100*float64(stats.Hits)/float64(stats.Hits+stats.Misses))
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache(t *testing.T) {
|
var TestError = errors.New("Hello")
|
||||||
|
|
||||||
|
func TestCache_Basic(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) {
|
|
||||||
return fmt.Sprintf("value for %s", key), nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
testCache("LRUTimeout", c, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCacheTTL(t *testing.T) {
|
|
||||||
c := New(Config{
|
|
||||||
MaxSize: 2048,
|
|
||||||
TTL: 100 * time.Millisecond,
|
|
||||||
Src: func(key string) (interface{}, error) {
|
Src: func(key string) (interface{}, error) {
|
||||||
|
if key == "err" {
|
||||||
|
return nil, TestError
|
||||||
|
}
|
||||||
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.
|
||||||
|
@ -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()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
c.stats.Misses++
|
||||||
|
c.cache[key] = c.ll.PushFront(lruItem{
|
||||||
|
key: key,
|
||||||
|
createdAt: time.Now(),
|
||||||
|
value: value,
|
||||||
|
err: err,
|
||||||
|
})
|
||||||
|
|
||||||
|
if c.maxSize != 0 && len(c.cache) > c.maxSize {
|
||||||
|
li := c.ll.Back()
|
||||||
|
c.ll.Remove(li)
|
||||||
|
delete(c.cache, li.Value.(lruItem).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) evict(key string) {
|
||||||
|
elem := c.cache[key]
|
||||||
|
if elem != nil {
|
||||||
|
delete(c.cache, key)
|
||||||
|
c.ll.Remove(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) get(key string) (val interface{}, err error, ok bool) {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
elem, ok := c.cache[key]
|
li := c.cache[key]
|
||||||
if !ok {
|
if li == nil {
|
||||||
c.stats.Misses++
|
return nil, nil, false
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item := elem.Value.(*lruItem)
|
item := li.Value.(lruItem)
|
||||||
|
// Maybe evict.
|
||||||
if c.ttl != 0 && time.Since(item.createdAt) > c.ttl {
|
if c.ttl != 0 && time.Since(item.createdAt) > c.ttl {
|
||||||
c.stats.Expired++
|
c.evict(key)
|
||||||
c.evictKey(key)
|
return nil, nil, false
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.stats.Hits++
|
c.stats.Hits++
|
||||||
c.ll.MoveToFront(elem)
|
return item.value, item.err, true
|
||||||
return item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) loadItemFromSource(key string) *lruItem {
|
func (c *Cache) load(key string) (interface{}, error) {
|
||||||
c.updateLock.Lock(key)
|
c.updateLock.Lock(key)
|
||||||
defer c.updateLock.Unlock(key)
|
defer c.updateLock.Unlock(key)
|
||||||
|
|
||||||
// May have lost update race.
|
// Check again in case we lost the update race.
|
||||||
if item := c.getItem(key); item != nil {
|
val, err, ok := c.get(key)
|
||||||
return item
|
if ok {
|
||||||
|
return val, err
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := c.src(key)
|
// Won the update race.
|
||||||
item := &lruItem{
|
val, err = c.src(key)
|
||||||
key: key,
|
c.put(key, val, err)
|
||||||
value: val,
|
return val, err
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ttl != 0 {
|
|
||||||
item.createdAt = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
elem, ok := c.cache[key]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(c.cache, key)
|
|
||||||
c.ll.Remove(elem)
|
|
||||||
}
|
}
|
||||||
|
6
kvmemcache/stats.go
Normal file
6
kvmemcache/stats.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package kvmemcache
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
Hits uint64
|
||||||
|
Misses uint64
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user