diff --git a/kvmemcache/cache.go b/kvmemcache/cache.go new file mode 100644 index 0000000..9baeb8a --- /dev/null +++ b/kvmemcache/cache.go @@ -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 +} diff --git a/kvmemcache/cache_test.go b/kvmemcache/cache_test.go new file mode 100644 index 0000000..1cb901e --- /dev/null +++ b/kvmemcache/cache_test.go @@ -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) +} diff --git a/kvmemcache/internal.go b/kvmemcache/internal.go new file mode 100644 index 0000000..34ffe5f --- /dev/null +++ b/kvmemcache/internal.go @@ -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) +}