Skip to content

Commit 220740b

Browse files
authored
Support retriable func condition (#687)
* Support retriable func condition * Refactor WithCodes option
1 parent f6f8eae commit 220740b

File tree

3 files changed

+54
-12
lines changed

3 files changed

+54
-12
lines changed

interceptors/retry/options.go

+31-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"google.golang.org/grpc"
1111
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
1213
)
1314

1415
var (
@@ -22,11 +23,11 @@ var (
2223
max: 0, // disabled
2324
perCallTimeout: 0, // disabled
2425
includeHeader: true,
25-
codes: DefaultRetriableCodes,
2626
backoffFunc: BackoffLinearWithJitter(50*time.Millisecond /*jitter*/, 0.10),
2727
onRetryCallback: OnRetryCallback(func(ctx context.Context, attempt uint, err error) {
2828
logTrace(ctx, "grpc_retry attempt: %d, backoff for %v", attempt, err)
2929
}),
30+
retriableFunc: newRetriableFuncForCodes(DefaultRetriableCodes),
3031
}
3132
)
3233

@@ -41,6 +42,9 @@ type BackoffFunc func(ctx context.Context, attempt uint) time.Duration
4142
// OnRetryCallback is the type of function called when a retry occurs.
4243
type OnRetryCallback func(ctx context.Context, attempt uint, err error)
4344

45+
// RetriableFunc denotes a family of functions that control which error should be retried.
46+
type RetriableFunc func(err error) bool
47+
4448
// Disable disables the retry behaviour on this call, or this interceptor.
4549
//
4650
// Its semantically the same to `WithMax`
@@ -78,7 +82,7 @@ func WithOnRetryCallback(fn OnRetryCallback) CallOption {
7882
// You cannot automatically retry on Cancelled and Deadline, please use `WithPerRetryTimeout` for these.
7983
func WithCodes(retryCodes ...codes.Code) CallOption {
8084
return CallOption{applyFunc: func(o *options) {
81-
o.codes = retryCodes
85+
o.retriableFunc = newRetriableFuncForCodes(retryCodes)
8286
}}
8387
}
8488

@@ -100,13 +104,20 @@ func WithPerRetryTimeout(timeout time.Duration) CallOption {
100104
}}
101105
}
102106

107+
// WithRetriable sets which error should be retried.
108+
func WithRetriable(retriableFunc RetriableFunc) CallOption {
109+
return CallOption{applyFunc: func(o *options) {
110+
o.retriableFunc = retriableFunc
111+
}}
112+
}
113+
103114
type options struct {
104115
max uint
105116
perCallTimeout time.Duration
106117
includeHeader bool
107-
codes []codes.Code
108118
backoffFunc BackoffFunc
109119
onRetryCallback OnRetryCallback
120+
retriableFunc RetriableFunc
110121
}
111122

112123
// CallOption is a grpc.CallOption that is local to grpc_retry.
@@ -137,3 +148,20 @@ func filterCallOptions(callOptions []grpc.CallOption) (grpcOptions []grpc.CallOp
137148
}
138149
return grpcOptions, retryOptions
139150
}
151+
152+
// newRetriableFuncForCodes returns retriable function for specific Codes.
153+
func newRetriableFuncForCodes(codes []codes.Code) func(err error) bool {
154+
return func(err error) bool {
155+
errCode := status.Code(err)
156+
if isContextError(err) {
157+
// context errors are not retriable based on user settings.
158+
return false
159+
}
160+
for _, code := range codes {
161+
if code == errCode {
162+
return true
163+
}
164+
}
165+
return false
166+
}
167+
}

interceptors/retry/retry.go

+2-9
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,8 @@ func waitRetryBackoff(attempt uint, parentCtx context.Context, callOpts *options
267267
}
268268

269269
func isRetriable(err error, callOpts *options) bool {
270-
errCode := status.Code(err)
271-
if isContextError(err) {
272-
// context errors are not retriable based on user settings.
273-
return false
274-
}
275-
for _, code := range callOpts.codes {
276-
if code == errCode {
277-
return true
278-
}
270+
if callOpts.retriableFunc != nil {
271+
return callOpts.retriableFunc(err)
279272
}
280273
return false
281274
}

interceptors/retry/retry_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package retry
66
import (
77
"context"
88
"io"
9+
"strings"
910
"sync"
1011
"testing"
1112
"time"
@@ -178,6 +179,16 @@ func (s *RetrySuite) TestUnary_OverrideFromDialOpts() {
178179
require.EqualValues(s.T(), 5, s.srv.requestCount(), "five requests should have been made")
179180
}
180181

182+
func (s *RetrySuite) TestUnary_OverrideFromDialOpts2() {
183+
s.srv.resetFailingConfiguration(5, codes.ResourceExhausted, noSleep) // default is 3 and retriable_errors
184+
out, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, WithRetriable(func(err error) bool {
185+
return strings.Contains(err.Error(), "maybeFailRequest")
186+
}), WithMax(5))
187+
require.NoError(s.T(), err, "the fifth invocation should succeed")
188+
require.NotNil(s.T(), out, "Pong must be not nil")
189+
require.EqualValues(s.T(), 5, s.srv.requestCount(), "five requests should have been made")
190+
}
191+
181192
func (s *RetrySuite) TestUnary_OnRetryCallbackCalled() {
182193
retryCallbackCount := 0
183194

@@ -209,6 +220,16 @@ func (s *RetrySuite) TestServerStream_OverrideFromContext() {
209220
require.EqualValues(s.T(), 5, s.srv.requestCount(), "three requests should have been made")
210221
}
211222

223+
func (s *RetrySuite) TestServerStream_OverrideFromContext2() {
224+
s.srv.resetFailingConfiguration(5, codes.ResourceExhausted, noSleep) // default is 3 and retriable_errors
225+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, WithRetriable(func(err error) bool {
226+
return strings.Contains(err.Error(), "maybeFailRequest")
227+
}), WithMax(5))
228+
require.NoError(s.T(), err, "establishing the connection must always succeed")
229+
s.assertPingListWasCorrect(stream)
230+
require.EqualValues(s.T(), 5, s.srv.requestCount(), "three requests should have been made")
231+
}
232+
212233
func (s *RetrySuite) TestServerStream_OnRetryCallbackCalled() {
213234
retryCallbackCount := 0
214235

0 commit comments

Comments
 (0)