1 // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
2 // Revel Framework source code and usage is governed by a MIT style
3 // license that can be found in the LICENSE file.
10 "github.com/garyburd/redigo/redis"
11 "github.com/revel/revel"
14 // RedisCache wraps the Redis client to meet the Cache interface.
15 type RedisCache struct {
17 defaultExpiration time.Duration
20 // NewRedisCache returns a new RedisCache with given parameters
21 // until redigo supports sharding/clustering, only one host will be in hostList
22 func NewRedisCache(host string, password string, defaultExpiration time.Duration) RedisCache {
23 var pool = &redis.Pool{
24 MaxIdle: revel.Config.IntDefault("cache.redis.maxidle", 5),
25 MaxActive: revel.Config.IntDefault("cache.redis.maxactive", 0),
26 IdleTimeout: time.Duration(revel.Config.IntDefault("cache.redis.idletimeout", 240)) * time.Second,
27 Dial: func() (redis.Conn, error) {
28 protocol := revel.Config.StringDefault("cache.redis.protocol", "tcp")
29 toc := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.connect", 10000))
30 tor := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.read", 5000))
31 tow := time.Millisecond * time.Duration(revel.Config.IntDefault("cache.redis.timeout.write", 5000))
32 c, err := redis.Dial(protocol, host,
33 redis.DialConnectTimeout(toc),
34 redis.DialReadTimeout(tor),
35 redis.DialWriteTimeout(tow))
39 if len(password) > 0 {
40 if _, err = c.Do("AUTH", password); err != nil {
46 if _, err = c.Do("PING"); err != nil {
53 // custom connection test method
54 TestOnBorrow: func(c redis.Conn, t time.Time) error {
55 _, err := c.Do("PING")
59 return RedisCache{pool, defaultExpiration}
62 func (c RedisCache) Set(key string, value interface{}, expires time.Duration) error {
67 return c.invoke(conn.Do, key, value, expires)
70 func (c RedisCache) Add(key string, value interface{}, expires time.Duration) error {
76 existed, err := exists(conn, key)
82 return c.invoke(conn.Do, key, value, expires)
85 func (c RedisCache) Replace(key string, value interface{}, expires time.Duration) error {
91 existed, err := exists(conn, key)
98 err = c.invoke(conn.Do, key, value, expires)
105 func (c RedisCache) Get(key string, ptrValue interface{}) error {
110 raw, err := conn.Do("GET", key)
113 } else if raw == nil {
116 item, err := redis.Bytes(raw, err)
120 return Deserialize(item, ptrValue)
123 func generalizeStringSlice(strs []string) []interface{} {
124 ret := make([]interface{}, len(strs))
125 for i, str := range strs {
131 func (c RedisCache) GetMulti(keys ...string) (Getter, error) {
137 items, err := redis.Values(conn.Do("MGET", generalizeStringSlice(keys)...))
140 } else if items == nil {
141 return nil, ErrCacheMiss
144 m := make(map[string][]byte)
145 for i, key := range keys {
147 if i < len(items) && items[i] != nil {
148 s, ok := items[i].([]byte)
154 return RedisItemMapGetter(m), nil
157 func exists(conn redis.Conn, key string) (bool, error) {
158 return redis.Bool(conn.Do("EXISTS", key))
161 func (c RedisCache) Delete(key string) error {
166 existed, err := redis.Bool(conn.Do("DEL", key))
167 if err == nil && !existed {
173 func (c RedisCache) Increment(key string, delta uint64) (uint64, error) {
178 // Check for existence *before* increment as per the cache contract.
179 // redis will auto create the key, and we don't want that. Since we need to do increment
180 // ourselves instead of natively via INCRBY (redis doesn't support wrapping), we get the value
181 // and do the exists check this way to minimize calls to Redis
182 val, err := conn.Do("GET", key)
185 } else if val == nil {
186 return 0, ErrCacheMiss
188 currentVal, err := redis.Int64(val, nil)
192 sum := currentVal + int64(delta)
193 _, err = conn.Do("SET", key, sum)
197 return uint64(sum), nil
200 func (c RedisCache) Decrement(key string, delta uint64) (newValue uint64, err error) {
205 // Check for existence *before* increment as per the cache contract.
206 // redis will auto create the key, and we don't want that, hence the exists call
207 existed, err := exists(conn, key)
211 return 0, ErrCacheMiss
213 // Decrement contract says you can only go to 0
214 // so we go fetch the value and if the delta is greater than the amount,
216 currentVal, err := redis.Int64(conn.Do("GET", key))
220 if delta > uint64(currentVal) {
222 tempint, err = redis.Int64(conn.Do("DECRBY", key, currentVal))
223 return uint64(tempint), err
225 tempint, err := redis.Int64(conn.Do("DECRBY", key, delta))
226 return uint64(tempint), err
229 func (c RedisCache) Flush() error {
234 _, err := conn.Do("FLUSHALL")
238 func (c RedisCache) invoke(f func(string, ...interface{}) (interface{}, error),
239 key string, value interface{}, expires time.Duration) error {
242 case DefaultExpiryTime:
243 expires = c.defaultExpiration
244 case ForEverNeverExpiry:
245 expires = time.Duration(0)
248 b, err := Serialize(value)
257 _, err = f("SETEX", key, int32(expires/time.Second), b)
260 _, err = f("SET", key, b)
264 // RedisItemMapGetter implements a Getter on top of the returned item map.
265 type RedisItemMapGetter map[string][]byte
267 func (g RedisItemMapGetter) Get(key string, ptrValue interface{}) error {
272 return Deserialize(item, ptrValue)