Added kv in-memory cache.
This commit is contained in:
parent
90cde9b4fd
commit
5cab19280e
73
kvmemcache/cache.go
Normal file
73
kvmemcache/cache.go
Normal 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
63
kvmemcache/cache_test.go
Normal 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
71
kvmemcache/internal.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user