Skip to content

Commit

Permalink
adding redis cache provider go-aah/aah#203
Browse files Browse the repository at this point in the history
  • Loading branch information
jeevatkm committed Aug 9, 2018
1 parent 9424c3b commit b4b1a82
Show file tree
Hide file tree
Showing 3 changed files with 404 additions and 0 deletions.
173 changes: 173 additions & 0 deletions redis.go
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)
}
}
223 changes: 223 additions & 0 deletions redis_test.go
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)
}
Loading

0 comments on commit b4b1a82

Please sign in to comment.