From 4f09082f6b31d4b0d0d87067d067e0bab3c1cf55 Mon Sep 17 00:00:00 2001
From: Monkey <golang@88.com>
Date: Fri, 14 Mar 2025 16:05:22 +0800
Subject: [PATCH] fix: connection pool timeout, increase retries (#3298)

* fix: connection pool timeout, increase retries

Signed-off-by: monkey <golang@88.com>

* fix: add shouldRetry test

Signed-off-by: monkey <golang@88.com>

---------

Signed-off-by: monkey <golang@88.com>
Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>
---
 error.go       |  3 +++
 error_test.go  | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++
 export_test.go |  6 +++++
 3 files changed, 74 insertions(+)
 create mode 100644 error_test.go

diff --git a/error.go b/error.go
index a7bf159c..ec2224c0 100644
--- a/error.go
+++ b/error.go
@@ -53,6 +53,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
 		return true
 	case nil, context.Canceled, context.DeadlineExceeded:
 		return false
+	case pool.ErrPoolTimeout:
+		// connection pool timeout, increase retries. #3289
+		return true
 	}
 
 	if v, ok := err.(timeoutError); ok {
diff --git a/error_test.go b/error_test.go
new file mode 100644
index 00000000..da9a471a
--- /dev/null
+++ b/error_test.go
@@ -0,0 +1,65 @@
+package redis_test
+
+import (
+	"context"
+	"errors"
+	"io"
+
+	. "github.com/bsm/ginkgo/v2"
+	. "github.com/bsm/gomega"
+	"github.com/redis/go-redis/v9"
+)
+
+type testTimeout struct {
+	timeout bool
+}
+
+func (t testTimeout) Timeout() bool {
+	return t.timeout
+}
+
+func (t testTimeout) Error() string {
+	return "test timeout"
+}
+
+var _ = Describe("error", func() {
+	BeforeEach(func() {
+
+	})
+
+	AfterEach(func() {
+
+	})
+
+	It("should retry", func() {
+		data := map[error]bool{
+			io.EOF:                   true,
+			io.ErrUnexpectedEOF:      true,
+			nil:                      false,
+			context.Canceled:         false,
+			context.DeadlineExceeded: false,
+			redis.ErrPoolTimeout:     true,
+			errors.New("ERR max number of clients reached"):                      true,
+			errors.New("LOADING Redis is loading the dataset in memory"):         true,
+			errors.New("READONLY You can't write against a read only replica"):   true,
+			errors.New("CLUSTERDOWN The cluster is down"):                        true,
+			errors.New("TRYAGAIN Command cannot be processed, please try again"): true,
+			errors.New("other"): false,
+		}
+
+		for err, expected := range data {
+			Expect(redis.ShouldRetry(err, false)).To(Equal(expected))
+			Expect(redis.ShouldRetry(err, true)).To(Equal(expected))
+		}
+	})
+
+	It("should retry timeout", func() {
+		t1 := testTimeout{timeout: true}
+		Expect(redis.ShouldRetry(t1, true)).To(Equal(true))
+		Expect(redis.ShouldRetry(t1, false)).To(Equal(false))
+
+		t2 := testTimeout{timeout: false}
+		Expect(redis.ShouldRetry(t2, true)).To(Equal(true))
+		Expect(redis.ShouldRetry(t2, false)).To(Equal(true))
+	})
+})
diff --git a/export_test.go b/export_test.go
index 3f92983d..10d8f23c 100644
--- a/export_test.go
+++ b/export_test.go
@@ -11,6 +11,8 @@ import (
 	"github.com/redis/go-redis/v9/internal/pool"
 )
 
+var ErrPoolTimeout = pool.ErrPoolTimeout
+
 func (c *baseClient) Pool() pool.Pooler {
 	return c.connPool
 }
@@ -102,3 +104,7 @@ func (c *Ring) ShardByName(name string) *ringShard {
 func (c *ModuleLoadexConfig) ToArgs() []interface{} {
 	return c.toArgs()
 }
+
+func ShouldRetry(err error, retryTimeout bool) bool {
+	return shouldRetry(err, retryTimeout)
+}