Skip to content

Commit 8c67e00

Browse files
committed
Add auto-retry and MaxRetries option. Fixes #84.
1 parent 2507be6 commit 8c67e00

File tree

9 files changed

+134
-50
lines changed

9 files changed

+134
-50
lines changed

command.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ func setCmdsErr(cmds []Cmder, e error) {
4747
}
4848
}
4949

50+
func resetCmds(cmds []Cmder) {
51+
for _, cmd := range cmds {
52+
cmd.reset()
53+
}
54+
}
55+
5056
func cmdString(cmd Cmder, val interface{}) string {
5157
s := strings.Join(cmd.args(), " ")
5258
if err := cmd.Err(); err != nil {

conn.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ import (
77
"gopkg.in/bufio.v1"
88
)
99

10+
var (
11+
zeroTime = time.Time{}
12+
)
13+
1014
type conn struct {
1115
netcn net.Conn
1216
rd *bufio.Reader
1317
buf []byte
1418

1519
usedAt time.Time
16-
readTimeout time.Duration
17-
writeTimeout time.Duration
20+
ReadTimeout time.Duration
21+
WriteTimeout time.Duration
1822
}
1923

2024
func newConnDialer(opt *options) func() (*conn, error) {
@@ -70,17 +74,17 @@ func (cn *conn) writeCmds(cmds ...Cmder) error {
7074
}
7175

7276
func (cn *conn) Read(b []byte) (int, error) {
73-
if cn.readTimeout != 0 {
74-
cn.netcn.SetReadDeadline(time.Now().Add(cn.readTimeout))
77+
if cn.ReadTimeout != 0 {
78+
cn.netcn.SetReadDeadline(time.Now().Add(cn.ReadTimeout))
7579
} else {
7680
cn.netcn.SetReadDeadline(zeroTime)
7781
}
7882
return cn.netcn.Read(b)
7983
}
8084

8185
func (cn *conn) Write(b []byte) (int, error) {
82-
if cn.writeTimeout != 0 {
83-
cn.netcn.SetWriteDeadline(time.Now().Add(cn.writeTimeout))
86+
if cn.WriteTimeout != 0 {
87+
cn.netcn.SetWriteDeadline(time.Now().Add(cn.WriteTimeout))
8488
} else {
8589
cn.netcn.SetWriteDeadline(zeroTime)
8690
}

error.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (err redisError) Error() string {
2626
}
2727

2828
func isNetworkError(err error) bool {
29-
if _, ok := err.(*net.OpError); ok || err == io.EOF {
29+
if _, ok := err.(net.Error); ok || err == io.EOF {
3030
return true
3131
}
3232
return false
@@ -53,3 +53,11 @@ func isMovedError(err error) (moved bool, ask bool, addr string) {
5353

5454
return
5555
}
56+
57+
// shouldRetry reports whether failed command should be retried.
58+
func shouldRetry(err error) bool {
59+
if err == nil {
60+
return false
61+
}
62+
return isNetworkError(err)
63+
}

export_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package redis
22

3+
import "net"
4+
35
func (c *baseClient) Pool() pool {
46
return c.connPool
57
}
68

9+
func (cn *conn) SetNetConn(netcn net.Conn) {
10+
cn.netcn = netcn
11+
}
12+
713
func HashSlot(key string) int {
814
return hashSlot(key)
915
}

pipeline.go

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,38 @@ func (c *Pipeline) Discard() error {
5050

5151
// Exec always returns list of commands and error of the first failed
5252
// command if any.
53-
func (c *Pipeline) Exec() ([]Cmder, error) {
53+
func (c *Pipeline) Exec() (cmds []Cmder, retErr error) {
5454
if c.closed {
5555
return nil, errClosed
5656
}
5757
if len(c.cmds) == 0 {
58-
return []Cmder{}, nil
58+
return c.cmds, nil
5959
}
6060

61-
cmds := c.cmds
61+
cmds = c.cmds
6262
c.cmds = make([]Cmder, 0, 0)
6363

64-
cn, err := c.client.conn()
65-
if err != nil {
66-
setCmdsErr(cmds, err)
67-
return cmds, err
64+
for i := 0; i <= c.client.opt.MaxRetries; i++ {
65+
if i > 0 {
66+
resetCmds(cmds)
67+
}
68+
69+
cn, err := c.client.conn()
70+
if err != nil {
71+
setCmdsErr(cmds, err)
72+
return cmds, err
73+
}
74+
75+
retErr = c.execCmds(cn, cmds)
76+
c.client.putConn(cn, err)
77+
if shouldRetry(err) {
78+
continue
79+
}
80+
81+
break
6882
}
6983

70-
err = c.execCmds(cn, cmds)
71-
c.client.putConn(cn, err)
72-
return cmds, err
84+
return cmds, retErr
7385
}
7486

7587
func (c *Pipeline) execCmds(cn *conn, cmds []Cmder) error {
@@ -79,17 +91,11 @@ func (c *Pipeline) execCmds(cn *conn, cmds []Cmder) error {
7991
}
8092

8193
var firstCmdErr error
82-
for i, cmd := range cmds {
94+
for _, cmd := range cmds {
8395
err := cmd.parseReply(cn.rd)
84-
if err == nil {
85-
continue
86-
}
87-
if firstCmdErr == nil {
96+
if err != nil && firstCmdErr == nil {
8897
firstCmdErr = err
8998
}
90-
if isNetworkError(err) {
91-
setCmdsErr(cmds[i:], err)
92-
}
9399
}
94100

95101
return firstCmdErr

pool.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ var (
1616
errPoolTimeout = errors.New("redis: connection pool timeout")
1717
)
1818

19-
var (
20-
zeroTime = time.Time{}
21-
)
22-
2319
type pool interface {
2420
First() *conn
2521
Get() (*conn, error)

pubsub.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (c *PubSub) ReceiveTimeout(timeout time.Duration) (interface{}, error) {
6363
if err != nil {
6464
return nil, err
6565
}
66-
cn.readTimeout = timeout
66+
cn.ReadTimeout = timeout
6767

6868
cmd := NewSliceCmd()
6969
if err := cmd.parseReply(cn.rd); err != nil {
@@ -92,6 +92,7 @@ func (c *PubSub) ReceiveTimeout(timeout time.Duration) (interface{}, error) {
9292
Payload: reply[3].(string),
9393
}, nil
9494
}
95+
9596
return nil, fmt.Errorf("redis: unsupported message name: %q", msgName)
9697
}
9798

redis.go

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,46 @@ func (c *baseClient) putConn(cn *conn, ei error) {
3232
}
3333

3434
func (c *baseClient) process(cmd Cmder) {
35-
cn, err := c.conn()
36-
if err != nil {
37-
cmd.setErr(err)
38-
return
39-
}
35+
for i := 0; i <= c.opt.MaxRetries; i++ {
36+
if i > 0 {
37+
cmd.reset()
38+
}
4039

41-
if timeout := cmd.writeTimeout(); timeout != nil {
42-
cn.writeTimeout = *timeout
43-
} else {
44-
cn.writeTimeout = c.opt.WriteTimeout
45-
}
40+
cn, err := c.conn()
41+
if err != nil {
42+
cmd.setErr(err)
43+
return
44+
}
4645

47-
if timeout := cmd.readTimeout(); timeout != nil {
48-
cn.readTimeout = *timeout
49-
} else {
50-
cn.readTimeout = c.opt.ReadTimeout
51-
}
46+
if timeout := cmd.writeTimeout(); timeout != nil {
47+
cn.WriteTimeout = *timeout
48+
} else {
49+
cn.WriteTimeout = c.opt.WriteTimeout
50+
}
5251

53-
if err := cn.writeCmds(cmd); err != nil {
52+
if timeout := cmd.readTimeout(); timeout != nil {
53+
cn.ReadTimeout = *timeout
54+
} else {
55+
cn.ReadTimeout = c.opt.ReadTimeout
56+
}
57+
58+
if err := cn.writeCmds(cmd); err != nil {
59+
c.putConn(cn, err)
60+
cmd.setErr(err)
61+
if shouldRetry(err) {
62+
continue
63+
}
64+
return
65+
}
66+
67+
err = cmd.parseReply(cn.rd)
5468
c.putConn(cn, err)
55-
cmd.setErr(err)
69+
if shouldRetry(err) {
70+
continue
71+
}
72+
5673
return
5774
}
58-
59-
err = cmd.parseReply(cn.rd)
60-
c.putConn(cn, err)
6175
}
6276

6377
// Close closes the client, releasing any open resources.
@@ -105,6 +119,10 @@ type Options struct {
105119
// than specified in this option.
106120
// Default: 0 = no eviction
107121
IdleTimeout time.Duration
122+
123+
// MaxRetries specifies maximum number of times client will retry
124+
// failed command. Default is to not retry failed command.
125+
MaxRetries int
108126
}
109127

110128
func (opt *Options) getDialer() func() (net.Conn, error) {
@@ -157,6 +175,8 @@ func (opt *Options) options() *options {
157175
DialTimeout: opt.getDialTimeout(),
158176
ReadTimeout: opt.ReadTimeout,
159177
WriteTimeout: opt.WriteTimeout,
178+
179+
MaxRetries: opt.MaxRetries,
160180
}
161181
}
162182

@@ -172,6 +192,8 @@ type options struct {
172192
DialTimeout time.Duration
173193
ReadTimeout time.Duration
174194
WriteTimeout time.Duration
195+
196+
MaxRetries int
175197
}
176198

177199
func (opt *options) connPoolOptions() *connPoolOptions {

redis_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,23 @@ var _ = Describe("Client", func() {
124124
Expect(db1.FlushDb().Err()).NotTo(HaveOccurred())
125125
})
126126

127+
It("should retry command on network error", func() {
128+
Expect(client.Close()).NotTo(HaveOccurred())
129+
130+
client = redis.NewClient(&redis.Options{
131+
Addr: redisAddr,
132+
MaxRetries: 1,
133+
})
134+
135+
// Put bad connection in the pool.
136+
cn, err := client.Pool().Get()
137+
Expect(err).NotTo(HaveOccurred())
138+
cn.SetNetConn(newBadNetConn())
139+
Expect(client.Pool().Put(cn)).NotTo(HaveOccurred())
140+
141+
err = client.Ping().Err()
142+
Expect(err).NotTo(HaveOccurred())
143+
})
127144
})
128145

129146
//------------------------------------------------------------------------------
@@ -266,6 +283,24 @@ func BenchmarkPipeline(b *testing.B) {
266283

267284
//------------------------------------------------------------------------------
268285

286+
type badNetConn struct {
287+
net.TCPConn
288+
}
289+
290+
var _ net.Conn = &badNetConn{}
291+
292+
func newBadNetConn() net.Conn {
293+
return &badNetConn{}
294+
}
295+
296+
func (badNetConn) Read([]byte) (int, error) {
297+
return 0, net.UnknownNetworkError("badNetConn")
298+
}
299+
300+
func (badNetConn) Write([]byte) (int, error) {
301+
return 0, net.UnknownNetworkError("badNetConn")
302+
}
303+
269304
// Replaces ginkgo's Eventually.
270305
func waitForSubstring(fn func() string, substr string, timeout time.Duration) error {
271306
var s string

0 commit comments

Comments
 (0)