Skip to content

Commit

Permalink
add scache wrapper #9
Browse files Browse the repository at this point in the history
  • Loading branch information
umputun committed Nov 11, 2019
1 parent a9cfcb7 commit d984f56
Show file tree
Hide file tree
Showing 2 changed files with 279 additions and 0 deletions.
141 changes: 141 additions & 0 deletions scache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package lcw

import (
"strings"

"github.com/pkg/errors"
)

// Scache wraps LoadingCache with partitions (sub-system), and scopes.
// Simplified interface with just 3 funcs - Get, Flush and Stats
type Scache struct {
lc LoadingCache
}

// NewScache creates Scache on top of LoadingCache
func NewScache(lc LoadingCache) *Scache {
return &Scache{lc: lc}
}

func (m *Scache) Get(key Key, fn func() ([]byte, error)) (data []byte, err error) {
keyStr := key.String()
val, err := m.lc.Get(keyStr, func() (value Value, e error) {
return fn()
})
return val.([]byte), err
}

// Stat delegates the call to the underlying cache backend
func (m *Scache) Stat() CacheStat {
return m.lc.Stat()
}

// Flush clears cache and calls postFlushFn async
func (m *Scache) Flush(req FlusherRequest) {

if len(req.scopes) == 0 {
m.lc.Purge()
return
}

// check if fullKey has matching scopes
inScope := func(fullKey string) bool {
key, err := parseKey(fullKey)
if err != nil {
return false
}
for _, s := range req.scopes {
for _, ks := range key.scopes {
if ks == s {
return true
}
}
}
return false
}

for _, k := range m.lc.Keys() {
if inScope(k) {
m.lc.Delete(k) // Keys() returns copy of cache's key, safe to remove directly
}
}
}

// Key for scoped cache. Created foe given partition (can be empty) and set with ID and Scopes.
// example: k := NewKey("sys1").ID(postID).Scopes("last_posts", customer_id)
type Key struct {
id string // the primary part of the key, i.e. usual cache's key
partition string // optional id for a subsystem or cache partition
scopes []string // list of scopes to use in invalidation
}

// NewKey makes base key for given partition. Partition can be omitted.
func NewKey(partition ...string) Key {
if len(partition) == 0 {
return Key{partition: ""}
}
return Key{partition: partition[0]}
}

// ID sets key id
func (k Key) ID(id string) Key {
k.id = id
return k
}

// Scopes of the key
func (k Key) Scopes(scopes ...string) Key {
k.scopes = scopes
return k
}

// String makes full string key from primary key, partition and scopes
// key string made as <partition>@@<id>@@<scope1>$$<scope2>....
func (k Key) String() string {
bld := strings.Builder{}
bld.WriteString(k.partition)
bld.WriteString("@@")
bld.WriteString(k.id)
bld.WriteString("@@")
bld.WriteString(strings.Join(k.scopes, "$$"))
return bld.String()
}

// parseKey gets compound key string created by Key func and split it to the actual key, partition and scopes
// key string made as <partition>@@<id>@@<scope1>$$<scope2>....
func parseKey(keyStr string) (Key, error) {
elems := strings.Split(keyStr, "@@")
if len(elems) != 3 {
return Key{}, errors.Errorf("can't parse cache key %s, invalid number of segments %d", keyStr, len(elems))
}

scopes := strings.Split(elems[2], "$$")
if len(scopes) == 1 && scopes[0] == "" {
scopes = []string{}
}
key := Key{
partition: elems[0],
id: elems[1],
scopes: scopes,
}

return key, nil
}

// FlusherRequest used as input for cache.Flush
type FlusherRequest struct {
partition string
scopes []string
}

// Flusher makes new FlusherRequest with empty scopes
func Flusher(partition string) FlusherRequest {
res := FlusherRequest{partition: partition}
return res
}

// Scopes adds scopes to FlusherRequest
func (f FlusherRequest) Scopes(scopes ...string) FlusherRequest {
f.scopes = scopes
return f
}
138 changes: 138 additions & 0 deletions scache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package lcw

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCache_Scopes(t *testing.T) {
lru, err := NewLruCache()
require.NoError(t, err)
lc := NewScache(lru)

res, err := lc.Get(NewKey("site").ID("key").Scopes("s1", "s2"), func() ([]byte, error) {
return []byte("value"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value", string(res))

res, err = lc.Get(NewKey("site").ID("key2").Scopes("s2"), func() ([]byte, error) {
return []byte("value2"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value2", string(res))

assert.Equal(t, 2, len(lc.lc.Keys()))
lc.Flush(Flusher("site").Scopes("s1"))
assert.Equal(t, 1, len(lc.lc.Keys()))

_, err = lc.Get(NewKey("site").ID("key2").Scopes("s2"), func() ([]byte, error) {
assert.Fail(t, "should stay")
return nil, nil
})
assert.Nil(t, err)
res, err = lc.Get(NewKey("site").ID("key").Scopes("s1", "s2"), func() ([]byte, error) {
return []byte("value-upd"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value-upd", string(res), "was deleted, update")
}

func TestCache_Flush(t *testing.T) {
lru, err := NewLruCache()
require.NoError(t, err)
lc := NewScache(lru)

addToCache := func(id string, scopes ...string) {
res, err := lc.Get(NewKey("site").ID(id).Scopes(scopes...), func() ([]byte, error) {
return []byte("value" + id), nil
})
require.Nil(t, err)
require.Equal(t, "value"+id, string(res))
}

init := func() {
lc.Flush(Flusher("site"))
addToCache("key1", "s1", "s2")
addToCache("key2", "s1", "s2", "s3")
addToCache("key3", "s1", "s2", "s3")
addToCache("key4", "s2", "s3")
addToCache("key5", "s2")
addToCache("key6")
addToCache("key7", "s4", "s3")
require.Equal(t, 7, len(lc.lc.Keys()), "cache init")
}

tbl := []struct {
scopes []string
left int
msg string
}{
{[]string{}, 0, "full flush, no scopes"},
{[]string{"s0"}, 7, "flush wrong scope"},
{[]string{"s1"}, 4, "flush s1 scope"},
{[]string{"s2", "s1"}, 2, "flush s2+s1 scope"},
{[]string{"s1", "s2"}, 2, "flush s1+s2 scope"},
{[]string{"s1", "s2", "s4"}, 1, "flush s1+s2+s4 scope"},
{[]string{"s1", "s2", "s3"}, 1, "flush s1+s2+s3 scope"},
{[]string{"s1", "s2", "ss"}, 2, "flush s1+s2+wrong scope"},
}

for i, tt := range tbl {
t.Run(tt.msg, func(t *testing.T) {
init()
lc.Flush(Flusher("site").Scopes(tt.scopes...))
assert.Equal(t, tt.left, len(lc.lc.Keys()), "keys size, %s #%d", tt.msg, i)
})
}
}

func TestCache_FlushFailed(t *testing.T) {
lru, err := NewLruCache()
require.NoError(t, err)
lc := NewScache(lru)

val, err := lc.Get(NewKey("site").ID("invalid-composite"), func() ([]byte, error) {
return []byte("value"), nil
})
assert.Nil(t, err)
assert.Equal(t, "value", string(val))
assert.Equal(t, 1, len(lc.lc.Keys()))

lc.Flush(Flusher("site").Scopes("invalid-composite"))
assert.Equal(t, 1, len(lc.lc.Keys()))
}

func TestScope_Key(t *testing.T) {
tbl := []struct {
key string
partition string
scopes []string
full string
}{
{"key1", "p1", []string{"s1"}, "p1@@key1@@s1"},
{"key2", "p2", []string{"s11", "s2"}, "p2@@key2@@s11$$s2"},
{"key3", "", []string{}, "@@key3@@"},
{"key3", "", []string{"xx", "yyy"}, "@@key3@@xx$$yyy"},
}

for _, tt := range tbl {
t.Run(tt.full, func(t *testing.T) {
k := NewKey(tt.partition).ID(tt.key).Scopes(tt.scopes...)
assert.Equal(t, tt.full, k.String())
k, err := parseKey(tt.full)
require.NoError(t, err)
assert.Equal(t, tt.partition, k.partition)
assert.Equal(t, tt.key, k.id)
assert.Equal(t, tt.scopes, k.scopes)
})
}

// parse invalid key strings
_, err := parseKey("abc")
assert.Error(t, err)
_, err = parseKey("")
assert.Error(t, err)
}

0 comments on commit d984f56

Please sign in to comment.