-
-
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
6 changed files
with
278 additions
and
11 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
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
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
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
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,129 @@ | ||
package lcw | ||
|
||
import ( | ||
"sync/atomic" | ||
"time" | ||
|
||
redis "github.com/go-redis/redis/v7" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// RedisSizeLimit is maximum allowed value size in Redis | ||
const RedisSizeLimit = 512 * 1024 * 1024 | ||
|
||
// RedisCache implements LoadingCache for Redis. | ||
type RedisCache struct { | ||
options | ||
CacheStat | ||
backend *redis.Client | ||
} | ||
|
||
// NewRedisCache makes Redis LoadingCache implementation. | ||
func NewRedisCache(backend *redis.Client, opts ...Option) (*RedisCache, error) { | ||
|
||
res := RedisCache{ | ||
options: options{ | ||
ttl: 5 * time.Minute, | ||
}, | ||
} | ||
for _, opt := range opts { | ||
if err := opt(&res.options); err != nil { | ||
return nil, errors.Wrap(err, "failed to set cache option") | ||
} | ||
} | ||
|
||
res.backend = backend | ||
|
||
return &res, nil | ||
} | ||
|
||
// Get gets value by key or load with fn if not found in cache | ||
func (c *RedisCache) Get(key string, fn func() (Value, error)) (data Value, err error) { | ||
|
||
v, getErr := c.backend.Get(key).Result() | ||
switch getErr { | ||
case nil: | ||
atomic.AddInt64(&c.Hits, 1) | ||
return v, nil | ||
case redis.Nil: | ||
if data, err = fn(); err != nil { | ||
atomic.AddInt64(&c.Errors, 1) | ||
return data, err | ||
} | ||
default: | ||
atomic.AddInt64(&c.Errors, 1) | ||
return v, getErr | ||
} | ||
atomic.AddInt64(&c.Misses, 1) | ||
|
||
if c.allowed(key, data) { | ||
c.backend.Set(key, data, c.ttl) | ||
} | ||
return data, nil | ||
} | ||
|
||
// Invalidate removes keys with passed predicate fn, i.e. fn(key) should be true to get evicted | ||
func (c *RedisCache) Invalidate(fn func(key string) bool) { | ||
for _, key := range c.backend.Keys("*").Val() { // Keys() returns copy of cache's key, safe to remove directly | ||
if fn(key) { | ||
c.backend.Del(key) | ||
} | ||
} | ||
} | ||
|
||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key. | ||
func (c *RedisCache) Peek(key string) (Value, bool) { | ||
ret, err := c.backend.Get(key).Result() | ||
if err != nil { | ||
return nil, false | ||
} | ||
return ret, true | ||
} | ||
|
||
// Purge clears the cache completely. | ||
func (c *RedisCache) Purge() { | ||
c.backend.FlushDB() | ||
|
||
} | ||
|
||
// Delete cache item by key | ||
func (c *RedisCache) Delete(key string) { | ||
c.backend.Del(key) | ||
} | ||
|
||
// Stat returns cache statistics | ||
func (c *RedisCache) Stat() CacheStat { | ||
return CacheStat{ | ||
Hits: c.Hits, | ||
Misses: c.Misses, | ||
Size: c.size(), | ||
Keys: c.keys(), | ||
Errors: c.Errors, | ||
} | ||
} | ||
|
||
func (c *RedisCache) size() int64 { | ||
return 0 | ||
} | ||
|
||
func (c *RedisCache) keys() int { | ||
return int(c.backend.DBSize().Val()) | ||
} | ||
|
||
func (c *RedisCache) allowed(key string, data Value) bool { | ||
if c.backend.DBSize().Val() >= int64(c.maxKeys) { | ||
return false | ||
} | ||
if c.maxKeySize > 0 && len(key) > c.maxKeySize { | ||
return false | ||
} | ||
if s, ok := data.(Sizer); ok { | ||
if c.maxValueSize > 0 && (s.Size() >= c.maxValueSize || s.Size() >= RedisSizeLimit) { | ||
return false | ||
} | ||
if c.maxValueSize <= 0 && s.Size() >= RedisSizeLimit { | ||
return false | ||
} | ||
} | ||
return true | ||
} |
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,132 @@ | ||
package lcw | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"sync/atomic" | ||
"testing" | ||
"time" | ||
|
||
"github.com/alicebob/miniredis" | ||
redis "github.com/go-redis/redis/v7" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// newTestRedis returns a redis.Cmdable. | ||
func newTestRedisServer() *miniredis.Miniredis { | ||
mr, err := miniredis.Run() | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return mr | ||
} | ||
|
||
func TestExpirableRedisCache(t *testing.T) { | ||
server := newTestRedisServer() | ||
client := redis.NewClient(&redis.Options{ | ||
Addr: server.Addr()}) | ||
lc, err := NewRedisCache(client, MaxKeys(5), TTL(time.Second*6)) | ||
if err != nil { | ||
log.Fatalf("can't make redis cache, %v", err) | ||
} | ||
log.Printf("TTL: %s", lc.ttl) | ||
require.NoError(t, err) | ||
for i := 0; i < 5; i++ { | ||
_, e := lc.Get(fmt.Sprintf("key-%d", i), func() (Value, error) { | ||
return fmt.Sprintf("result-%d", i), nil | ||
}) | ||
assert.NoError(t, e) | ||
server.FastForward(1000 * time.Millisecond) | ||
} | ||
|
||
assert.Equal(t, 5, lc.Stat().Keys) | ||
assert.Equal(t, int64(5), lc.Stat().Misses) | ||
|
||
_, e := lc.Get("key-xx", func() (Value, error) { | ||
return "result-xx", nil | ||
}) | ||
assert.NoError(t, e) | ||
assert.Equal(t, 5, lc.Stat().Keys) | ||
assert.Equal(t, int64(6), lc.Stat().Misses) | ||
|
||
server.FastForward(1000 * time.Millisecond) | ||
assert.Equal(t, 4, lc.Stat().Keys) | ||
|
||
server.FastForward(4000 * time.Millisecond) | ||
assert.Equal(t, 0, lc.keys()) | ||
|
||
} | ||
|
||
func TestRedisCache(t *testing.T) { | ||
var coldCalls int32 | ||
|
||
server := newTestRedisServer() | ||
client := redis.NewClient(&redis.Options{ | ||
Addr: server.Addr()}) | ||
lc, err := NewRedisCache(client, MaxKeys(5), MaxValSize(10)) | ||
if err != nil { | ||
log.Fatalf("can't make redis cache, %v", err) | ||
} | ||
// put 5 keys to cache | ||
for i := 0; i < 5; i++ { | ||
res, e := lc.Get(fmt.Sprintf("key-%d", i), func() (Value, error) { | ||
atomic.AddInt32(&coldCalls, 1) | ||
return fmt.Sprintf("result-%d", i), nil | ||
}) | ||
assert.Nil(t, e) | ||
assert.Equal(t, fmt.Sprintf("result-%d", i), res.(string)) | ||
assert.Equal(t, int32(i+1), atomic.LoadInt32(&coldCalls)) | ||
} | ||
|
||
// check if really cached | ||
res, err := lc.Get("key-3", func() (Value, error) { | ||
return "result-blah", nil | ||
}) | ||
assert.Nil(t, err) | ||
assert.Equal(t, "result-3", res.(string), "should be cached") | ||
|
||
// try to cache after maxKeys reached | ||
res, err = lc.Get("key-X", func() (Value, error) { | ||
return "result-X", nil | ||
}) | ||
assert.Nil(t, err) | ||
assert.Equal(t, "result-X", res.(string)) | ||
assert.Equal(t, int64(5), lc.backend.DBSize().Val()) | ||
|
||
// put to cache and make sure it cached | ||
res, err = lc.Get("key-Z", func() (Value, error) { | ||
return "result-Z", nil | ||
}) | ||
assert.Nil(t, err) | ||
assert.Equal(t, "result-Z", res.(string)) | ||
|
||
res, err = lc.Get("key-Z", func() (Value, error) { | ||
return "result-Zzzz", nil | ||
}) | ||
assert.Nil(t, err) | ||
assert.Equal(t, "result-Zzzz", res.(string), "got non-cached value") | ||
assert.Equal(t, 5, lc.keys()) | ||
} | ||
|
||
func TestRedisCache_BadOptions(t *testing.T) { | ||
server := newTestRedisServer() | ||
client := redis.NewClient(&redis.Options{ | ||
Addr: server.Addr()}) | ||
|
||
_, err := NewRedisCache(client, MaxCacheSize(-1)) | ||
assert.EqualError(t, err, "failed to set cache option: negative max cache size") | ||
|
||
_, err = NewRedisCache(client, MaxCacheSize(-1)) | ||
assert.EqualError(t, err, "failed to set cache option: negative max cache size") | ||
|
||
_, err = NewRedisCache(client, MaxKeys(-1)) | ||
assert.EqualError(t, err, "failed to set cache option: negative max keys") | ||
|
||
_, err = NewRedisCache(client, MaxValSize(-1)) | ||
assert.EqualError(t, err, "failed to set cache option: negative max value size") | ||
|
||
_, err = NewRedisCache(client, TTL(-1)) | ||
assert.EqualError(t, err, "failed to set cache option: negative ttl") | ||
} |