-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |