Compare commits

...

13 Commits
master ... v1

Author SHA1 Message Date
  maze b17e31603b Looks like plugins are broken in 0.5 5 years ago
  maze 5c220d905f Fix plugins? 5 years ago
  maze 40129683c2 Use matrix 5 years ago
  maze 23488a078b Build for 1.7 5 years ago
  maze be06772992 Use workspace 5 years ago
  maze f5760d353f Use pipeline 5 years ago
  maze 98c1345156 Use pipeline 5 years ago
  maze 43502c507a Use pipeline 5 years ago
  maze e8036c27c0 Use pipeline 5 years ago
  maze 2a189abd39 Badges 5 years ago
  maze 1a432200ca Trigger build 5 years ago
  maze 542244a7dc Added drone config 5 years ago
  maze c42f96e6f6 Initial import 5 years ago
11 changed files with 1150 additions and 1 deletions
Split View
  1. +16
    -0
      .drone.yml
  2. +3
    -1
      README.md
  3. +25
    -0
      backend.go
  4. +130
    -0
      cache.go
  5. +102
    -0
      cache_test.go
  6. +34
    -0
      gc.go
  7. +35
    -0
      hash.go
  8. +285
    -0
      kv.go
  9. +139
    -0
      kv_test.go
  10. +263
    -0
      lru.go
  11. +118
    -0
      lru_test.go

+ 16
- 0
.drone.yml View File

@ -0,0 +1,16 @@
workspace:
base: /go
path: src/git.maze.io/go/cache
pipeline:
build:
image: golang:${GO_VERSION}
commands:
- go get -v
- go build -v
- go test -v -cover
matrix:
GO_VERSION:
- latest
- 1.7

+ 3
- 1
README.md View File

@ -1,3 +1,5 @@
# cache
Package cache contains caching functions.
[![GoDoc](https://godoc.org/maze.io/cache.v1?status.svg)](https://godoc.org/maze.io/cache.v1) [![Build Status](https://ci.maze.io/api/badges/go/cache/status.svg)](https://ci.maze.io/go/cache)
Package cache contains caching functions.

+ 25
- 0
backend.go View File

@ -0,0 +1,25 @@
package cache
// Backend defines an interface for storing cached items.
type Backend interface {
// Len returns the number of unique keys in the cache
Len() int
// Contains checks if a key exists in the cache
Contains(key interface{}) (bool, error)
// Delete an item from the cache
Delete(key interface{}) (bool, error)
// Get an item from the cache
Get(key interface{}) (interface{}, bool, error)
// Iter iterates over all the key-value pairs in the cache
Iter(func(key, value interface{})) error
// Set an item in the cache
Set(key, value interface{}) error
// Replace an item in the cache if it exists, ignore otherwise
Replace(key, value interface{}) (bool, error)
}

+ 130
- 0
cache.go View File

@ -0,0 +1,130 @@
// Package cache contains caching functions
package cache
import "time"
// Cache describes an interface for storing arbitrary key/value pairs. Structs
// implementing the Cache interface should have a configurable Backend.
type Cache interface {
// Len returns the number of unique keys in the cache
Len() int
// Contains if the item is found in the cache
Contains(key interface{}) (bool, error)
// Get an item from the cache
Get(key interface{}) (interface{}, bool, error)
// Set an item in the cache
Set(key, value interface{}) error
// RunGC runs garbage collection on the cache
RunGC()
}
// SimpleCache is a cache without any additional features.
type SimpleCache struct {
Backend
}
// NewCache returns a simple cache without expiration. By default the MemoryKV
// backend is used.
func NewCache() *SimpleCache {
return &SimpleCache{NewMemoryKV()}
}
func (c *SimpleCache) RunGC() {}
// TTLCache is a cache with values that can expire.
type TTLCache struct {
Backend Backend
TTL time.Duration
gc *GC
}
// NewTTLCache runs with a timed garbage collector that runs at the selected
// interval. By default the MemoryKV backend is used.
func NewTTLCache(ttl, purge time.Duration) *TTLCache {
c := &TTLCache{
Backend: NewMemoryKV(),
TTL: ttl,
}
c.gc = NewGC(purge, c)
go c.gc.Run()
return c
}
// TTLCacheItem is a single cached item.
type TTLCacheItem struct {
Value interface{}
Expire int64
}
// Contains checks if a key is contained in the cache and if it hasn't expired.
func (c *TTLCache) Contains(key interface{}) (bool, error) {
if item, ok, err := c.Backend.Get(key); err != nil {
return false, err
} else if ok {
now := time.Now().Unix()
if item.(*TTLCacheItem).Expire > now {
return true, nil
}
if _, err = c.Backend.Delete(key); err != nil {
return true, err
}
}
return false, nil
}
// Get a value from the cache.
func (c *TTLCache) Get(key interface{}) (interface{}, bool, error) {
if item, ok, err := c.Backend.Get(key); err != nil {
return nil, false, err
} else if ok {
now := time.Now().Unix()
if item.(*TTLCacheItem).Expire > now {
return item.(*TTLCacheItem).Value, true, nil
}
if _, err = c.Backend.Delete(key); err != nil {
return nil, false, err
}
}
return nil, false, nil
}
// Len is the number of cached entries, it does not check if the keys are
// expired.
func (c *TTLCache) Len() int {
return c.Backend.Len()
}
// Set a key-value pair.
func (c *TTLCache) Set(key, value interface{}) error {
return c.Backend.Set(key, &TTLCacheItem{
Value: value,
Expire: time.Now().Add(c.TTL).Unix(),
})
}
// Stop the GC process.
func (c *TTLCache) Stop() {
if c.gc != nil {
c.gc.Stop()
}
}
// RunGC removes expired entries from the cache.
func (c *TTLCache) RunGC() {
now := time.Now().Unix()
c.Backend.Iter(func(key, value interface{}) {
if value.(*TTLCacheItem).Expire <= now {
c.Backend.Delete(key)
}
})
}
var (
_ Cache = (*SimpleCache)(nil)
_ Cache = (*TTLCache)(nil)
)

+ 102
- 0
cache_test.go View File

@ -0,0 +1,102 @@
package cache
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
)
func testCache(t *testing.T, c Cache) {
var (
ok bool
err error
)
iterTest := make(map[interface{}]interface{})
for _, testCase := range kvTestCases {
c.Set(testCase.Key, testCase.Value)
iterTest[testCase.Key] = testCase.Value
if ok, err = c.Contains(testCase.Key); err != nil {
t.Fatalf("contains failed: %v", err)
} else if !ok {
t.Fatalf("key %v missing", testCase.Key)
}
var value interface{}
if value, ok, err = c.Get(testCase.Key); err != nil {
t.Fatalf("get failed: %v", err)
} else if !ok {
t.Fatalf("key %v missing", testCase.Key)
} else if !reflect.DeepEqual(value, testCase.Value) {
t.Fatalf("corrupt, expected %+v, got %+v", testCase.Value, value)
}
}
if l := c.Len(); l != len(kvTestCases) {
t.Fatalf("corrupt, expected %d, got %d length", len(kvTestCases), l)
}
}
func TestCache(t *testing.T) {
t.Run("SimpleCache", func(t *testing.T) {
testCache(t, NewCache())
})
t.Run("TTLCache", func(t *testing.T) {
c := NewTTLCache(time.Second, time.Millisecond*100)
defer c.Stop()
testCache(t, c)
})
}
func TestTTLCache(t *testing.T) {
var backends = []Backend{
NewMemoryKV(),
NewMemoryLRU(1),
}
for _, backend := range backends {
t.Run(strings.SplitN(fmt.Sprintf("%T", backend), ".", 2)[1], func(t *testing.T) {
c := NewTTLCache(time.Second, time.Millisecond*100)
c.Backend = backend
defer c.Stop()
c.Set("foo", "bar")
if l := c.Len(); l != 1 {
t.Fatalf("len returned %d, expected 1", l)
}
if v, ok, err := c.Get("foo"); err != nil {
t.Fatalf("get failed: %v", err)
} else if !ok {
t.Fatal("get failed")
} else {
if s, ok := v.(string); !ok {
t.Fatalf("get returned %T, expected string", v)
} else if s != "bar" {
t.Fatalf("get returned %s, expected \"bar\"", s)
}
}
time.Sleep(time.Second)
if ok, err := c.Contains("foo"); err != nil {
t.Fatalf("contains failed: %v", err)
} else if ok {
t.Fatal("contains failed, it should return false")
}
if _, ok, err := c.Get("foo"); err != nil {
t.Fatalf("get failed: %v", err)
} else if ok {
t.Fatal("get failed, it should return false")
}
if l := c.Len(); l != 0 {
t.Fatalf("len returned %d, expected 0", l)
}
})
}
}

+ 34
- 0
gc.go View File

@ -0,0 +1,34 @@
package cache
import "time"
type GC struct {
Interval time.Duration
Cache Cache
stop chan struct{}
}
func NewGC(interval time.Duration, cache Cache) *GC {
return &GC{
Interval: interval,
Cache: cache,
stop: make(chan struct{}),
}
}
func (gc *GC) Run() {
ticker := time.NewTicker(gc.Interval)
for {
select {
case <-ticker.C:
gc.Cache.RunGC()
case <-gc.stop:
ticker.Stop()
return
}
}
}
func (gc *GC) Stop() {
gc.stop <- struct{}{}
}

+ 35
- 0
hash.go View File

@ -0,0 +1,35 @@
package cache
import (
"bytes"
"encoding/gob"
"hash"
"hash/fnv"
)
// Hasher is the hash interface used for computing hashes by the Hash function.
// By default an FNV-1a hash function is used.
var Hasher func() hash.Hash
// Hash an arbitrary object.
func Hash(value interface{}) ([]byte, error) {
return HashUsing(value, Hasher())
}
// HashUsing hashes an arbitrary object using the provider hasher.
func HashUsing(value interface{}, hasher hash.Hash) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(value)
if err != nil {
return nil, err
}
hasher.Reset()
return hasher.Sum(buf.Bytes()), nil
}
func init() {
Hasher = func() hash.Hash {
return hash.Hash(fnv.New64a())
}
}

+ 285
- 0
kv.go View File

@ -0,0 +1,285 @@
package cache
import (
"encoding/gob"
"encoding/hex"
"fmt"
"hash"
"os"
"path"
"path/filepath"
"sync"
)
// kv is a key-value pair
type kv struct {
Key, Value interface{}
}
// MemoryKV implements a concurrent safe in-memory key-value store.
type MemoryKV struct {
items map[interface{}]interface{}
mutex sync.RWMutex
}
func NewMemoryKV() *MemoryKV {
return &MemoryKV{
items: make(map[interface{}]interface{}),
}
}
func (kv *MemoryKV) Contains(key interface{}) (bool, error) {
_, contained := kv.items[key]
return contained, nil
}
func (kv *MemoryKV) Delete(key interface{}) (bool, error) {
if _, ok := kv.items[key]; ok {
delete(kv.items, key)
return true, nil
}
return false, nil
}
func (kv *MemoryKV) Get(key interface{}) (interface{}, bool, error) {
value, ok := kv.items[key]
return value, ok, nil
}
func (kv *MemoryKV) Iter(fn func(key, value interface{})) error {
kv.mutex.RLock()
for key, value := range kv.items {
fn(key, value)
}
kv.mutex.RUnlock()
return nil
}
func (kv *MemoryKV) Len() int {
kv.mutex.RLock()
size := len(kv.items)
kv.mutex.RUnlock()
return size
}
func (kv *MemoryKV) Set(key, value interface{}) error {
kv.mutex.Lock()
kv.items[key] = value
kv.mutex.Unlock()
return nil
}
func (kv *MemoryKV) Replace(key, value interface{}) (bool, error) {
kv.mutex.Lock()
var exists bool
if _, exists = kv.items[key]; exists {
kv.items[key] = value
}
kv.mutex.Unlock()
return exists, nil
}
// DiskKV implements a concurrent safe on-disk key-value store.
type DiskKV struct {
Hasher hash.Hash
path string
perm os.FileMode
size int
mutex sync.RWMutex
}
func NewDiskKV(path string, perm os.FileMode) (*DiskKV, error) {
d, err := newDiskKV(path, perm)
if err != nil {
return nil, err
}
if err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
d.size++
}
return nil
}); err != nil {
return nil, err
}
return d, nil
}
func newDiskKV(path string, perm os.FileMode) (*DiskKV, error) {
var err error
if !filepath.IsAbs(path) {
if path, err = filepath.Abs(path); err != nil {
return nil, err
}
}
var info os.FileInfo
if info, err = os.Stat(path); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err = os.MkdirAll(path, perm); err != nil {
return nil, err
}
} else if !info.IsDir() {
return nil, fmt.Errorf("cache: %s is not a directory", path)
} else if err = os.Chmod(path, perm); err != nil {
return nil, err
}
return &DiskKV{
path: path,
perm: perm,
Hasher: Hasher(),
}, nil
}
func (d *DiskKV) pathFor(key interface{}) string {
b, err := HashUsing(key, d.Hasher)
if err != nil {
panic(err)
}
h := hex.EncodeToString(b)
return filepath.Join(d.path, h[:2], h[2:4], h)
}
func (d *DiskKV) Contains(key interface{}) (bool, error) {
d.mutex.RLock()
contains, err := d.contains(d.pathFor(key))
d.mutex.RUnlock()
return contains, err
}
func (d *DiskKV) contains(filename string) (bool, error) {
info, err := os.Stat(filename)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return !info.IsDir(), nil
}
func (d *DiskKV) Len() int {
return d.size
}
func (d *DiskKV) Delete(key interface{}) (bool, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
p := d.pathFor(key)
c, err := d.contains(p)
if !c || err != nil {
return c, err
}
return d.delete(p)
}
func (d *DiskKV) delete(p string) (bool, error) {
if err := os.Remove(p); err != nil {
return false, err
}
// Cleanup cache parent directories if there are no entries.
for _, parent := range []string{path.Dir(p), path.Dir(path.Dir(p))} {
d, err := os.Open(parent)
if err != nil {
return true, err
}
defer d.Close()
items, _ := d.Readdirnames(-1)
if len(items) == 0 {
if err = os.Remove(parent); err != nil {
return true, err
}
}
}
d.size--
return true, nil
}
func (d *DiskKV) Get(key interface{}) (interface{}, bool, error) {
value := new(kv)
if err := d.get(d.pathFor(key), value); err != nil {
return nil, false, err
}
return value.Value, true, nil
}
func (d *DiskKV) get(filename string, value interface{}) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
dec := gob.NewDecoder(f)
return dec.Decode(value)
}
func (d *DiskKV) Iter(fn func(key, value interface{})) error {
return nil
}
func (d *DiskKV) Set(key, value interface{}) error {
d.mutex.Lock()
defer d.mutex.Unlock()
return d.set(d.pathFor(key), key, value)
}
func (d *DiskKV) set(filename string, key, value interface{}) error {
c, err := d.contains(filename)
if err != nil {
return err
}
if err := os.MkdirAll(path.Dir(filename), d.perm); err != nil {
return err
}
var f *os.File
if f, err = os.Create(filename); err != nil {
return err
}
defer f.Close()
enc := gob.NewEncoder(f)
if err := enc.Encode(&kv{key, value}); err != nil {
return err
}
if !c {
d.size++
}
return nil
}
func (d *DiskKV) Replace(key, value interface{}) (bool, error) {
c, err := d.Contains(key)
if err != nil {
return c, err
}
if c {
return true, d.Set(key, value)
}
return false, nil
}
var (
_ Backend = (*MemoryKV)(nil)
_ Backend = (*DiskKV)(nil)
)

+ 139
- 0
kv_test.go View File

@ -0,0 +1,139 @@
package cache
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
var kvTestCases = []kv{
{"foo", "bar"},
{"int", 666},
{"float", 23.42},
{"slice", []string{"foo", "bar"}},
}
func testKV(t *testing.T, b Backend) {
var (
ok bool
err error
)
iterTest := make(map[interface{}]interface{})
for _, testCase := range kvTestCases {
b.Set(testCase.Key, testCase.Value)
iterTest[testCase.Key] = testCase.Value
if ok, err = b.Contains(testCase.Key); err != nil {
t.Fatalf("contains failed: %v", err)
} else if !ok {
t.Fatalf("key %v missing", testCase.Key)
}
var value interface{}
if value, ok, err = b.Get(testCase.Key); err != nil {
t.Fatalf("get failed: %v", err)
} else if !ok {
t.Fatalf("key %v missing", testCase.Key)
} else if !reflect.DeepEqual(value, testCase.Value) {
t.Fatalf("corrupt, expected %+v, got %+v", testCase.Value, value)
}
}
if l := b.Len(); l != len(kvTestCases) {
t.Fatalf("corrupt, expected %d, got %d length", len(kvTestCases), l)
}
b.Iter(func(key, value interface{}) {
if !reflect.DeepEqual(value, iterTest[key]) {
t.Fatalf("expected %v, got %v", iterTest[key], value)
}
})
if ok, err = b.Replace("foo", "biz"); err != nil {
t.Fatalf("replace failed: %v", err)
} else if !ok {
t.Fatal("replace failed")
}
if ok, err = b.Replace("missing", "foo"); err != nil {
t.Fatalf("replace failed: %v", err)
} else if ok {
t.Fatal("replace failed, replace of missing entry returned true")
}
if ok, err = b.Delete("foo"); err != nil {
t.Fatalf("delete failed: %v", err)
} else if !ok {
t.Fatal("delete failed")
}
if ok, err = b.Delete("foo"); err != nil {
t.Fatalf("delete failed: %v", err)
} else if ok {
t.Fatal("delete failed, delete of missing entry returned true")
}
}
func TestKV(t *testing.T) {
t.Run("MemoryKV", func(t *testing.T) {
testKV(t, NewMemoryKV())
})
t.Run("DiskKV", func(t *testing.T) {
p := filepath.Join("testdata", "kv")
defer os.RemoveAll(p)
b, err := NewDiskKV(p, 0750)
if err != nil {
t.Fatal(err)
}
testKV(t, b)
})
}
func TestDiskKVError(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("running as root")
}
var (
d *DiskKV
err error
)
// Test creation in privileged path
if d, err = NewDiskKV("/root", 0755); err == nil {
t.Fatal("NewDiskKV in /root should fail")
} else if d != nil {
t.Fatal("NewDiskKV must return nil on error")
} else {
t.Logf("error %v (expected)", err)
}
// Test reusage of privileged path
if d, err = NewDiskKV("/tmp", 0755); err == nil {
t.Fatal("NewDiskKV in /tmp should fail")
} else if d != nil {
t.Fatal("NewDiskKV must return nil on error")
} else {
t.Logf("error %v (expected)", err)
}
// Test creation over existing file
var f *os.File
if f, err = ioutil.TempFile("", "testkv"); err != nil {
t.Fatal(err)
} else {
defer os.Remove(f.Name())
defer f.Close()
if d, err = NewDiskKV(f.Name(), 0755); err == nil {
t.Fatalf("NewDiskKV in %s should fail", f.Name())
} else if d != nil {
t.Fatal("NewDiskKV must return nil on error")
} else {
t.Logf("error %v (expected)", err)
}
}
}

+ 263
- 0
lru.go View File

@ -0,0 +1,263 @@
package cache
import (
"container/list"
"os"
"path/filepath"
"sync"
"time"
)
type mKey struct {
Path string
Key interface{}
Modified time.Time
}
// MemoryLRU implements a concurrent safe in-memory LRU key-value store.
type MemoryLRU struct {
size int
evict *list.List
items map[interface{}]*list.Element
}
func NewMemoryLRU(size uint) *MemoryLRU {
return &MemoryLRU{
size: int(size),
evict: list.New(),
items: make(map[interface{}]*list.Element),
}
}
// Len returns the number of items in the cache.
func (m *MemoryLRU) Len() int {
return m.evict.Len()
}
// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale.
func (m *MemoryLRU) Contains(key interface{}) (ok bool, err error) {
_, ok = m.items[key]
return
}
func (m *MemoryLRU) Delete(key interface{}) (bool, error) {
if entry, ok := m.items[key]; ok {
m.evict.Remove(entry)
delete(m.items, key)
return true, nil
}
return false, nil
}
// Get an item from the cache.
func (m *MemoryLRU) Get(key interface{}) (interface{}, bool, error) {
if entry, ok := m.items[key]; ok {
m.evict.MoveToFront(entry)
return entry.Value.(*kv).Value, true, nil
}
return nil, false, nil
}
// Iter iterates over all key-value pairs, without updating the recent-ness or
// deleting items for being stale.
func (m *MemoryLRU) Iter(fn func(key, value interface{})) error {
for key, entry := range m.items {
fn(key, entry.Value.(*kv).Value)
}
return nil
}
// Set an item in the cache.
func (m *MemoryLRU) Set(key, value interface{}) error {
r, err := m.Replace(key, value)
if r || err != nil {
return err
}
// Add new item
item := &kv{key, value}
entry := m.evict.PushFront(item)
m.items[key] = entry
if m.evict.Len() > m.size {
m.removeOldest()
}
return nil
}
// Replace an item in the cache.
func (m *MemoryLRU) Replace(key, value interface{}) (bool, error) {
// Check for existing item
if entry, ok := m.items[key]; ok {
m.evict.MoveToFront(entry)
entry.Value.(*kv).Value = value
return true, nil
}
return false, nil
}
// removeOldest removes the oldest item from the cache.
func (m *MemoryLRU) removeOldest() {
ent := m.evict.Back()
if ent != nil {
m.removeElement(ent)
}
}
// removeElement is used to remove a given list element from the cache
func (m *MemoryLRU) removeElement(e *list.Element) {
m.evict.Remove(e)
entry := e.Value.(*kv)
delete(m.items, entry.Key)
}
type DiskLRU struct {
kv *DiskKV
size int
evict *list.List
items map[string]*list.Element
mutex sync.RWMutex
}
func NewDiskLRU(path string, perm os.FileMode, size uint) (d *DiskLRU, err error) {
defer func() {
if reason := recover(); reason != nil {
if perr, ok := reason.(error); ok {
err = perr
} else {
panic(reason)
}
}
}()
d = &DiskLRU{
size: int(size),
evict: list.New(),
items: make(map[string]*list.Element),
}
if d.kv, err = NewDiskKV(path, perm); err != nil {
return nil, err
}
if err = filepath.Walk(d.kv.path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
value := new(kv)
if err = d.kv.get(path, value); err != nil {
return err
}
d.insert(&mKey{path, value.Key, info.ModTime()})
return nil
}); err != nil {
return nil, err
}
return d, nil
}
func (d *DiskLRU) Len() int {
return d.evict.Len()
}
// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale.
func (d *DiskLRU) Contains(key interface{}) (ok bool, err error) {
p := d.kv.pathFor(key)
if ok, err = d.kv.contains(p); err != nil {
return
}
if ok {
_, ok = d.items[p]
}
return
}
func (d *DiskLRU) Delete(key interface{}) (bool, error) {
p := d.kv.pathFor(key)
if entry, ok := d.items[p]; ok {
d.evict.Remove(entry)
delete(d.items, p)
return d.kv.delete(p)
}
return false, nil
}
func (d *DiskLRU) Get(key interface{}) (interface{}, bool, error) {
p := d.kv.pathFor(key)
if entry, ok := d.items[p]; ok {
value := new(kv)
if err := d.kv.get(p, value); err != nil {
return nil, false, err
}
d.evict.MoveToFront(entry)
return value.Value, true, nil
}
return nil, false, nil
}
func (d *DiskLRU) Iter(fn func(key, value interface{})) error {
return d.kv.Iter(fn)
}
func (d *DiskLRU) Replace(key, value interface{}) (bool, error) {
return d.kv.Replace(key, value)
}
// Set an item in the cache.
func (d *DiskLRU) Set(key, value interface{}) error {
r, err := d.Replace(key, value)
if r || err != nil {
return err
}
// Add new item
p := d.kv.pathFor(key)
entry := d.evict.PushFront(&mKey{p, key, time.Now()})
d.items[p] = entry
if d.evict.Len() > d.size {
d.removeOldest()
}
return d.kv.set(p, key, value)
}
// insert adds a new item to the cache
func (d *DiskLRU) insert(key *mKey) {
var ent *list.Element
first := d.evict.Front()
if first == nil {
ent = d.evict.PushFront(key)
} else if first.Value.(*mKey).Modified.After(key.Modified) {
ent = d.evict.PushFront(key)
d.evict.MoveAfter(ent, first)
} else {
ent = d.evict.PushFront(key)
}
d.items[key.Path] = ent
if d.evict.Len() > d.size {
d.removeOldest()
}
}
// removeOldest removes the oldest item from the cache.
func (d *DiskLRU) removeOldest() {
ent := d.evict.Back()
if ent != nil {
d.removeElement(ent)
}
}
// removeElement is used to remove a given list element from the cache
func (d *DiskLRU) removeElement(e *list.Element) {
d.evict.Remove(e)
entry := e.Value.(*mKey)
d.kv.delete(entry.Path)
}

+ 118
- 0
lru_test.go View File

@ -0,0 +1,118 @@
package cache
import (
"os"
"path/filepath"
"reflect"
"testing"
)
var lruTestCases = []kv{
{"a", "b"},
{"b", "c"},
{"c", "d"},
{"d", "e"},
{"e", "a"},
}
func testLRU(t *testing.T, c Backend) {
var (
ok bool
err error
)
iterTest := make(map[interface{}]interface{})
for i, testCase := range lruTestCases {
c.Set(testCase.Key, testCase.Value)
iterTest[testCase.Key] = testCase.Value
var value interface{}
if value, ok, err = c.Get(testCase.Key); err != nil {
t.Fatalf("get failed: %v", err)
} else if !ok {
t.Fatalf("key %v missing", testCase.Key)
} else if !reflect.DeepEqual(value, testCase.Value) {
t.Fatalf("corrupt, expected %+v, got %+v", testCase.Value, value)
}
if l := c.Len(); l != (i + 1) {
t.Fatalf("size is %d, expected %d", l, i+1)
}
}
c.Iter(func(key, value interface{}) {
if !reflect.DeepEqual(value, iterTest[key]) {
t.Fatalf("expected %v, got %v", iterTest[key], value)
}
})
// Set already existing entry
c.Set("e", "e")
if ok, err = c.Replace("e", "f"); err != nil {
t.Fatalf("replace failed: %v", err)
} else if !ok {
t.Fatal("key missing")
}
if ok, err = c.Replace("f", "g"); err != nil {
t.Fatalf("replace failed: %v", err)
} else if ok {
t.Fatal("replace of missing entry returned true")
}
if ok, err = c.Contains("e"); err != nil {
t.Fatalf("contains failed: %v", err)
} else if !ok {
t.Fatal("key missing")
}
if ok, err = c.Delete("e"); err != nil {
t.Fatalf("delete failed: %v", err)
} else if !ok {
t.Fatal("delete failed")
}
if ok, err = c.Delete("e"); err != nil {
t.Fatalf("delete failed: %v", err)
} else if ok {
t.Fatal("delete of missing entry returned true")
}
if _, ok, err = c.Get("missing"); err != nil {
t.Fatalf("get failed: %v", err)
} else if ok {
t.Fatal("get returned true for missing key")
}
}
func testLRUOverflow(t *testing.T, c Backend) {
for _, testCase := range lruTestCases {
c.Set(testCase.Key, testCase.Value)
}
if l := c.Len(); l != 3 {
t.Fatalf("size is %d, expected 3", l)
}
}
func TestLRU(t *testing.T) {
t.Run("MemoryLRU", func(t *testing.T) {
testLRU(t, NewMemoryLRU(5))
testLRUOverflow(t, NewMemoryLRU(3))
})
t.Run("DiskLRU", func(t *testing.T) {
p := filepath.Join("testdata", "lru")
d, err := NewDiskLRU(p, 0750, 5)
if err != nil {
t.Fatal(err)
}
testLRU(t, d)
os.RemoveAll(p)
})
}
var (
_ Backend = (*MemoryLRU)(nil)
_ Backend = (*DiskLRU)(nil)
)

Loading…
Cancel
Save