From a6290db03b9685de59e80ea50005ebe490122fce Mon Sep 17 00:00:00 2001 From: jdl Date: Tue, 6 Apr 2021 08:58:17 +0200 Subject: [PATCH] WIP: kvmemcache --- kvmemcache/cache.go | 23 ++--- kvmemcache/cache_test.go | 189 +++++++++++++++++++++++++-------------- kvmemcache/internal.go | 94 +++++++++---------- kvmemcache/stats.go | 6 ++ 4 files changed, 179 insertions(+), 133 deletions(-) create mode 100644 kvmemcache/stats.go diff --git a/kvmemcache/cache.go b/kvmemcache/cache.go index 72041e5..eea9b90 100644 --- a/kvmemcache/cache.go +++ b/kvmemcache/cache.go @@ -34,39 +34,34 @@ type Config struct { Src func(string) (interface{}, error) } -type Stats struct { - Hits uint64 - Misses uint64 - Expired uint64 -} - func New(conf Config) *Cache { return &Cache{ - lock: sync.Mutex{}, updateLock: keyedmutex.New(), src: conf.Src, ttl: conf.TTL, maxSize: conf.MaxSize, - cache: make(map[string]*list.Element), + lock: sync.Mutex{}, + cache: make(map[string]*list.Element, conf.MaxSize+1), ll: list.New(), } } func (c *Cache) Get(key string) (interface{}, error) { - item := c.getItem(key) - if item == nil { - item = c.loadItemFromSource(key) + val, err, ok := c.get(key) + if ok { + return val, err } - return item.value, item.err + + return c.load(key) } func (c *Cache) Evict(key string) { c.lock.Lock() defer c.lock.Unlock() - c.evictKey(key) + c.evict(key) } -func (c Cache) Stats() Stats { +func (c *Cache) Stats() Stats { c.lock.Lock() defer c.lock.Unlock() return c.stats diff --git a/kvmemcache/cache_test.go b/kvmemcache/cache_test.go index 593529f..a9eb5b4 100644 --- a/kvmemcache/cache_test.go +++ b/kvmemcache/cache_test.go @@ -1,98 +1,149 @@ package kvmemcache import ( + "errors" "fmt" - "math/rand" "testing" "time" ) -func testRunner(c *Cache, t *testing.T, done chan bool) { - rand.Seed(time.Now().UnixNano()) - for i := 0; i < 200001; i++ { - 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 +type State struct { + Keys []string + Stats Stats } -func testCache(name string, c *Cache, t *testing.T) { - N := 8 - done := make(chan bool, N) +func (c *Cache) assert(state State) error { + c.lock.Lock() + defer c.lock.Unlock() - for i := 0; i < N; i++ { - go testRunner(c, t, done) + if len(c.cache) != len(state.Keys) { + return fmt.Errorf( + "Expected %d keys but found %d.", + len(state.Keys), + len(c.cache)) } - for i := 0; i < N; i++ { - <-done + for _, k := range state.Keys { + 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) - 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)) + if c.stats.Misses != state.Stats.Misses { + return fmt.Errorf( + "Expected %d misses, but found %d.", + state.Stats.Misses, + c.stats.Misses) + } + + return nil } -func TestCache(t *testing.T) { +var TestError = errors.New("Hello") + +func TestCache_Basic(t *testing.T) { c := New(Config{ - MaxSize: 2048, - TTL: 100 * 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, + MaxSize: 4, + TTL: 50 * time.Millisecond, Src: func(key string) (interface{}, error) { + if key == "err" { + return nil, TestError + } return key, nil }, }) - c.Get("a") - c.Get("b") - time.Sleep(50 * time.Millisecond) - c.Get("a") - c.Get("b") - c.Get("c") - - stats := c.Stats() - if stats.Expired != 0 { - t.Fatal(stats.Expired) + type testCase struct { + name string + sleep time.Duration + key string + state State } - time.Sleep(50 * time.Millisecond) - c.Get("a") - c.Get("b") - c.Get("c") - - stats = c.Stats() - if stats.Expired != 2 { - t.Fatal(stats.Expired) + cases := []testCase{ + { + name: "get a", + key: "a", + state: State{ + Keys: []string{"a"}, + Stats: Stats{Hits: 0, Misses: 1}, + }, + }, { + 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. diff --git a/kvmemcache/internal.go b/kvmemcache/internal.go index a1a89d5..ea4efee 100644 --- a/kvmemcache/internal.go +++ b/kvmemcache/internal.go @@ -4,70 +4,64 @@ import ( "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() defer c.lock.Unlock() - elem, ok := c.cache[key] - if !ok { - c.stats.Misses++ - return nil + li := c.cache[key] + if li == nil { + return nil, nil, false } - item := elem.Value.(*lruItem) + item := li.Value.(lruItem) + // Maybe evict. if c.ttl != 0 && time.Since(item.createdAt) > c.ttl { - c.stats.Expired++ - c.evictKey(key) - return nil + c.evict(key) + return nil, nil, false } c.stats.Hits++ - c.ll.MoveToFront(elem) - return item + return item.value, item.err, true } -func (c *Cache) loadItemFromSource(key string) *lruItem { +func (c *Cache) load(key string) (interface{}, error) { c.updateLock.Lock(key) defer c.updateLock.Unlock(key) - // May have lost update race. - if item := c.getItem(key); item != nil { - return item + // Check again in case we lost the update race. + val, err, ok := c.get(key) + if ok { + return val, err } - val, err := c.src(key) - item := &lruItem{ - key: key, - value: val, - 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) + // Won the update race. + val, err = c.src(key) + c.put(key, val, err) + return val, err } diff --git a/kvmemcache/stats.go b/kvmemcache/stats.go new file mode 100644 index 0000000..b6415f9 --- /dev/null +++ b/kvmemcache/stats.go @@ -0,0 +1,6 @@ +package kvmemcache + +type Stats struct { + Hits uint64 + Misses uint64 +}