Skip to content

Commit

Permalink
Merge db684dc into 51598f3
Browse files Browse the repository at this point in the history
  • Loading branch information
tyzhnenko committed Oct 14, 2019
2 parents 51598f3 + db684dc commit 6d3e214
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 11 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@

The library adds a thin layer on top of [lru cache](https://github.com/hashicorp/golang-lru) and [patrickmn/go-cache](https://github.com/patrickmn/go-cache).

| Cache name | Constructor | Defaults | Description |
| -------------- | --------------------- | ----------------- | --------------------- |
| LruCache | lcw.NewLruCache | keys=1000 | LRU cache with limits |
| ExpirableCache | lcw.NewExpirableCache | keys=1000, ttl=5m | TTL cache with limits |
| Nop | lcw.NewNopCache | | Do-nothing cache |
| Cache name | Constructor | Defaults | Description |
| -------------- | --------------------- | ----------------- | ----------------------- |
| LruCache | lcw.NewLruCache | keys=1000 | LRU cache with limits |
| ExpirableCache | lcw.NewExpirableCache | keys=1000, ttl=5m | TTL cache with limits |
| RedisCache | lcw.NewRedisCache | ttl=5m | Redis cache with limits |
| Nop | lcw.NewNopCache | | Do-nothing cache |


Main features:

- LoadingCache (guava style)
- Limit maximum cache size (in bytes)
- Limit maximum key size
- Limit maximum size of a value
- Limit maximum size of a value
- Limit number of keys
- TTL support (`ExpirableCache` only)
- Callback on eviction event
- TTL support (`ExpirableCache` and `RedisCache`)
- Callback on eviction event (not supported in `RedisCache`)
- Functional style invalidation
- Functional options
- Sane defaults

## Install and update

`go get -u github.com/go-pkgz/lcw`
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
module github.com/go-pkgz/lcw

require (
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-redis/redis/v7 v7.0.0-beta.4
github.com/hashicorp/golang-lru v0.5.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.8.1
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-redis/redis/v7 v7.0.0-beta.4 h1:p6z7Pde69EGRWvlC++y8aFcaWegyrKHzOBGo0zUACTQ=
github.com/go-redis/redis/v7 v7.0.0-beta.4/go.mod h1:xhhSbUMTsleRPur+Vgx9sUHtyN33bdjxY+9/0n9Ig8s=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
Expand Down
2 changes: 1 addition & 1 deletion lru_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/pkg/errors"
)

// LruCache wraps lru.LruCache with laoding cache Get and size limits
// LruCache wraps lru.LruCache with loading cache Get and size limits
type LruCache struct {
options
CacheStat
Expand Down
129 changes: 129 additions & 0 deletions redis_cache.go
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
}
132 changes: 132 additions & 0 deletions redis_cache_test.go
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")
}

0 comments on commit 6d3e214

Please sign in to comment.