From eb29a80753264079ceb36cfdaae55b928f151e75 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 6 Aug 2019 18:40:13 +0300 Subject: [PATCH] redis sessiondb: support more than one driver - builtin redigo(default) and radix - rel to: #1328 Former-commit-id: 1eee58f2c49f64899fffc3ad61bcf074f8949cc1 --- _examples/sessions/database/redis/main.go | 10 +- go.mod | 1 + go.sum | 2 + sessions/sessiondb/redis/database.go | 122 ++++++- sessions/sessiondb/redis/driver.go | 34 ++ .../redis/{service.go => driver_radix.go} | 138 ++------ sessions/sessiondb/redis/driver_redigo.go | 326 ++++++++++++++++++ 7 files changed, 498 insertions(+), 135 deletions(-) create mode 100644 sessions/sessiondb/redis/driver.go rename sessions/sessiondb/redis/{service.go => driver_radix.go} (59%) create mode 100644 sessions/sessiondb/redis/driver_redigo.go diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index 1effb9ad..4b6a6d35 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -22,7 +22,15 @@ func main() { Database: "", Prefix: "", Delim: "-", - }) // optionally configure the bridge between your redis server. + Driver: redis.Redigo(), // redis.Radix() can be used instead. + }) + + // optionally configure the underline driver: + // driver := redis.Redigo() + // driver.MaxIdle = ... + // driver.IdleTimeout = ... + // driver.Wait = ... + // redis.Config {Driver: driver} // close connection when control+C/cmd+C iris.RegisterOnInterrupt(func() { diff --git a/go.mod b/go.mod index e5441374..2b77b023 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40 github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect github.com/mediocregopher/radix/v3 v3.3.0 + github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38 github.com/microcosm-cc/bluemonday v1.0.2 github.com/ryanuber/columnize v2.1.0+incompatible github.com/iris-contrib/schema v0.0.1 diff --git a/go.sum b/go.sum index d2b84984..3d42c983 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40/go.mod h1:PcaEvfvhGsqwXZ6S3CgCbmjcp+4UDUh2MIfF2ZEul8M= github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38 h1:y0Wmhvml7cGnzPa9nocn/fMraMH/lMDdeG+rkx4VgYY= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go index fd16fa05..8b511317 100644 --- a/sessions/sessiondb/redis/database.go +++ b/sessions/sessiondb/redis/database.go @@ -3,27 +3,108 @@ package redis import ( "time" + "github.com/kataras/iris/core/errors" "github.com/kataras/iris/sessions" "github.com/kataras/golog" ) +const ( + // DefaultRedisNetwork the redis network option, "tcp". + DefaultRedisNetwork = "tcp" + // DefaultRedisAddr the redis address option, "127.0.0.1:6379". + DefaultRedisAddr = "127.0.0.1:6379" + // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second + DefaultRedisTimeout = time.Duration(30) * time.Second + // DefaultDelim ths redis delim option, "-". + DefaultDelim = "-" +) + +// Config the redis configuration used inside sessions +type Config struct { + // Network protocol. Defaults to "tcp". + Network string + // Addr of the redis server. Defaults to "127.0.0.1:6379". + Addr string + // Password string .If no password then no 'AUTH'. Defaults to "". + Password string + // If Database is empty "" then no 'SELECT'. Defaults to "". + Database string + // MaxActive. Defaults to 10. + MaxActive int + // Timeout for connect, write and read, defaults to 30 seconds, 0 means no timeout. + Timeout time.Duration + // Prefix "myprefix-for-this-website". Defaults to "". + Prefix string + // Delim the delimeter for the keys on the sessiondb. Defaults to "-". + Delim string + + // Driver supports `Redigo()` or `Radix()` go clients for redis. + // Configure each driver by the return value of their constructors. + // + // Defaults to `Redigo()`. + Driver Driver +} + +// DefaultConfig returns the default configuration for Redis service. +func DefaultConfig() Config { + return Config{ + Network: DefaultRedisNetwork, + Addr: DefaultRedisAddr, + Password: "", + Database: "", + MaxActive: 10, + Timeout: DefaultRedisTimeout, + Prefix: "", + Delim: DefaultDelim, + Driver: Redigo(), + } +} + // Database the redis back-end session database for the sessions. type Database struct { - redis *Service + c Config } var _ sessions.Database = (*Database)(nil) // New returns a new redis database. func New(cfg ...Config) *Database { - service := newService(cfg...) - if err := service.Connect(); err != nil { + c := DefaultConfig() + if len(cfg) > 0 { + c = cfg[0] + + if c.Timeout < 0 { + c.Timeout = DefaultRedisTimeout + } + + if c.Network == "" { + c.Network = DefaultRedisNetwork + } + + if c.Addr == "" { + c.Addr = DefaultRedisAddr + } + + if c.MaxActive == 0 { + c.MaxActive = 10 + } + + if c.Delim == "" { + c.Delim = DefaultDelim + } + + if c.Driver == nil { + c.Driver = Redigo() + } + } + + if err := c.Driver.Connect(c); err != nil { panic(err) } - db := &Database{redis: service} - _, err := db.redis.PingPong() + db := &Database{c: c} + _, err := db.c.Driver.PingPong() if err != nil { golog.Debugf("error connecting to redis: %v", err) return nil @@ -34,17 +115,17 @@ func New(cfg ...Config) *Database { // Config returns the configuration for the redis server bridge, you can change them. func (db *Database) Config() *Config { - return db.redis.Config + return &db.c // 6 Aug 2019 - keep that for no breaking change. } // Acquire receives a session's lifetime from the database, // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime { - seconds, hasExpiration, found := db.redis.TTL(sid) + seconds, hasExpiration, found := db.c.Driver.TTL(sid) if !found { // fmt.Printf("db.Acquire expires: %s. Seconds: %v\n", expires, expires.Seconds()) // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. - if err := db.redis.Set(sid, sid, int64(expires.Seconds())); err != nil { + if err := db.c.Driver.Set(sid, sid, int64(expires.Seconds())); err != nil { golog.Debug(err) } @@ -62,11 +143,11 @@ func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime // OnUpdateExpiration will re-set the database's session's entry ttl. // https://redis.io/commands/expire#refreshing-expires func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { - return db.redis.UpdateTTLMany(sid, int64(newExpires.Seconds())) + return db.c.Driver.UpdateTTLMany(sid, int64(newExpires.Seconds())) } func (db *Database) makeKey(sid, key string) string { - return sid + db.redis.Config.Delim + key + return sid + db.c.Delim + key } // Set sets a key value of a specific session. @@ -80,7 +161,7 @@ func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, valu // fmt.Println("database.Set") // fmt.Printf("lifetime.DurationUntilExpiration(): %s. Seconds: %v\n", lifetime.DurationUntilExpiration(), lifetime.DurationUntilExpiration().Seconds()) - if err = db.redis.Set(db.makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { + if err = db.c.Driver.Set(db.makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { golog.Debug(err) } } @@ -92,7 +173,7 @@ func (db *Database) Get(sid string, key string) (value interface{}) { } func (db *Database) get(key string, outPtr interface{}) { - data, err := db.redis.Get(key) + data, err := db.c.Driver.Get(key) if err != nil { // not found. return @@ -104,7 +185,7 @@ func (db *Database) get(key string, outPtr interface{}) { } func (db *Database) keys(sid string) []string { - keys, err := db.redis.GetKeys(sid) + keys, err := db.c.Driver.GetKeys(sid) if err != nil { golog.Debugf("unable to get all redis keys of session '%s': %v", sid, err) return nil @@ -130,7 +211,7 @@ func (db *Database) Len(sid string) (n int) { // Delete removes a session key value based on its key. func (db *Database) Delete(sid string, key string) (deleted bool) { - err := db.redis.Delete(db.makeKey(sid, key)) + err := db.c.Driver.Delete(db.makeKey(sid, key)) if err != nil { golog.Error(err) } @@ -141,7 +222,7 @@ func (db *Database) Delete(sid string, key string) (deleted bool) { func (db *Database) Clear(sid string) { keys := db.keys(sid) for _, key := range keys { - if err := db.redis.Delete(key); err != nil { + if err := db.c.Driver.Delete(key); err != nil { golog.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err) } } @@ -153,7 +234,7 @@ func (db *Database) Release(sid string) { // clear all $sid-$key. db.Clear(sid) // and remove the $sid. - db.redis.Delete(sid) + db.c.Driver.Delete(sid) } // Close terminates the redis connection. @@ -162,5 +243,12 @@ func (db *Database) Close() error { } func closeDB(db *Database) error { - return db.redis.CloseConnection() + return db.c.Driver.CloseConnection() } + +var ( + // ErrRedisClosed an error with message 'Redis is already closed' + ErrRedisClosed = errors.New("Redis is already closed") + // ErrKeyNotFound an error with message 'Key $thekey doesn't found' + ErrKeyNotFound = errors.New("Key '%s' doesn't found") +) diff --git a/sessions/sessiondb/redis/driver.go b/sessions/sessiondb/redis/driver.go new file mode 100644 index 00000000..3aae3ab9 --- /dev/null +++ b/sessions/sessiondb/redis/driver.go @@ -0,0 +1,34 @@ +package redis + +// Driver is the interface which each supported redis client +// should support in order to be used in the redis session database. +type Driver interface { + Connect(c Config) error + PingPong() (bool, error) + CloseConnection() error + Set(key string, value interface{}, secondsLifetime int64) error + Get(key string) (interface{}, error) + TTL(key string) (seconds int64, hasExpiration bool, found bool) + UpdateTTL(key string, newSecondsLifeTime int64) error + UpdateTTLMany(prefix string, newSecondsLifeTime int64) error + GetAll() (interface{}, error) + GetKeys(prefix string) ([]string, error) + Delete(key string) error +} + +var ( + _ Driver = (*RedigoDriver)(nil) + _ Driver = (*RadixDriver)(nil) +) + +// Redigo returns the driver for the redigo go redis client. +// Which is the default one. +// You can customize further any specific driver's properties. +func Redigo() *RedigoDriver { + return &RedigoDriver{} +} + +// Radix returns the driver for the radix go redis client. +func Radix() *RadixDriver { + return &RadixDriver{} +} diff --git a/sessions/sessiondb/redis/service.go b/sessions/sessiondb/redis/driver_radix.go similarity index 59% rename from sessions/sessiondb/redis/service.go rename to sessions/sessiondb/redis/driver_radix.go index b8f83095..e379b8db 100644 --- a/sessions/sessiondb/redis/service.go +++ b/sessions/sessiondb/redis/driver_radix.go @@ -3,90 +3,22 @@ package redis import ( "fmt" "strconv" - "time" - - "github.com/kataras/iris/core/errors" "github.com/mediocregopher/radix/v3" ) -const ( - // DefaultRedisNetwork the redis network option, "tcp". - DefaultRedisNetwork = "tcp" - // DefaultRedisAddr the redis address option, "127.0.0.1:6379". - DefaultRedisAddr = "127.0.0.1:6379" - // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second - DefaultRedisTimeout = time.Duration(30) * time.Second - // DefaultDelim ths redis delim option, "-". - DefaultDelim = "-" -) - -// Config the redis configuration used inside sessions -type Config struct { - // Network protocol. Defaults to "tcp". - Network string - // Addr of the redis server. Defaults to "127.0.0.1:6379". - Addr string - // Password string .If no password then no 'AUTH'. Defaults to "". - Password string - // If Database is empty "" then no 'SELECT'. Defaults to "". - Database string - // MaxActive. Defaults to 10. - MaxActive int - // Timeout for connect, write and read, defaults to 30 seconds, 0 means no timeout. - Timeout time.Duration - // Prefix "myprefix-for-this-website". Defaults to "". - Prefix string - // Delim the delimeter for the keys on the sessiondb. Defaults to "-". - Delim string -} - -// DefaultConfig returns the default configuration for Redis service. -func DefaultConfig() Config { - return Config{ - Network: DefaultRedisNetwork, - Addr: DefaultRedisAddr, - Password: "", - Database: "", - MaxActive: 10, - Timeout: DefaultRedisTimeout, - Prefix: "", - Delim: DefaultDelim, - } -} - -var ( - // ErrRedisClosed an error with message 'Redis is already closed' - ErrRedisClosed = errors.New("Redis is already closed") - // ErrKeyNotFound an error with message 'Key $thekey doesn't found' - ErrKeyNotFound = errors.New("Key '%s' doesn't found") -) - -// Service the Redis service, contains the config and the redis pool -type Service struct { +// RadixDriver the Redis service based on the radix go client, +// contains the config and the redis pool. +type RadixDriver struct { // Connected is true when the Service has already connected Connected bool - // Config the redis config for this redis - Config *Config + // Config the read-only redis database config. + Config Config pool *radix.Pool } -// newService returns a Redis service filled by the passed config -// to connect call the .Connect(). -func newService(cfg ...Config) *Service { - c := DefaultConfig() - if len(cfg) > 0 { - c = cfg[0] - } - - r := &Service{Config: &c} - return r -} - // Connect connects to the redis, called only once -func (r *Service) Connect() error { - c := r.Config - +func (r *RadixDriver) Connect(c Config) error { if c.Timeout < 0 { c.Timeout = DefaultRedisTimeout } @@ -139,11 +71,12 @@ func (r *Service) Connect() error { r.Connected = true r.pool = pool + r.Config = c return nil } // PingPong sends a ping and receives a pong, if no pong received then returns false and filled error -func (r *Service) PingPong() (bool, error) { +func (r *RadixDriver) PingPong() (bool, error) { var msg string err := r.pool.Do(radix.Cmd(&msg, "PING")) if err != nil { @@ -152,8 +85,8 @@ func (r *Service) PingPong() (bool, error) { return (msg == "PONG"), nil } -// CloseConnection closes the redis connection -func (r *Service) CloseConnection() error { +// CloseConnection closes the redis connection. +func (r *RadixDriver) CloseConnection() error { if r.pool != nil { return r.pool.Close() } @@ -161,8 +94,8 @@ func (r *Service) CloseConnection() error { } // Set sets a key-value to the redis store. -// The expiration is setted by the MaxAgeSeconds. -func (r *Service) Set(key string, value interface{}, secondsLifetime int64) error { +// The expiration is setted by the secondsLifetime. +func (r *RadixDriver) Set(key string, value interface{}, secondsLifetime int64) error { // fmt.Printf("%#+v. %T. %s\n", value, value, value) // if vB, ok := value.([]byte); ok && secondsLifetime <= 0 { @@ -182,7 +115,7 @@ func (r *Service) Set(key string, value interface{}, secondsLifetime int64) erro // Get returns value, err by its key //returns nil and a filled error if something bad happened. -func (r *Service) Get(key string) (interface{}, error) { +func (r *RadixDriver) Get(key string) (interface{}, error) { var redisVal interface{} mn := radix.MaybeNil{Rcv: &redisVal} @@ -199,7 +132,7 @@ func (r *Service) Get(key string) (interface{}, error) { // TTL returns the seconds to expire, if the key has expiration and error if action failed. // Read more at: https://redis.io/commands/ttl -func (r *Service) TTL(key string) (seconds int64, hasExpiration bool, found bool) { +func (r *RadixDriver) TTL(key string) (seconds int64, hasExpiration bool, found bool) { var redisVal interface{} err := r.pool.Do(radix.Cmd(&redisVal, "TTL", r.Config.Prefix+key)) if err != nil { @@ -213,7 +146,7 @@ func (r *Service) TTL(key string) (seconds int64, hasExpiration bool, found bool return } -func (r *Service) updateTTLConn(key string, newSecondsLifeTime int64) error { +func (r *RadixDriver) updateTTLConn(key string, newSecondsLifeTime int64) error { var reply int err := r.pool.Do(radix.FlatCmd(&reply, "EXPIRE", r.Config.Prefix+key, newSecondsLifeTime)) if err != nil { @@ -237,14 +170,14 @@ func (r *Service) updateTTLConn(key string, newSecondsLifeTime int64) error { // UpdateTTL will update the ttl of a key. // Using the "EXPIRE" command. // Read more at: https://redis.io/commands/expire#refreshing-expires -func (r *Service) UpdateTTL(key string, newSecondsLifeTime int64) error { +func (r *RadixDriver) UpdateTTL(key string, newSecondsLifeTime int64) error { return r.updateTTLConn(key, newSecondsLifeTime) } // UpdateTTLMany like `UpdateTTL` but for all keys starting with that "prefix", // it is a bit faster operation if you need to update all sessions keys (although it can be even faster if we used hash but this will limit other features), // look the `sessions/Database#OnUpdateExpiration` for example. -func (r *Service) UpdateTTLMany(prefix string, newSecondsLifeTime int64) error { +func (r *RadixDriver) UpdateTTLMany(prefix string, newSecondsLifeTime int64) error { keys, err := r.getKeys(prefix) if err != nil { return err @@ -260,7 +193,7 @@ func (r *Service) UpdateTTLMany(prefix string, newSecondsLifeTime int64) error { } // GetAll returns all redis entries using the "SCAN" command (2.8+). -func (r *Service) GetAll() (interface{}, error) { +func (r *RadixDriver) GetAll() (interface{}, error) { var redisVal []interface{} mn := radix.MaybeNil{Rcv: &redisVal} err := r.pool.Do(radix.Cmd(&mn, "SCAN", strconv.Itoa(0))) // 0 -> cursor @@ -276,7 +209,7 @@ func (r *Service) GetAll() (interface{}, error) { return redisVal, nil } -func (r *Service) getKeys(prefix string) ([]string, error) { +func (r *RadixDriver) getKeys(prefix string) ([]string, error) { var keys []string // err := r.pool.Do(radix.Cmd(&keys, "MATCH", r.Config.Prefix+prefix+"*")) // if err != nil { @@ -297,41 +230,12 @@ func (r *Service) getKeys(prefix string) ([]string, error) { return nil, err } - // if err := c.Send("SCAN", 0, "MATCH", r.Config.Prefix+prefix+"*", "COUNT", 9999999999); err != nil { - // return nil, err - // } - - // if err := c.Flush(); err != nil { - // return nil, err - // } - - // reply, err := c.Receive() - // if err != nil || reply == nil { - // return nil, err - // } - - // it returns []interface, with two entries, the first one is "0" and the second one is a slice of the keys as []interface{uint8....}. - - // if keysInterface, ok := reply.([]interface{}); ok { - // if len(keysInterface) == 2 { - // // take the second, it must contain the slice of keys. - // if keysSliceAsBytes, ok := keysInterface[1].([]interface{}); ok { - // keys := make([]string, len(keysSliceAsBytes), len(keysSliceAsBytes)) - // for i, k := range keysSliceAsBytes { - // keys[i] = fmt.Sprintf("%s", k)[len(r.Config.Prefix):] - // } - - // return keys, nil - // } - // } - // } - return keys, nil } // GetKeys returns all redis keys using the "SCAN" with MATCH command. // Read more at: https://redis.io/commands/scan#the-match-option. -func (r *Service) GetKeys(prefix string) ([]string, error) { +func (r *RadixDriver) GetKeys(prefix string) ([]string, error) { return r.getKeys(prefix) } @@ -351,7 +255,7 @@ func (r *Service) GetKeys(prefix string) ([]string, error) { // } // Delete removes redis entry by specific key -func (r *Service) Delete(key string) error { +func (r *RadixDriver) Delete(key string) error { err := r.pool.Do(radix.Cmd(nil, "DEL", r.Config.Prefix+key)) return err } diff --git a/sessions/sessiondb/redis/driver_redigo.go b/sessions/sessiondb/redis/driver_redigo.go new file mode 100644 index 00000000..675ba58f --- /dev/null +++ b/sessions/sessiondb/redis/driver_redigo.go @@ -0,0 +1,326 @@ +package redis + +import ( + "fmt" + "time" + + "github.com/gomodule/redigo/redis" +) + +// RedigoDriver is the redigo Redis go client, +// contains the config and the redis pool +type RedigoDriver struct { + // Config the read-only redis database config. + Config Config + + // Maximum number of idle connections in the pool. + MaxIdle int + + // Close connections after remaining idle for this duration. If the value + // is zero, then idle connections are not closed. Applications should set + // the timeout to a value less than the server's timeout. + IdleTimeout time.Duration + + // If Wait is true and the pool is at the MaxActive limit, then Get() waits + // for a connection to be returned to the pool before returning. + Wait bool + + // Connected is true when the Service has already connected + Connected bool + + pool *redis.Pool +} + +// PingPong sends a ping and receives a pong, if no pong received then returns false and filled error +func (r *RedigoDriver) PingPong() (bool, error) { + c := r.pool.Get() + defer c.Close() + msg, err := c.Do("PING") + if err != nil || msg == nil { + return false, err + } + return (msg == "PONG"), nil +} + +// CloseConnection closes the redis connection. +func (r *RedigoDriver) CloseConnection() error { + if r.pool != nil { + return r.pool.Close() + } + return ErrRedisClosed +} + +// Set sets a key-value to the redis store. +// The expiration is setted by the secondsLifetime. +func (r *RedigoDriver) Set(key string, value interface{}, secondsLifetime int64) (err error) { + c := r.pool.Get() + defer c.Close() + if c.Err() != nil { + return c.Err() + } + + // if has expiration, then use the "EX" to delete the key automatically. + if secondsLifetime > 0 { + _, err = c.Do("SETEX", r.Config.Prefix+key, secondsLifetime, value) + } else { + _, err = c.Do("SET", r.Config.Prefix+key, value) + } + + return +} + +// Get returns value, err by its key +//returns nil and a filled error if something bad happened. +func (r *RedigoDriver) Get(key string) (interface{}, error) { + c := r.pool.Get() + defer c.Close() + if err := c.Err(); err != nil { + return nil, err + } + + redisVal, err := c.Do("GET", r.Config.Prefix+key) + + if err != nil { + return nil, err + } + if redisVal == nil { + return nil, ErrKeyNotFound.Format(key) + } + return redisVal, nil +} + +// TTL returns the seconds to expire, if the key has expiration and error if action failed. +// Read more at: https://redis.io/commands/ttl +func (r *RedigoDriver) TTL(key string) (seconds int64, hasExpiration bool, found bool) { + c := r.pool.Get() + defer c.Close() + redisVal, err := c.Do("TTL", r.Config.Prefix+key) + if err != nil { + return -2, false, false + } + seconds = redisVal.(int64) + // if -1 means the key has unlimited life time. + hasExpiration = seconds > -1 + // if -2 means key does not exist. + found = !(c.Err() != nil || seconds == -2) + return +} + +func (r *RedigoDriver) updateTTLConn(c redis.Conn, key string, newSecondsLifeTime int64) error { + reply, err := c.Do("EXPIRE", r.Config.Prefix+key, newSecondsLifeTime) + + if err != nil { + return err + } + + // https://redis.io/commands/expire#return-value + // + // 1 if the timeout was set. + // 0 if key does not exist. + if hadTTLOrExists, ok := reply.(int); ok { + if hadTTLOrExists == 1 { + return nil + } else if hadTTLOrExists == 0 { + return fmt.Errorf("unable to update expiration, the key '%s' was stored without ttl", key) + } // do not check for -1. + } + + return nil +} + +// UpdateTTL will update the ttl of a key. +// Using the "EXPIRE" command. +// Read more at: https://redis.io/commands/expire#refreshing-expires +func (r *RedigoDriver) UpdateTTL(key string, newSecondsLifeTime int64) error { + c := r.pool.Get() + defer c.Close() + err := c.Err() + if err != nil { + return err + } + + return r.updateTTLConn(c, key, newSecondsLifeTime) +} + +// UpdateTTLMany like `UpdateTTL` but for all keys starting with that "prefix", +// it is a bit faster operation if you need to update all sessions keys (although it can be even faster if we used hash but this will limit other features), +// look the `sessions/Database#OnUpdateExpiration` for example. +func (r *RedigoDriver) UpdateTTLMany(prefix string, newSecondsLifeTime int64) error { + c := r.pool.Get() + defer c.Close() + if err := c.Err(); err != nil { + return err + } + + keys, err := r.getKeysConn(c, prefix) + if err != nil { + return err + } + + for _, key := range keys { + if err = r.updateTTLConn(c, key, newSecondsLifeTime); err != nil { // fail on first error. + return err + } + } + + return err +} + +// GetAll returns all redis entries using the "SCAN" command (2.8+). +func (r *RedigoDriver) GetAll() (interface{}, error) { + c := r.pool.Get() + defer c.Close() + if err := c.Err(); err != nil { + return nil, err + } + + redisVal, err := c.Do("SCAN", 0) // 0 -> cursor + + if err != nil { + return nil, err + } + + if redisVal == nil { + return nil, err + } + + return redisVal, nil +} + +func (r *RedigoDriver) getKeysConn(c redis.Conn, prefix string) ([]string, error) { + if err := c.Send("SCAN", 0, "MATCH", r.Config.Prefix+prefix+"*", "COUNT", 9999999999); err != nil { + return nil, err + } + + if err := c.Flush(); err != nil { + return nil, err + } + + reply, err := c.Receive() + if err != nil || reply == nil { + return nil, err + } + + // it returns []interface, with two entries, the first one is "0" and the second one is a slice of the keys as []interface{uint8....}. + + if keysInterface, ok := reply.([]interface{}); ok { + if len(keysInterface) == 2 { + // take the second, it must contain the slice of keys. + if keysSliceAsBytes, ok := keysInterface[1].([]interface{}); ok { + keys := make([]string, len(keysSliceAsBytes), len(keysSliceAsBytes)) + for i, k := range keysSliceAsBytes { + keys[i] = fmt.Sprintf("%s", k)[len(r.Config.Prefix):] + } + + return keys, nil + } + } + } + + return nil, nil +} + +// GetKeys returns all redis keys using the "SCAN" with MATCH command. +// Read more at: https://redis.io/commands/scan#the-match-option. +func (r *RedigoDriver) GetKeys(prefix string) ([]string, error) { + c := r.pool.Get() + defer c.Close() + if err := c.Err(); err != nil { + return nil, err + } + + return r.getKeysConn(c, prefix) +} + +// GetBytes returns value, err by its key +// you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{}) +//returns nil and a filled error if something wrong happens +func (r *RedigoDriver) GetBytes(key string) ([]byte, error) { + c := r.pool.Get() + defer c.Close() + if err := c.Err(); err != nil { + return nil, err + } + + redisVal, err := c.Do("GET", r.Config.Prefix+key) + + if err != nil { + return nil, err + } + if redisVal == nil { + return nil, ErrKeyNotFound.Format(key) + } + + return redis.Bytes(redisVal, err) +} + +// Delete removes redis entry by specific key +func (r *RedigoDriver) Delete(key string) error { + c := r.pool.Get() + defer c.Close() + + _, err := c.Do("DEL", r.Config.Prefix+key) + return err +} + +func dial(network string, addr string, pass string, timeout time.Duration) (redis.Conn, error) { + if network == "" { + network = DefaultRedisNetwork + } + if addr == "" { + addr = DefaultRedisAddr + } + + var options []redis.DialOption + + if timeout > 0 { + options = append(options, + redis.DialConnectTimeout(timeout), + redis.DialReadTimeout(timeout), + redis.DialWriteTimeout(timeout)) + } + + c, err := redis.Dial(network, addr, options...) + if err != nil { + return nil, err + } + + if pass != "" { + if _, err = c.Do("AUTH", pass); err != nil { + c.Close() + return nil, err + } + } + return c, err +} + +// Connect connects to the redis, called only once +func (r *RedigoDriver) Connect(c Config) error { + pool := &redis.Pool{IdleTimeout: r.IdleTimeout, MaxIdle: r.MaxIdle, Wait: r.Wait, MaxActive: c.MaxActive} + pool.TestOnBorrow = func(c redis.Conn, t time.Time) error { + _, err := c.Do("PING") + return err + } + + if c.Database != "" { + pool.Dial = func() (redis.Conn, error) { + red, err := dial(c.Network, c.Addr, c.Password, c.Timeout) + if err != nil { + return nil, err + } + if _, err = red.Do("SELECT", c.Database); err != nil { + red.Close() + return nil, err + } + return red, err + } + } else { + pool.Dial = func() (redis.Conn, error) { + return dial(c.Network, c.Addr, c.Password, c.Timeout) + } + } + r.Connected = true + r.pool = pool + r.Config = c + return nil +}