-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adding redis cache provider go-aah/aah#203
- Loading branch information
Showing
3 changed files
with
404 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,173 @@ | ||
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) | ||
// aahframework.org/cache/redis source code and usage is governed by a MIT style | ||
// license that can be found in the LICENSE file. | ||
|
||
package redis // import "aahframework.org/cache/redis" | ||
|
||
import ( | ||
"bytes" | ||
"encoding/gob" | ||
"fmt" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"aahframework.org/aah.v0/cache" | ||
"aahframework.org/config.v0" | ||
"aahframework.org/log.v0" | ||
"github.com/go-redis/redis" | ||
) | ||
|
||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | ||
// Provider and its exported methods | ||
//______________________________________________________________________________ | ||
|
||
// Provider struct represents the Redis cache provider. | ||
type Provider struct { | ||
name string | ||
logger log.Loggerer | ||
cfg *cache.Config | ||
appCfg *config.Config | ||
client *redis.Client | ||
clientOpts *redis.Options | ||
} | ||
|
||
var _ cache.Provider = (*Provider)(nil) | ||
|
||
// Init method initializes the Redis cache provider. | ||
func (p *Provider) Init(providerName string, appCfg *config.Config, logger log.Loggerer) error { | ||
p.name = providerName | ||
p.appCfg = appCfg | ||
p.logger = logger | ||
|
||
if strings.ToLower(p.appCfg.StringDefault("cache."+p.name+".provider", "")) != "redis" { | ||
return fmt.Errorf("aah/cache: not a vaild provider name, expected 'redis'") | ||
} | ||
|
||
p.clientOpts = &redis.Options{ | ||
Addr: p.appCfg.StringDefault("cache."+p.name+".address", ":6379"), | ||
Password: p.appCfg.StringDefault("cache."+p.name+".password", ""), | ||
DB: p.appCfg.IntDefault("cache."+p.name+".db", 0), | ||
} | ||
|
||
p.client = redis.NewClient(p.clientOpts) | ||
if _, err := p.client.Ping().Result(); err != nil { | ||
return fmt.Errorf("aah/cache: %s", err) | ||
} | ||
|
||
gob.Register(entry{}) | ||
p.logger.Infof("Cache provider: %s connected successfully with %s", p.name, p.clientOpts.Addr) | ||
|
||
return nil | ||
} | ||
|
||
// Create method creates new Redis cache with given options. | ||
func (p *Provider) Create(cfg *cache.Config) (cache.Cache, error) { | ||
p.cfg = cfg | ||
r := &redisCache{ | ||
keyPrefix: p.cfg.Name + "-", | ||
p: p, | ||
} | ||
return r, nil | ||
} | ||
|
||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | ||
// redisCache struct implements `cache.Cache` interface. | ||
//______________________________________________________________________________ | ||
|
||
type redisCache struct { | ||
keyPrefix string | ||
p *Provider | ||
} | ||
|
||
var _ cache.Cache = (*redisCache)(nil) | ||
|
||
// Name method returns the cache store name. | ||
func (r *redisCache) Name() string { | ||
return r.p.cfg.Name | ||
} | ||
|
||
// Get method returns the cached entry for given key if it exists otherwise nil. | ||
// Method uses `gob.Decoder` to unmarshal cache value from bytes. | ||
func (r *redisCache) Get(k string) interface{} { | ||
k = r.keyPrefix + k | ||
v, err := r.p.client.Get(k).Bytes() | ||
if err != nil { | ||
return nil | ||
} | ||
|
||
var e entry | ||
err = gob.NewDecoder(bytes.NewBuffer(v)).Decode(&e) | ||
if err != nil { | ||
return nil | ||
} | ||
if r.p.cfg.EvictionMode == cache.EvictionModeSlide { | ||
_ = r.p.client.Expire(k, e.D) | ||
} | ||
|
||
return e.V | ||
} | ||
|
||
// GetOrPut method returns the cached entry for the given key if it exists otherwise | ||
// it puts the new entry into cache store and returns the value. | ||
func (r *redisCache) GetOrPut(k string, v interface{}, d time.Duration) interface{} { | ||
ev := r.Get(k) | ||
if ev == nil { | ||
_ = r.Put(k, v, d) | ||
return v | ||
} | ||
return ev | ||
} | ||
|
||
// Put method adds the cache entry with specified expiration. Returns error | ||
// if cache entry exists. Method uses `gob.Encoder` to marshal cache value into bytes. | ||
func (r *redisCache) Put(k string, v interface{}, d time.Duration) error { | ||
e := entry{D: d, V: v} | ||
buf := acquireBuffer() | ||
enc := gob.NewEncoder(buf) | ||
if err := enc.Encode(e); err != nil { | ||
return fmt.Errorf("aah/cache: %v", err) | ||
} | ||
|
||
cmd := r.p.client.Set(r.keyPrefix+k, buf.Bytes(), d) | ||
releaseBuffer(buf) | ||
return cmd.Err() | ||
} | ||
|
||
// Delete method deletes the cache entry from cache store. | ||
func (r *redisCache) Delete(k string) { | ||
r.p.client.Del(r.keyPrefix + k) | ||
} | ||
|
||
// Exists method checks given key exists in cache store and its not expried. | ||
func (r *redisCache) Exists(k string) bool { | ||
result, err := r.p.client.Exists(r.keyPrefix + k).Result() | ||
return err == nil && result == 1 | ||
} | ||
|
||
// Flush methods flushes(deletes) all the cache entries from cache. | ||
func (r *redisCache) Flush() { | ||
r.p.client.FlushDB() | ||
} | ||
|
||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | ||
// Helper methods | ||
//______________________________________________________________________________ | ||
|
||
type entry struct { | ||
D time.Duration | ||
V interface{} | ||
} | ||
|
||
var bufPool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} | ||
|
||
func acquireBuffer() *bytes.Buffer { | ||
return bufPool.Get().(*bytes.Buffer) | ||
} | ||
|
||
func releaseBuffer(b *bytes.Buffer) { | ||
if b != nil { | ||
b.Reset() | ||
bufPool.Put(b) | ||
} | ||
} |
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,223 @@ | ||
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) | ||
// aahframework.org/cache/redis source code and usage is governed by a MIT style | ||
// license that can be found in the LICENSE file. | ||
|
||
package redis | ||
|
||
import ( | ||
"encoding/gob" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"testing" | ||
"time" | ||
|
||
"aahframework.org/aah.v0/cache" | ||
"aahframework.org/config.v0" | ||
"aahframework.org/log.v0" | ||
"aahframework.org/test.v0/assert" | ||
) | ||
|
||
func TestRedisCache(t *testing.T) { | ||
mgr := createCacheMgr(t, "redis1", ` | ||
cache { | ||
redis1 { | ||
provider = "redis" | ||
address = "localhost:6379" | ||
} | ||
static { | ||
} | ||
} | ||
`) | ||
|
||
e := mgr.CreateCache(&cache.Config{Name: "cache1", ProviderName: "redis1"}) | ||
assert.FailNowOnError(t, e, "unable to create cache") | ||
c := mgr.Cache("cache1") | ||
|
||
type sample struct { | ||
Name string | ||
Present bool | ||
Value string | ||
} | ||
|
||
testcases := []struct { | ||
label string | ||
key string | ||
value interface{} | ||
}{ | ||
{ | ||
label: "Redis Cache integer", | ||
key: "key1", | ||
value: 342348347, | ||
}, | ||
{ | ||
label: "Redis Cache float", | ||
key: "key2", | ||
value: 0.78346374, | ||
}, | ||
{ | ||
label: "Redis Cache string", | ||
key: "key3", | ||
value: "This is mt cache string", | ||
}, | ||
{ | ||
label: "Redis Cache map", | ||
key: "key4", | ||
value: map[string]interface{}{"key1": 343434, "key2": "kjdhdsjkdhjs", "key3": 87235.3465}, | ||
}, | ||
{ | ||
label: "Redis Cache struct", | ||
key: "key5", | ||
value: sample{Name: "Jeeva", Present: true, Value: "redis cache provider"}, | ||
}, | ||
} | ||
|
||
err := c.Put("pre-test-key1", sample{Name: "Jeeva", Present: true, Value: "redis cache provider"}, 3*time.Second) | ||
assert.Equal(t, errors.New("aah/cache: gob: type not registered for interface: redis.sample"), err) | ||
|
||
gob.Register(map[string]interface{}{}) | ||
gob.Register(sample{}) | ||
|
||
for _, tc := range testcases { | ||
t.Run(tc.label, func(t *testing.T) { | ||
assert.False(t, c.Exists(tc.key)) | ||
assert.Nil(t, c.Get(tc.key)) | ||
|
||
err := c.Put(tc.key, tc.value, 3*time.Second) | ||
assert.Nil(t, err) | ||
|
||
v := c.Get(tc.key) | ||
assert.Equal(t, tc.value, v) | ||
|
||
c.Delete(tc.key) | ||
v = c.GetOrPut(tc.key, tc.value, 3*time.Second) | ||
assert.Equal(t, tc.value, v) | ||
}) | ||
} | ||
|
||
c.Flush() | ||
} | ||
|
||
func TestRedisCacheAddAndGet(t *testing.T) { | ||
c := createTestCache(t, "redis1", ` | ||
cache { | ||
redis1 { | ||
provider = "redis" | ||
address = "localhost:6379" | ||
} | ||
} | ||
`, &cache.Config{Name: "addgetcache", ProviderName: "redis1"}) | ||
|
||
for i := 0; i < 20; i++ { | ||
c.Put(fmt.Sprintf("key_%v", i), i, 3*time.Second) | ||
} | ||
|
||
for i := 5; i < 10; i++ { | ||
v := c.Get(fmt.Sprintf("key_%v", i)) | ||
assert.Equal(t, i, v) | ||
} | ||
assert.Equal(t, "addgetcache", c.Name()) | ||
} | ||
|
||
func TestRedisMultipleCache(t *testing.T) { | ||
mgr := createCacheMgr(t, "redis1", ` | ||
cache { | ||
redis1 { | ||
provider = "redis" | ||
address = "localhost:6379" | ||
} | ||
} | ||
`) | ||
|
||
names := []string{"testcache1", "testcache2", "testcache3"} | ||
for _, name := range names { | ||
err := mgr.CreateCache(&cache.Config{Name: name, ProviderName: "redis1"}) | ||
assert.FailNowOnError(t, err, "unable to create cache") | ||
|
||
c := mgr.Cache(name) | ||
assert.NotNil(t, c) | ||
assert.Equal(t, name, c.Name()) | ||
|
||
for i := 0; i < 20; i++ { | ||
c.Put(fmt.Sprintf("key_%v", i), i, 3*time.Second) | ||
} | ||
|
||
for i := 5; i < 10; i++ { | ||
v := c.Get(fmt.Sprintf("key_%v", i)) | ||
assert.Equal(t, i, v) | ||
} | ||
c.Flush() | ||
} | ||
} | ||
|
||
func TestRedisSlideEvictionMode(t *testing.T) { | ||
c := createTestCache(t, "redis1", ` | ||
cache { | ||
redis1 { | ||
provider = "redis" | ||
address = "localhost:6379" | ||
} | ||
} | ||
`, &cache.Config{Name: "addgetcache", ProviderName: "redis1", EvictionMode: cache.EvictionModeSlide}) | ||
|
||
for i := 0; i < 20; i++ { | ||
c.Put(fmt.Sprintf("key_%v", i), i, 3*time.Second) | ||
} | ||
|
||
for i := 5; i < 10; i++ { | ||
v := c.GetOrPut(fmt.Sprintf("key_%v", i), i, 3*time.Second) | ||
assert.Equal(t, i, v) | ||
} | ||
|
||
assert.Equal(t, "addgetcache", c.Name()) | ||
} | ||
|
||
func TestRedisInvalidProviderName(t *testing.T) { | ||
mgr := cache.NewManager() | ||
mgr.AddProvider("redis1", new(Provider)) | ||
|
||
cfg, _ := config.ParseString(`cache { | ||
redis1 { | ||
provider = "myredis" | ||
address = "localhost:6379" | ||
} | ||
}`) | ||
l, _ := log.New(config.NewEmpty()) | ||
err := mgr.InitProviders(cfg, l) | ||
assert.Equal(t, errors.New("aah/cache: not a vaild provider name, expected 'redis'"), err) | ||
} | ||
|
||
func TestRedisInvalidAddress(t *testing.T) { | ||
mgr := cache.NewManager() | ||
mgr.AddProvider("redis1", new(Provider)) | ||
|
||
cfg, _ := config.ParseString(`cache { | ||
redis1 { | ||
provider = "redis" | ||
address = "localhost:637967" | ||
} | ||
}`) | ||
l, _ := log.New(config.NewEmpty()) | ||
err := mgr.InitProviders(cfg, l) | ||
assert.Equal(t, errors.New("aah/cache: dial tcp: address 637967: invalid port"), err) | ||
} | ||
|
||
func createCacheMgr(t *testing.T, name, appCfgStr string) *cache.Manager { | ||
mgr := cache.NewManager() | ||
mgr.AddProvider(name, new(Provider)) | ||
|
||
cfg, _ := config.ParseString(appCfgStr) | ||
l, _ := log.New(config.NewEmpty()) | ||
l.SetWriter(ioutil.Discard) | ||
err := mgr.InitProviders(cfg, l) | ||
assert.FailNowOnError(t, err, "unexpected") | ||
return mgr | ||
} | ||
|
||
func createTestCache(t *testing.T, name, appCfgStr string, cacheCfg *cache.Config) cache.Cache { | ||
mgr := createCacheMgr(t, name, appCfgStr) | ||
e := mgr.CreateCache(cacheCfg) | ||
assert.FailNowOnError(t, e, "unable to create cache") | ||
return mgr.Cache(cacheCfg.Name) | ||
} |
Oops, something went wrong.