New cache implementation and prefetch handing in mw/cache (#731)

* cache: add sharded cache implementation

Add Cache impl and a few tests. This cache is 256-way sharded, mainly
so each shard has it's own lock. The main cache structure is a readonly
jump plane into the right shard.

This should remove the single lock contention on the main lock and
provide more concurrent throughput - Obviously this hasn't been tested
or measured.

The key into the cache was made a uint32 (hash.fnv) and the hashing op
is not using strings.ToLower anymore remove any GC in that code path.

* here too

* Minimum shard size

* typos

* blurp

* small cleanups no defer

* typo

* Add freq based on Johns idea

* cherry-pick conflict resolv

* typo

* update from early code review from john

* add prefetch to the cache

* mw/cache: add prefetch

* remove println

* remove comment

* Fix tests

* Test prefetch in setup

* Add start of cache

* try add diff cache options

* Add hacky testcase

* not needed

* allow the use of a percentage for prefetch

If the TTL falls below xx% do a prefetch, if the record was popular.
Some other fixes and correctly prefetch only popular records.
This commit is contained in:
Miek Gieben
2017-06-13 12:39:10 -07:00
committed by GitHub
parent b1efd3736e
commit e9eda7e7c8
23 changed files with 595 additions and 142 deletions

129
middleware/pkg/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,129 @@
// Package cache implements a cache. The cache hold 256 shards, each shard
// holds a cache: a map with a mutex. There is no fancy expunge algorithm, it
// just randomly evicts elements when it gets full.
package cache
import (
"hash/fnv"
"sync"
)
// Hash returns the FNV hash of what.
func Hash(what []byte) uint32 {
h := fnv.New32()
h.Write(what)
return h.Sum32()
}
// Cache is cache.
type Cache struct {
shards [shardSize]*shard
}
// shard is a cache with random eviction.
type shard struct {
items map[uint32]interface{}
size int
sync.RWMutex
}
// New returns a new cache.
func New(size int) *Cache {
ssize := size / shardSize
if ssize < 512 {
ssize = 512
}
c := &Cache{}
// Initialize all the shards
for i := 0; i < shardSize; i++ {
c.shards[i] = newShard(ssize)
}
return c
}
// Add adds a new element to the cache. If the element already exists it is overwritten.
func (c *Cache) Add(key uint32, el interface{}) {
shard := key & (shardSize - 1)
c.shards[shard].Add(key, el)
}
// Get looks up element index under key.
func (c *Cache) Get(key uint32) (interface{}, bool) {
shard := key & (shardSize - 1)
return c.shards[shard].Get(key)
}
// Remove removes the element indexed with key.
func (c *Cache) Remove(key uint32) {
shard := key & (shardSize - 1)
c.shards[shard].Remove(key)
}
// Len returns the number of elements in the cache.
func (c *Cache) Len() int {
l := 0
for _, s := range c.shards {
l += s.Len()
}
return l
}
// newShard returns a new shard with size.
func newShard(size int) *shard { return &shard{items: make(map[uint32]interface{}), size: size} }
// Add adds element indexed by key into the cache. Any existing element is overwritten
func (s *shard) Add(key uint32, el interface{}) {
l := s.Len()
if l+1 > s.size {
s.Evict()
}
s.Lock()
s.items[key] = el
s.Unlock()
}
// Remove removes the element indexed by key from the cache.
func (s *shard) Remove(key uint32) {
s.Lock()
delete(s.items, key)
s.Unlock()
}
// Evict removes a random element from the cache.
func (s *shard) Evict() {
s.Lock()
defer s.Unlock()
key := -1
for k := range s.items {
key = int(k)
break
}
if key == -1 {
// empty cache
return
}
delete(s.items, uint32(key))
}
// Get looks up the element indexed under key.
func (s *shard) Get(key uint32) (interface{}, bool) {
s.RLock()
el, found := s.items[key]
s.RUnlock()
return el, found
}
// Len returns the current length of the cache.
func (s *shard) Len() int {
s.RLock()
l := len(s.items)
s.RUnlock()
return l
}
const shardSize = 256

31
middleware/pkg/cache/cache_test.go vendored Normal file
View File

@@ -0,0 +1,31 @@
package cache
import "testing"
func TestCacheAddAndGet(t *testing.T) {
c := New(4)
c.Add(1, 1)
if _, found := c.Get(1); !found {
t.Fatal("Failed to find inserted record")
}
}
func TestCacheLen(t *testing.T) {
c := New(4)
c.Add(1, 1)
if l := c.Len(); l != 1 {
t.Fatalf("Cache size should %d, got %d", 1, l)
}
c.Add(1, 1)
if l := c.Len(); l != 1 {
t.Fatalf("Cache size should %d, got %d", 1, l)
}
c.Add(2, 2)
if l := c.Len(); l != 2 {
t.Fatalf("Cache size should %d, got %d", 2, l)
}
}

60
middleware/pkg/cache/shard_test.go vendored Normal file
View File

@@ -0,0 +1,60 @@
package cache
import "testing"
func TestShardAddAndGet(t *testing.T) {
s := newShard(4)
s.Add(1, 1)
if _, found := s.Get(1); !found {
t.Fatal("Failed to find inserted record")
}
}
func TestShardLen(t *testing.T) {
s := newShard(4)
s.Add(1, 1)
if l := s.Len(); l != 1 {
t.Fatalf("Shard size should %d, got %d", 1, l)
}
s.Add(1, 1)
if l := s.Len(); l != 1 {
t.Fatalf("Shard size should %d, got %d", 1, l)
}
s.Add(2, 2)
if l := s.Len(); l != 2 {
t.Fatalf("Shard size should %d, got %d", 2, l)
}
}
func TestShardEvict(t *testing.T) {
s := newShard(1)
s.Add(1, 1)
s.Add(2, 2)
// 1 should be gone
if _, found := s.Get(1); found {
t.Fatal("Found item that should have been evicted")
}
}
func TestShardLenEvict(t *testing.T) {
s := newShard(4)
s.Add(1, 1)
s.Add(2, 1)
s.Add(3, 1)
s.Add(4, 1)
if l := s.Len(); l != 4 {
t.Fatalf("Shard size should %d, got %d", 4, l)
}
// This should evict one element
s.Add(5, 1)
if l := s.Len(); l != 4 {
t.Fatalf("Shard size should %d, got %d", 4, l)
}
}