Added kv in-memory cache.

This commit is contained in:
jdl 2021-04-02 17:14:32 +02:00
parent 90cde9b4fd
commit 5cab19280e
3 changed files with 207 additions and 0 deletions

73
kvmemcache/cache.go Normal file
View File

@ -0,0 +1,73 @@
package kvmemcache
import (
"container/list"
"sync"
"time"
"git.crumpington.com/public/toolbox/keyedmutex"
)
type Cache struct {
updateLock keyedmutex.KeyedMutex
src func(string) (interface{}, error)
ttl time.Duration
maxSize int
// Lock protects cache, ll, and stats.
lock sync.Mutex
cache map[string]*list.Element
ll *list.List
stats Stats
}
type lruItem struct {
key string
createdAt time.Time
value interface{}
err error
}
type Config struct {
MaxSize int
TTL time.Duration // Zero to ignore.
Src func(string) (interface{}, error)
}
type Stats struct {
Hits uint64
Misses 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),
ll: list.New(),
}
}
func (c *Cache) Get(key string) (interface{}, error) {
item := c.getItem(key)
if item == nil {
item = c.loadItemFromSource(key)
}
return item.value, item.err
}
func (c *Cache) Evict(key string) {
c.lock.Lock()
defer c.lock.Unlock()
c.evictKey(key)
}
func (c Cache) GetStats() Stats {
c.lock.Lock()
defer c.lock.Unlock()
return c.stats
}

63
kvmemcache/cache_test.go Normal file
View File

@ -0,0 +1,63 @@
package kvmemcache
import (
"fmt"
"math/rand"
"testing"
"time"
)
func testRunner(c *Cache, t *testing.T, done chan bool) {
for i := 0; i < 2000000; 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
}
func testCache(name string, c *Cache, t *testing.T) {
N := 8
done := make(chan bool, N)
for i := 0; i < N; i++ {
go testRunner(c, t, done)
}
for i := 0; i < N; i++ {
<-done
}
stats := c.GetStats()
fmt.Println(name)
fmt.Printf(" Hits: %d\n", stats.Hits)
fmt.Printf(" Misses: %d\n", stats.Misses)
fmt.Printf(" Hit-rate: %.2f%%\n",
100*float64(stats.Hits)/float64(stats.Hits+stats.Misses))
}
func TestCache(t *testing.T) {
c := New(Config{
MaxSize: 2048,
TTL: time.Second,
Src: func(key string) (interface{}, error) {
return fmt.Sprintf("value for %s", key), nil
},
})
testCache("LRUTimeout", c, t)
}

71
kvmemcache/internal.go Normal file
View File

@ -0,0 +1,71 @@
package kvmemcache
import "time"
func (c *Cache) getItem(key string) *lruItem {
c.lock.Lock()
defer c.lock.Unlock()
li, ok := c.cache[key]
if !ok {
c.stats.Misses++
return nil
}
item := li.Value.(*lruItem)
if c.ttl != 0 && time.Since(item.createdAt) > c.ttl {
c.evictKey(key)
c.stats.Misses++
return nil
}
c.stats.Hits++
c.ll.MoveToFront(li)
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,
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 {
li := c.ll.Back()
c.ll.Remove(li)
delete(c.cache, li.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)
}