2019-08-06 17:40:13 +02:00
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
2019-08-17 09:06:20 +02:00
// returns nil and a filled error if something bad happened.
2019-08-06 17:40:13 +02:00
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 {
2019-10-24 17:57:05 +02:00
return nil , fmt . Errorf ( "%s: %w" , key , ErrKeyNotFound )
2019-08-06 17:40:13 +02:00
}
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
}
2019-08-20 13:19:42 +02:00
keys , err := r . getKeysConn ( c , 0 , prefix )
2019-08-06 17:40:13 +02:00
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
}
2019-08-20 13:19:42 +02:00
func ( r * RedigoDriver ) getKeysConn ( c redis . Conn , cursor interface { } , prefix string ) ( [ ] string , error ) {
if err := c . Send ( "SCAN" , cursor , "MATCH" , r . Config . Prefix + prefix + "*" , "COUNT" , 300000 ) ; err != nil {
2019-08-06 17:40:13 +02:00
return nil , err
}
if err := c . Flush ( ) ; err != nil {
return nil , err
}
reply , err := c . Receive ( )
if err != nil || reply == nil {
return nil , err
}
2019-08-20 13:19:42 +02:00
// it returns []interface, with two entries, the first one is the cursor, if "0" then full iteration
// and the second one is a slice of the keys as []interface{uint8....}.
2019-08-06 17:40:13 +02:00
2019-08-20 13:19:42 +02:00
if replies , ok := reply . ( [ ] interface { } ) ; ok {
if len ( replies ) == 2 {
2019-08-06 17:40:13 +02:00
// take the second, it must contain the slice of keys.
2019-08-20 13:19:42 +02:00
if keysSliceAsBytes , ok := replies [ 1 ] . ( [ ] interface { } ) ; ok {
2020-02-02 15:29:06 +01:00
keys := make ( [ ] string , len ( keysSliceAsBytes ) )
2019-08-20 13:19:42 +02:00
2019-08-06 17:40:13 +02:00
for i , k := range keysSliceAsBytes {
keys [ i ] = fmt . Sprintf ( "%s" , k ) [ len ( r . Config . Prefix ) : ]
}
2019-08-20 13:19:42 +02:00
if cur := fmt . Sprintf ( "%s" , replies [ 0 ] ) ; cur != "0" {
moreKeys , err := r . getKeysConn ( c , cur , prefix )
if err != nil {
return nil , err
}
keys = append ( keys , moreKeys ... )
}
2019-08-06 17:40:13 +02:00
return keys , nil
}
2019-08-20 13:19:42 +02:00
2019-08-06 17:40:13 +02:00
}
}
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
}
2019-08-20 13:19:42 +02:00
return r . getKeysConn ( c , 0 , prefix )
2019-08-06 17:40:13 +02:00
}
// GetBytes returns value, err by its key
// you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{})
2019-08-17 09:06:20 +02:00
// returns nil and a filled error if something wrong happens
2019-08-06 17:40:13 +02:00
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 {
2019-10-24 17:57:05 +02:00
return nil , fmt . Errorf ( "%s: %w" , key , ErrKeyNotFound )
2019-08-06 17:40:13 +02:00
}
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
}
2020-05-17 02:25:32 +02:00
// Connect connects to the redis, called only once.
func ( r * RedigoDriver ) Connect ( c Config ) error {
if c . Network == "" {
c . Network = DefaultRedisNetwork
2019-08-06 17:40:13 +02:00
}
2020-05-17 02:25:32 +02:00
if c . Addr == "" {
c . Addr = DefaultRedisAddr
}
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
2019-08-06 17:40:13 +02:00
}
var options [ ] redis . DialOption
2020-05-17 02:25:32 +02:00
if c . Timeout > 0 {
2019-08-06 17:40:13 +02:00
options = append ( options ,
2020-05-17 02:25:32 +02:00
redis . DialConnectTimeout ( c . Timeout ) ,
redis . DialReadTimeout ( c . Timeout ) ,
redis . DialWriteTimeout ( c . Timeout ) )
2019-08-06 17:40:13 +02:00
}
2020-05-17 02:25:32 +02:00
if c . TLSConfig != nil {
options = append ( options ,
redis . DialTLSConfig ( c . TLSConfig ) ,
redis . DialUseTLS ( true ) ,
)
2019-08-06 17:40:13 +02:00
}
2020-05-17 02:25:32 +02:00
pool . Dial = func ( ) ( redis . Conn , error ) {
conn , err := redis . Dial ( c . Network , c . Addr , options ... )
if err != nil {
2019-08-06 17:40:13 +02:00
return nil , err
}
2020-05-17 02:25:32 +02:00
if c . Password != "" {
if _ , err = conn . Do ( "AUTH" , c . Password ) ; err != nil {
conn . Close ( )
2019-08-06 17:40:13 +02:00
return nil , err
}
2020-05-17 02:25:32 +02:00
}
if c . Database != "" {
if _ , err = conn . Do ( "SELECT" , c . Database ) ; err != nil {
conn . Close ( )
2019-08-06 17:40:13 +02:00
return nil , err
}
}
2020-05-17 02:25:32 +02:00
return conn , err
2019-08-06 17:40:13 +02:00
}
2020-05-17 02:25:32 +02:00
2019-08-06 17:40:13 +02:00
r . Connected = true
r . pool = pool
r . Config = c
return nil
}