Skip to content

Commit

Permalink
feat(redis): support redis failover (sentinel) (#38)
Browse files Browse the repository at this point in the history
* feat(redisfailover): support redis failover (sentinel)

This adds the go-redis/redis FailoverClient which is used for Redis Sentinel.

* fix(redisfailover): sentinel password

* refactor(redisfailover): combine with redis provider

* refactor(redis): expressly define structs

* refactor: use upstream addrs name

* fix: invalid struct key

* refactor: remove cluster for a later PR

* fix(redis): remove invalid options for failover

the routing options are only valid for cluster clients as per upstream docs

* refactor(redis): support both NewFailoverClient and NewFailoverClusterClient

The documentation upstream is slightly confusing. This has already been tested upstream by Authelia.

* docs(redis): add docs about the different clients and their purpose.

* docs(redis): add go-docs

* refactor: keyprefix -> keyPrefix
  • Loading branch information
james-d-elliott authored Mar 11, 2021
1 parent de41073 commit e884ba3
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 5 deletions.
12 changes: 12 additions & 0 deletions providers/redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,15 @@ Better encoder:

- Encode: `session.MSGPEncode`
- Decode: `session.MSGPDecode`

## Clients

The redis provider supports the upstream standard client via New(), the sentinel client via NewFailover(), and
the sentinel fail over client via NewFailoverCluster().

The difference between the standard client and the sentinel clients is that the standard client will only connect to a
single redis server and the sentinel clients will connect to sentinel servers and figure out which redis server
to connect to automatically. This allows for a failure of a redis server if you configure sentinel and redis correctly.

The difference between the sentinel client via NewFailover() and the sentinel fail over client via NewFailoverCluster()
is the fail over client will fail over to other sentinels if they are configured in the event of a failure.
2 changes: 2 additions & 0 deletions providers/redis/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (

var errConfigAddrEmpty = errors.New("Config Addr must not be empty")

var errConfigMasterNameEmpty = errors.New("Config MasterName must not be empty")

func errRedisConnection(err error) error {
return fmt.Errorf("Redis connection error: %v", err)
}
90 changes: 87 additions & 3 deletions providers/redis/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,100 @@ func New(cfg Config) (*Provider, error) {
}

p := &Provider{
config: cfg,
db: db,
keyPrefix: cfg.KeyPrefix,
db: db,
}

return p, nil
}

// NewFailover returns a new redis provider using sentinel to determine the redis server to connect to.
func NewFailover(cfg FailoverConfig) (*Provider, error) {
if cfg.MasterName == "" {
return nil, errConfigMasterNameEmpty
}

db := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: cfg.MasterName,
SentinelAddrs: cfg.SentinelAddrs,
SentinelPassword: cfg.SentinelPassword,
SlaveOnly: cfg.SlaveOnly,
Username: cfg.Username,
Password: cfg.Password,
DB: cfg.DB,
MaxRetries: cfg.MaxRetries,
MinRetryBackoff: cfg.MinRetryBackoff,
MaxRetryBackoff: cfg.MaxRetryBackoff,
DialTimeout: cfg.DialTimeout,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
PoolSize: cfg.PoolSize,
MinIdleConns: cfg.MinIdleConns,
MaxConnAge: cfg.MaxConnAge,
PoolTimeout: cfg.PoolTimeout,
IdleTimeout: cfg.IdleTimeout,
IdleCheckFrequency: cfg.IdleCheckFrequency,
TLSConfig: cfg.TLSConfig,
})

if err := db.Ping(context.Background()).Err(); err != nil {
return nil, errRedisConnection(err)
}

p := &Provider{
keyPrefix: cfg.KeyPrefix,
db: db,
}

return p, nil
}

// NewFailoverCluster returns a new redis provider using a group of sentinels to determine the redis server to connect to.
func NewFailoverCluster(cfg FailoverConfig) (*Provider, error) {
if cfg.MasterName == "" {
return nil, errConfigMasterNameEmpty
}

db := redis.NewFailoverClusterClient(&redis.FailoverOptions{
MasterName: cfg.MasterName,
SentinelAddrs: cfg.SentinelAddrs,
SentinelPassword: cfg.SentinelPassword,
RouteByLatency: cfg.RouteByLatency,
RouteRandomly: cfg.RouteRandomly,
SlaveOnly: cfg.SlaveOnly,
Username: cfg.Username,
Password: cfg.Password,
DB: cfg.DB,
MaxRetries: cfg.MaxRetries,
MinRetryBackoff: cfg.MinRetryBackoff,
MaxRetryBackoff: cfg.MaxRetryBackoff,
DialTimeout: cfg.DialTimeout,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
PoolSize: cfg.PoolSize,
MinIdleConns: cfg.MinIdleConns,
MaxConnAge: cfg.MaxConnAge,
PoolTimeout: cfg.PoolTimeout,
IdleTimeout: cfg.IdleTimeout,
IdleCheckFrequency: cfg.IdleCheckFrequency,
TLSConfig: cfg.TLSConfig,
})

if err := db.Ping(context.Background()).Err(); err != nil {
return nil, errRedisConnection(err)
}

p := &Provider{
keyPrefix: cfg.KeyPrefix,
db: db,
}

return p, nil
}

func (p *Provider) getRedisSessionKey(sessionID []byte) string {
key := bytebufferpool.Get()
key.SetString(p.config.KeyPrefix)
key.SetString(p.keyPrefix)
key.WriteString(":")
key.Write(sessionID)

Expand Down
95 changes: 93 additions & 2 deletions providers/redis/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,99 @@ type Config struct {
Limiter redis.Limiter
}

// FailoverConfig provider settings.
type FailoverConfig struct {
// Key prefix
KeyPrefix string

// Optional username.
Username string

// Optional password. Must match the password specified in the
// requirepass server configuration option.
Password string

// Database to be selected after connecting to the server.
DB int

// The sentinel master name.
MasterName string

// The sentinel nodes seed list (host:port).
SentinelAddrs []string

// The password for the sentinel connection if required (different to username/password).
SentinelPassword string

// Routes read-only commands to the closest node. Only relevant with NewFailoverCluster.
RouteByLatency bool

// Routes read-only commands in random order. Only relevant with NewFailoverCluster.
RouteRandomly bool

// Route read-only commands to slave nodes.
SlaveOnly bool

// Maximum number of retries before giving up.
// Default is to not retry failed commands.
MaxRetries int

// Minimum backoff between each retry.
// Default is 8 milliseconds; -1 disables backoff.
MinRetryBackoff time.Duration

// Maximum backoff between each retry.
// Default is 512 milliseconds; -1 disables backoff.
MaxRetryBackoff time.Duration

// Dial timeout for establishing new connections.
// Default is 5 seconds.
DialTimeout time.Duration

// Timeout for socket reads. If reached, commands will fail
// with a timeout instead of blocking. Use value -1 for no timeout and 0 for default.
// Default is 3 seconds.
ReadTimeout time.Duration

// Timeout for socket writes. If reached, commands will fail
// with a timeout instead of blocking.
// Default is ReadTimeout.
WriteTimeout time.Duration

// Maximum number of socket connections.
// Default is 10 connections per every CPU as reported by runtime.NumCPU.
PoolSize int

// Minimum number of idle connections which is useful when establishing
// new connection is slow.
MinIdleConns int

// Connection age at which client retires (closes) the connection.
// Default is to not close aged connections.
MaxConnAge time.Duration

// Amount of time client waits for connection if all connections
// are busy before returning an error.
// Default is ReadTimeout + 1 second.
PoolTimeout time.Duration

// Amount of time after which client closes idle connections.
// Should be less than server's timeout.
// Default is 5 minutes. -1 disables idle timeout check.
IdleTimeout time.Duration

// Frequency of idle checks made by idle connections reaper.
// Default is 1 minute. -1 disables idle connections reaper,
// but idle connections are still discarded by the client
// if IdleTimeout is set.
IdleCheckFrequency time.Duration

// TLS Config to use. When set TLS will be negotiated.
TLSConfig *tls.Config
}

// Provider backend manager
type Provider struct {
config Config
db *redis.Client
keyPrefix string
db redis.Cmdable
}

0 comments on commit e884ba3

Please sign in to comment.