Skip to content

Commit b4b1a82

Browse files
committed
adding redis cache provider go-aah/aah#203
1 parent 9424c3b commit b4b1a82

File tree

3 files changed

+404
-0
lines changed

3 files changed

+404
-0
lines changed

redis.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
2+
// aahframework.org/cache/redis source code and usage is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package redis // import "aahframework.org/cache/redis"
6+
7+
import (
8+
"bytes"
9+
"encoding/gob"
10+
"fmt"
11+
"strings"
12+
"sync"
13+
"time"
14+
15+
"aahframework.org/aah.v0/cache"
16+
"aahframework.org/config.v0"
17+
"aahframework.org/log.v0"
18+
"github.com/go-redis/redis"
19+
)
20+
21+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
22+
// Provider and its exported methods
23+
//______________________________________________________________________________
24+
25+
// Provider struct represents the Redis cache provider.
26+
type Provider struct {
27+
name string
28+
logger log.Loggerer
29+
cfg *cache.Config
30+
appCfg *config.Config
31+
client *redis.Client
32+
clientOpts *redis.Options
33+
}
34+
35+
var _ cache.Provider = (*Provider)(nil)
36+
37+
// Init method initializes the Redis cache provider.
38+
func (p *Provider) Init(providerName string, appCfg *config.Config, logger log.Loggerer) error {
39+
p.name = providerName
40+
p.appCfg = appCfg
41+
p.logger = logger
42+
43+
if strings.ToLower(p.appCfg.StringDefault("cache."+p.name+".provider", "")) != "redis" {
44+
return fmt.Errorf("aah/cache: not a vaild provider name, expected 'redis'")
45+
}
46+
47+
p.clientOpts = &redis.Options{
48+
Addr: p.appCfg.StringDefault("cache."+p.name+".address", ":6379"),
49+
Password: p.appCfg.StringDefault("cache."+p.name+".password", ""),
50+
DB: p.appCfg.IntDefault("cache."+p.name+".db", 0),
51+
}
52+
53+
p.client = redis.NewClient(p.clientOpts)
54+
if _, err := p.client.Ping().Result(); err != nil {
55+
return fmt.Errorf("aah/cache: %s", err)
56+
}
57+
58+
gob.Register(entry{})
59+
p.logger.Infof("Cache provider: %s connected successfully with %s", p.name, p.clientOpts.Addr)
60+
61+
return nil
62+
}
63+
64+
// Create method creates new Redis cache with given options.
65+
func (p *Provider) Create(cfg *cache.Config) (cache.Cache, error) {
66+
p.cfg = cfg
67+
r := &redisCache{
68+
keyPrefix: p.cfg.Name + "-",
69+
p: p,
70+
}
71+
return r, nil
72+
}
73+
74+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
75+
// redisCache struct implements `cache.Cache` interface.
76+
//______________________________________________________________________________
77+
78+
type redisCache struct {
79+
keyPrefix string
80+
p *Provider
81+
}
82+
83+
var _ cache.Cache = (*redisCache)(nil)
84+
85+
// Name method returns the cache store name.
86+
func (r *redisCache) Name() string {
87+
return r.p.cfg.Name
88+
}
89+
90+
// Get method returns the cached entry for given key if it exists otherwise nil.
91+
// Method uses `gob.Decoder` to unmarshal cache value from bytes.
92+
func (r *redisCache) Get(k string) interface{} {
93+
k = r.keyPrefix + k
94+
v, err := r.p.client.Get(k).Bytes()
95+
if err != nil {
96+
return nil
97+
}
98+
99+
var e entry
100+
err = gob.NewDecoder(bytes.NewBuffer(v)).Decode(&e)
101+
if err != nil {
102+
return nil
103+
}
104+
if r.p.cfg.EvictionMode == cache.EvictionModeSlide {
105+
_ = r.p.client.Expire(k, e.D)
106+
}
107+
108+
return e.V
109+
}
110+
111+
// GetOrPut method returns the cached entry for the given key if it exists otherwise
112+
// it puts the new entry into cache store and returns the value.
113+
func (r *redisCache) GetOrPut(k string, v interface{}, d time.Duration) interface{} {
114+
ev := r.Get(k)
115+
if ev == nil {
116+
_ = r.Put(k, v, d)
117+
return v
118+
}
119+
return ev
120+
}
121+
122+
// Put method adds the cache entry with specified expiration. Returns error
123+
// if cache entry exists. Method uses `gob.Encoder` to marshal cache value into bytes.
124+
func (r *redisCache) Put(k string, v interface{}, d time.Duration) error {
125+
e := entry{D: d, V: v}
126+
buf := acquireBuffer()
127+
enc := gob.NewEncoder(buf)
128+
if err := enc.Encode(e); err != nil {
129+
return fmt.Errorf("aah/cache: %v", err)
130+
}
131+
132+
cmd := r.p.client.Set(r.keyPrefix+k, buf.Bytes(), d)
133+
releaseBuffer(buf)
134+
return cmd.Err()
135+
}
136+
137+
// Delete method deletes the cache entry from cache store.
138+
func (r *redisCache) Delete(k string) {
139+
r.p.client.Del(r.keyPrefix + k)
140+
}
141+
142+
// Exists method checks given key exists in cache store and its not expried.
143+
func (r *redisCache) Exists(k string) bool {
144+
result, err := r.p.client.Exists(r.keyPrefix + k).Result()
145+
return err == nil && result == 1
146+
}
147+
148+
// Flush methods flushes(deletes) all the cache entries from cache.
149+
func (r *redisCache) Flush() {
150+
r.p.client.FlushDB()
151+
}
152+
153+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
154+
// Helper methods
155+
//______________________________________________________________________________
156+
157+
type entry struct {
158+
D time.Duration
159+
V interface{}
160+
}
161+
162+
var bufPool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
163+
164+
func acquireBuffer() *bytes.Buffer {
165+
return bufPool.Get().(*bytes.Buffer)
166+
}
167+
168+
func releaseBuffer(b *bytes.Buffer) {
169+
if b != nil {
170+
b.Reset()
171+
bufPool.Put(b)
172+
}
173+
}

redis_test.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
2+
// aahframework.org/cache/redis source code and usage is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package redis
6+
7+
import (
8+
"encoding/gob"
9+
"errors"
10+
"fmt"
11+
"io/ioutil"
12+
"testing"
13+
"time"
14+
15+
"aahframework.org/aah.v0/cache"
16+
"aahframework.org/config.v0"
17+
"aahframework.org/log.v0"
18+
"aahframework.org/test.v0/assert"
19+
)
20+
21+
func TestRedisCache(t *testing.T) {
22+
mgr := createCacheMgr(t, "redis1", `
23+
cache {
24+
redis1 {
25+
provider = "redis"
26+
address = "localhost:6379"
27+
}
28+
static {
29+
30+
}
31+
}
32+
`)
33+
34+
e := mgr.CreateCache(&cache.Config{Name: "cache1", ProviderName: "redis1"})
35+
assert.FailNowOnError(t, e, "unable to create cache")
36+
c := mgr.Cache("cache1")
37+
38+
type sample struct {
39+
Name string
40+
Present bool
41+
Value string
42+
}
43+
44+
testcases := []struct {
45+
label string
46+
key string
47+
value interface{}
48+
}{
49+
{
50+
label: "Redis Cache integer",
51+
key: "key1",
52+
value: 342348347,
53+
},
54+
{
55+
label: "Redis Cache float",
56+
key: "key2",
57+
value: 0.78346374,
58+
},
59+
{
60+
label: "Redis Cache string",
61+
key: "key3",
62+
value: "This is mt cache string",
63+
},
64+
{
65+
label: "Redis Cache map",
66+
key: "key4",
67+
value: map[string]interface{}{"key1": 343434, "key2": "kjdhdsjkdhjs", "key3": 87235.3465},
68+
},
69+
{
70+
label: "Redis Cache struct",
71+
key: "key5",
72+
value: sample{Name: "Jeeva", Present: true, Value: "redis cache provider"},
73+
},
74+
}
75+
76+
err := c.Put("pre-test-key1", sample{Name: "Jeeva", Present: true, Value: "redis cache provider"}, 3*time.Second)
77+
assert.Equal(t, errors.New("aah/cache: gob: type not registered for interface: redis.sample"), err)
78+
79+
gob.Register(map[string]interface{}{})
80+
gob.Register(sample{})
81+
82+
for _, tc := range testcases {
83+
t.Run(tc.label, func(t *testing.T) {
84+
assert.False(t, c.Exists(tc.key))
85+
assert.Nil(t, c.Get(tc.key))
86+
87+
err := c.Put(tc.key, tc.value, 3*time.Second)
88+
assert.Nil(t, err)
89+
90+
v := c.Get(tc.key)
91+
assert.Equal(t, tc.value, v)
92+
93+
c.Delete(tc.key)
94+
v = c.GetOrPut(tc.key, tc.value, 3*time.Second)
95+
assert.Equal(t, tc.value, v)
96+
})
97+
}
98+
99+
c.Flush()
100+
}
101+
102+
func TestRedisCacheAddAndGet(t *testing.T) {
103+
c := createTestCache(t, "redis1", `
104+
cache {
105+
redis1 {
106+
provider = "redis"
107+
address = "localhost:6379"
108+
}
109+
}
110+
`, &cache.Config{Name: "addgetcache", ProviderName: "redis1"})
111+
112+
for i := 0; i < 20; i++ {
113+
c.Put(fmt.Sprintf("key_%v", i), i, 3*time.Second)
114+
}
115+
116+
for i := 5; i < 10; i++ {
117+
v := c.Get(fmt.Sprintf("key_%v", i))
118+
assert.Equal(t, i, v)
119+
}
120+
assert.Equal(t, "addgetcache", c.Name())
121+
}
122+
123+
func TestRedisMultipleCache(t *testing.T) {
124+
mgr := createCacheMgr(t, "redis1", `
125+
cache {
126+
redis1 {
127+
provider = "redis"
128+
address = "localhost:6379"
129+
}
130+
}
131+
`)
132+
133+
names := []string{"testcache1", "testcache2", "testcache3"}
134+
for _, name := range names {
135+
err := mgr.CreateCache(&cache.Config{Name: name, ProviderName: "redis1"})
136+
assert.FailNowOnError(t, err, "unable to create cache")
137+
138+
c := mgr.Cache(name)
139+
assert.NotNil(t, c)
140+
assert.Equal(t, name, c.Name())
141+
142+
for i := 0; i < 20; i++ {
143+
c.Put(fmt.Sprintf("key_%v", i), i, 3*time.Second)
144+
}
145+
146+
for i := 5; i < 10; i++ {
147+
v := c.Get(fmt.Sprintf("key_%v", i))
148+
assert.Equal(t, i, v)
149+
}
150+
c.Flush()
151+
}
152+
}
153+
154+
func TestRedisSlideEvictionMode(t *testing.T) {
155+
c := createTestCache(t, "redis1", `
156+
cache {
157+
redis1 {
158+
provider = "redis"
159+
address = "localhost:6379"
160+
}
161+
}
162+
`, &cache.Config{Name: "addgetcache", ProviderName: "redis1", EvictionMode: cache.EvictionModeSlide})
163+
164+
for i := 0; i < 20; i++ {
165+
c.Put(fmt.Sprintf("key_%v", i), i, 3*time.Second)
166+
}
167+
168+
for i := 5; i < 10; i++ {
169+
v := c.GetOrPut(fmt.Sprintf("key_%v", i), i, 3*time.Second)
170+
assert.Equal(t, i, v)
171+
}
172+
173+
assert.Equal(t, "addgetcache", c.Name())
174+
}
175+
176+
func TestRedisInvalidProviderName(t *testing.T) {
177+
mgr := cache.NewManager()
178+
mgr.AddProvider("redis1", new(Provider))
179+
180+
cfg, _ := config.ParseString(`cache {
181+
redis1 {
182+
provider = "myredis"
183+
address = "localhost:6379"
184+
}
185+
}`)
186+
l, _ := log.New(config.NewEmpty())
187+
err := mgr.InitProviders(cfg, l)
188+
assert.Equal(t, errors.New("aah/cache: not a vaild provider name, expected 'redis'"), err)
189+
}
190+
191+
func TestRedisInvalidAddress(t *testing.T) {
192+
mgr := cache.NewManager()
193+
mgr.AddProvider("redis1", new(Provider))
194+
195+
cfg, _ := config.ParseString(`cache {
196+
redis1 {
197+
provider = "redis"
198+
address = "localhost:637967"
199+
}
200+
}`)
201+
l, _ := log.New(config.NewEmpty())
202+
err := mgr.InitProviders(cfg, l)
203+
assert.Equal(t, errors.New("aah/cache: dial tcp: address 637967: invalid port"), err)
204+
}
205+
206+
func createCacheMgr(t *testing.T, name, appCfgStr string) *cache.Manager {
207+
mgr := cache.NewManager()
208+
mgr.AddProvider(name, new(Provider))
209+
210+
cfg, _ := config.ParseString(appCfgStr)
211+
l, _ := log.New(config.NewEmpty())
212+
l.SetWriter(ioutil.Discard)
213+
err := mgr.InitProviders(cfg, l)
214+
assert.FailNowOnError(t, err, "unexpected")
215+
return mgr
216+
}
217+
218+
func createTestCache(t *testing.T, name, appCfgStr string, cacheCfg *cache.Config) cache.Cache {
219+
mgr := createCacheMgr(t, name, appCfgStr)
220+
e := mgr.CreateCache(cacheCfg)
221+
assert.FailNowOnError(t, e, "unable to create cache")
222+
return mgr.Cache(cacheCfg.Name)
223+
}

0 commit comments

Comments
 (0)