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:
1
.github/wordlist.txt
vendored
1
.github/wordlist.txt
vendored
@@ -77,3 +77,4 @@ entraid
|
||||
MiB
|
||||
KiB
|
||||
oldstable
|
||||
backoff
|
||||
|
||||
23
README.md
23
README.md
@@ -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
39
dial_retry_backoff.go
Normal 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)
|
||||
}
|
||||
}
|
||||
29
internal/pool/dial_retry_backoff_test.go
Normal file
29
internal/pool/dial_retry_backoff_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
5
ring.go
5
ring.go
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user