1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-22 19:49:00 +03:00

feat(options): add dial retry backoff function. (#3706)

* add dial retry backoff function.

* add readme

* add backoff to wordlist

* add logical consistency check

* fix(cluster): propagate DialerRetryBackoff to per-node clients

ClusterOptions.clientOptions() was not propagating DialerRetryBackoff,
causing the backoff function to be silently ignored when set on a cluster
client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthew Hooker
2026-03-19 11:45:20 -07:00
committed by GitHub
parent 9aebc97b42
commit 7c6b09e297
9 changed files with 139 additions and 4 deletions

View File

@@ -77,3 +77,4 @@ entraid
MiB
KiB
oldstable
backoff

View File

@@ -135,6 +135,29 @@ func ExampleClient() {
}
```
### Dial retries and backoff
Connection establishment can be retried by the connection pool when dialing fails.
- **`DialerRetries`**: maximum number of dial attempts (default: 5).
- **`DialerRetryTimeout`**: default delay between attempts when no custom backoff is provided (default: 100ms).
- **`DialerRetryBackoff`**: optional function hook to control the delay between attempts.
Example:
```go
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
DialerRetries: 5,
DialerRetryTimeout: 100 * time.Millisecond, // used when DialerRetryBackoff is nil
// Optional: exponential backoff with jitter and a cap.
DialerRetryBackoff: redis.DialRetryBackoffExponential(100*time.Millisecond, 2*time.Second),
})
defer rdb.Close()
```
### Authentication
The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options:

39
dial_retry_backoff.go Normal file
View File

@@ -0,0 +1,39 @@
package redis
import (
"time"
"github.com/redis/go-redis/v9/internal"
)
// DialRetryBackoffConstant returns a dial retry backoff function that always returns d.
// attempt is 0-based: attempt=0 is the delay after the 1st failed dial.
func DialRetryBackoffConstant(d time.Duration) func(attempt int) time.Duration {
if d < 0 {
d = 0
}
return func(int) time.Duration { return d }
}
// DialRetryBackoffExponential returns a dial retry backoff function that uses exponential
// backoff with jitter and a cap, using internal.RetryBackoff.
//
// attempt is 0-based: attempt=0 is the delay after the 1st failed dial.
func DialRetryBackoffExponential(minBackoff, maxBackoff time.Duration) func(attempt int) time.Duration {
if minBackoff < 0 {
minBackoff = 0
}
if maxBackoff < 0 {
maxBackoff = 0
}
if minBackoff > maxBackoff {
minBackoff = maxBackoff
}
return func(attempt int) time.Duration {
// internal.RetryBackoff expects retry >= 0.
if attempt < 0 {
attempt = 0
}
return internal.RetryBackoff(attempt, minBackoff, maxBackoff)
}
}

View File

@@ -0,0 +1,29 @@
package pool
import (
"testing"
"time"
)
func TestDialRetryBackoff_ConstantByDefault_ExponentialWhenMaxSet(t *testing.T) {
p := &ConnPool{cfg: &Options{DialerRetryTimeout: 10 * time.Millisecond}}
// default constant when max not set
for i := 0; i < 5; i++ {
if got := p.dialRetryBackoff(i); got != 10*time.Millisecond {
t.Fatalf("constant: attempt=%d got %v", i, got)
}
}
// custom function
p.cfg.DialerRetryBackoff = func(attempt int) time.Duration {
return time.Duration(attempt+1) * time.Millisecond
}
for i := 0; i < 5; i++ {
got := p.dialRetryBackoff(i)
want := time.Duration(i+1) * time.Millisecond
if got != want {
t.Fatalf("custom: attempt=%d got %v, want %v", i, got, want)
}
}
}

View File

@@ -326,6 +326,10 @@ type Options struct {
// Default: 100ms
DialerRetryTimeout time.Duration
// DialerRetryBackoff controls the delay between dial retry attempts.
// If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms).
DialerRetryBackoff func(attempt int) time.Duration
// Name is a unique identifier for this pool, used in metrics.
// Format: addr_uniqueID (e.g., "localhost:6379_a1b2c3d4")
Name string
@@ -605,10 +609,6 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
if maxRetries <= 0 {
maxRetries = 5 // Default value
}
backoffDuration := p.cfg.DialerRetryTimeout
if backoffDuration <= 0 {
backoffDuration = 100 * time.Millisecond // Default value
}
var lastErr error
shouldLoop := true
@@ -636,6 +636,7 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
// (not for the first attempt, do at least one)
// Do not sleep after the last attempt.
if attempt+1 < maxRetries {
backoffDuration := p.dialRetryBackoff(attempt)
select {
case <-ctx.Done():
shouldLoop = false
@@ -668,6 +669,22 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
return nil, lastErr
}
func (p *ConnPool) dialRetryBackoff(attempt int) time.Duration {
if p.cfg.DialerRetryBackoff != nil {
d := p.cfg.DialerRetryBackoff(attempt)
if d < 0 {
return 0
}
return d
}
base := p.cfg.DialerRetryTimeout
if base <= 0 {
base = 100 * time.Millisecond
}
return base
}
// calcConnExpiresAt calculates the expiration time for a connection.
// It applies random jitter to prevent all connections from expiring simultaneously,
// avoiding the "thundering herd" problem where all connections expire at once.

View File

@@ -143,6 +143,13 @@ type Options struct {
// default: 100 milliseconds
DialerRetryTimeout time.Duration
// DialerRetryBackoff controls the delay between dial retry attempts.
//
// attempt is 0-based: attempt=0 is the delay after the 1st failed dial (before the 2nd attempt).
//
// If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms).
DialerRetryBackoff func(attempt int) time.Duration
// ReadTimeout for socket reads. If reached, commands will fail
// with a timeout instead of blocking. Supported values:
//
@@ -755,6 +762,7 @@ func newConnPool(
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
MinIdleConns: minIdleConns,
MaxIdleConns: maxIdleConns,
MaxActiveConns: maxActiveConns,
@@ -801,6 +809,7 @@ func newPubSubPool(
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
MinIdleConns: minIdleConns,
MaxIdleConns: maxIdleConns,
MaxActiveConns: maxActiveConns,

View File

@@ -104,6 +104,10 @@ type ClusterOptions struct {
// default: 100 milliseconds
DialerRetryTimeout time.Duration
// DialerRetryBackoff controls the delay between dial retry attempts.
// See Options.DialerRetryBackoff for details.
DialerRetryBackoff func(attempt int) time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
@@ -429,6 +433,7 @@ func (opt *ClusterOptions) clientOptions() *Options {
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,

View File

@@ -120,6 +120,10 @@ type RingOptions struct {
// default: 100 milliseconds
DialerRetryTimeout time.Duration
// DialerRetryBackoff controls the delay between dial retry attempts.
// See Options.DialerRetryBackoff for details.
DialerRetryBackoff func(attempt int) time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
@@ -233,6 +237,7 @@ func (opt *RingOptions) clientOptions() *Options {
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,

View File

@@ -100,6 +100,10 @@ type FailoverOptions struct {
// default: 100 milliseconds
DialerRetryTimeout time.Duration
// DialerRetryBackoff controls the delay between dial retry attempts.
// See Options.DialerRetryBackoff for details.
DialerRetryBackoff func(attempt int) time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
@@ -197,6 +201,7 @@ func (opt *FailoverOptions) clientOptions() *Options {
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,
@@ -251,6 +256,7 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,
@@ -311,6 +317,7 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
DialTimeout: opt.DialTimeout,
DialerRetries: opt.DialerRetries,
DialerRetryTimeout: opt.DialerRetryTimeout,
DialerRetryBackoff: opt.DialerRetryBackoff,
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,