Skip to content

Commit 7d56e76

Browse files
Remove backoffutils and added the files to retry package (#390)
* removed backoffutils and added the files to retry package Signed-off-by: Yash Sharma <[email protected]> * linting changes Signed-off-by: Yash Sharma <[email protected]> * move the jitter up function Signed-off-by: Yash Sharma <[email protected]> * renamed exponentbase2 to private Signed-off-by: Yash Sharma <[email protected]> * renamed the test package to retry, making it internal Signed-off-by: Yash Sharma <[email protected]>
1 parent 5b8ad84 commit 7d56e76

File tree

4 files changed

+101
-57
lines changed

4 files changed

+101
-57
lines changed

interceptors/retry/backoff.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
package retry
55

66
import (
7+
"math/rand"
78
"time"
8-
9-
"github.com/grpc-ecosystem/go-grpc-middleware/v2/util/backoffutils"
109
)
1110

1211
// BackoffLinear is very simple: it waits for a fixed period of time between calls.
@@ -16,29 +15,40 @@ func BackoffLinear(waitBetween time.Duration) BackoffFunc {
1615
}
1716
}
1817

18+
// jitterUp adds random jitter to the duration.
19+
// This adds or subtracts time from the duration within a given jitter fraction.
20+
// For example for 10s and jitter 0.1, it will return a time within [9s, 11s])
21+
func jitterUp(duration time.Duration, jitter float64) time.Duration {
22+
multiplier := jitter * (rand.Float64()*2 - 1)
23+
return time.Duration(float64(duration) * (1 + multiplier))
24+
}
25+
26+
// exponentBase2 computes 2^(a-1) where a >= 1. If a is 0, the result is 0.
27+
func exponentBase2(a uint) uint {
28+
return (1 << a) >> 1
29+
}
30+
1931
// BackoffLinearWithJitter waits a set period of time, allowing for jitter (fractional adjustment).
20-
//
2132
// For example waitBetween=1s and jitter=0.10 can generate waits between 900ms and 1100ms.
2233
func BackoffLinearWithJitter(waitBetween time.Duration, jitterFraction float64) BackoffFunc {
2334
return func(attempt uint) time.Duration {
24-
return backoffutils.JitterUp(waitBetween, jitterFraction)
35+
return jitterUp(waitBetween, jitterFraction)
2536
}
2637
}
2738

2839
// BackoffExponential produces increasing intervals for each attempt.
29-
//
3040
// The scalar is multiplied times 2 raised to the current attempt. So the first
3141
// retry with a scalar of 100ms is 100ms, while the 5th attempt would be 1.6s.
3242
func BackoffExponential(scalar time.Duration) BackoffFunc {
3343
return func(attempt uint) time.Duration {
34-
return scalar * time.Duration(backoffutils.ExponentBase2(attempt))
44+
return scalar * time.Duration(exponentBase2(attempt))
3545
}
3646
}
3747

3848
// BackoffExponentialWithJitter creates an exponential backoff like
3949
// BackoffExponential does, but adds jitter.
4050
func BackoffExponentialWithJitter(scalar time.Duration, jitterFraction float64) BackoffFunc {
4151
return func(attempt uint) time.Duration {
42-
return backoffutils.JitterUp(scalar*time.Duration(backoffutils.ExponentBase2(attempt)), jitterFraction)
52+
return jitterUp(scalar*time.Duration(exponentBase2(attempt)), jitterFraction)
4353
}
4454
}

interceptors/retry/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the Apache License 2.0.
33

44
/*
5-
`retry` provides client-side request retry logic for gRPC.
5+
Package retry provides client-side request retry logic for gRPC.
66
77
Client-Side Request Retry Interceptor
88

interceptors/retry/examples_test.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) The go-grpc-middleware Authors.
22
// Licensed under the Apache License 2.0.
33

4-
package retry_test
4+
package retry
55

66
import (
77
"context"
@@ -11,7 +11,6 @@ import (
1111
"google.golang.org/grpc"
1212
"google.golang.org/grpc/codes"
1313

14-
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
1514
"github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb"
1615
)
1716

@@ -20,33 +19,33 @@ var cc *grpc.ClientConn
2019
// Simple example of using the default interceptor configuration.
2120
func Example_initialization() {
2221
_, _ = grpc.Dial("myservice.example.com",
23-
grpc.WithStreamInterceptor(retry.StreamClientInterceptor()),
24-
grpc.WithUnaryInterceptor(retry.UnaryClientInterceptor()),
22+
grpc.WithStreamInterceptor(StreamClientInterceptor()),
23+
grpc.WithUnaryInterceptor(UnaryClientInterceptor()),
2524
)
2625
}
2726

2827
// Complex example with a 100ms linear backoff interval, and retry only on NotFound and Unavailable.
2928
func Example_initializationWithOptions() {
30-
opts := []retry.CallOption{
31-
retry.WithBackoff(retry.BackoffLinear(100 * time.Millisecond)),
32-
retry.WithCodes(codes.NotFound, codes.Aborted),
29+
opts := []CallOption{
30+
WithBackoff(BackoffLinear(100 * time.Millisecond)),
31+
WithCodes(codes.NotFound, codes.Aborted),
3332
}
3433
_, _ = grpc.Dial("myservice.example.com",
35-
grpc.WithStreamInterceptor(retry.StreamClientInterceptor(opts...)),
36-
grpc.WithUnaryInterceptor(retry.UnaryClientInterceptor(opts...)),
34+
grpc.WithStreamInterceptor(StreamClientInterceptor(opts...)),
35+
grpc.WithUnaryInterceptor(UnaryClientInterceptor(opts...)),
3736
)
3837
}
3938

4039
// Example with an exponential backoff starting with 100ms.
4140
//
4241
// Each next interval is the previous interval multiplied by 2.
4342
func Example_initializationWithExponentialBackoff() {
44-
opts := []retry.CallOption{
45-
retry.WithBackoff(retry.BackoffExponential(100 * time.Millisecond)),
43+
opts := []CallOption{
44+
WithBackoff(BackoffExponential(100 * time.Millisecond)),
4645
}
4746
_, _ = grpc.Dial("myservice.example.com",
48-
grpc.WithStreamInterceptor(retry.StreamClientInterceptor(opts...)),
49-
grpc.WithUnaryInterceptor(retry.UnaryClientInterceptor(opts...)),
47+
grpc.WithStreamInterceptor(StreamClientInterceptor(opts...)),
48+
grpc.WithUnaryInterceptor(UnaryClientInterceptor(opts...)),
5049
)
5150
}
5251

@@ -56,7 +55,7 @@ func Example_simpleCall() {
5655
defer cancel()
5756

5857
client := testpb.NewTestServiceClient(cc)
59-
stream, _ := client.PingList(ctx, &testpb.PingListRequest{}, retry.WithMax(3))
58+
stream, _ := client.PingList(ctx, &testpb.PingListRequest{}, WithMax(3))
6059

6160
for {
6261
_, err := stream.Recv() // retries happen here
@@ -85,6 +84,11 @@ func ExampleWithPerRetryTimeout() {
8584
_, _ = client.Ping(
8685
ctx,
8786
&testpb.PingRequest{},
88-
retry.WithMax(3),
89-
retry.WithPerRetryTimeout(1*time.Second))
87+
WithMax(3),
88+
WithPerRetryTimeout(1*time.Second))
89+
}
90+
91+
// Scale duration by a factor.
92+
func scaleDuration(d time.Duration, factor float64) time.Duration {
93+
return time.Duration(float64(d) * factor)
9094
}

interceptors/retry/retry_test.go

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) The go-grpc-middleware Authors.
22
// Licensed under the Apache License 2.0.
33

4-
package retry_test
4+
package retry
55

66
import (
77
"context"
@@ -18,7 +18,6 @@ import (
1818
"google.golang.org/grpc/status"
1919

2020
middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
21-
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
2221
"github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb"
2322
)
2423

@@ -94,15 +93,15 @@ func TestRetrySuite(t *testing.T) {
9493
service := &failingService{
9594
TestServiceServer: &testpb.TestPingService{T: t},
9695
}
97-
unaryInterceptor := retry.UnaryClientInterceptor(
98-
retry.WithCodes(retriableErrors...),
99-
retry.WithMax(3),
100-
retry.WithBackoff(retry.BackoffLinear(retryTimeout)),
96+
unaryInterceptor := UnaryClientInterceptor(
97+
WithCodes(retriableErrors...),
98+
WithMax(3),
99+
WithBackoff(BackoffLinear(retryTimeout)),
101100
)
102-
streamInterceptor := retry.StreamClientInterceptor(
103-
retry.WithCodes(retriableErrors...),
104-
retry.WithMax(3),
105-
retry.WithBackoff(retry.BackoffLinear(retryTimeout)),
101+
streamInterceptor := StreamClientInterceptor(
102+
WithCodes(retriableErrors...),
103+
WithMax(3),
104+
WithBackoff(BackoffLinear(retryTimeout)),
106105
)
107106
s := &RetrySuite{
108107
srv: service,
@@ -148,10 +147,10 @@ func (s *RetrySuite) TestCallOptionsDontPanicWithoutInterceptor() {
148147
s.srv.resetFailingConfiguration(100, codes.DataLoss, noSleep) // doesn't matter all requests should fail
149148
nonMiddlewareClient := s.NewClient()
150149
_, err := nonMiddlewareClient.Ping(s.SimpleCtx(), testpb.GoodPing,
151-
retry.WithMax(5),
152-
retry.WithBackoff(retry.BackoffLinear(1*time.Millisecond)),
153-
retry.WithCodes(codes.DataLoss),
154-
retry.WithPerRetryTimeout(1*time.Millisecond),
150+
WithMax(5),
151+
WithBackoff(BackoffLinear(1*time.Millisecond)),
152+
WithCodes(codes.DataLoss),
153+
WithPerRetryTimeout(1*time.Millisecond),
155154
)
156155
require.Error(s.T(), err)
157156
}
@@ -175,7 +174,7 @@ func (s *RetrySuite) TestUnary_SucceedsOnRetriableError() {
175174

176175
func (s *RetrySuite) TestUnary_OverrideFromDialOpts() {
177176
s.srv.resetFailingConfiguration(5, codes.ResourceExhausted, noSleep) // default is 3 and retriable_errors
178-
out, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, retry.WithCodes(codes.ResourceExhausted), retry.WithMax(5))
177+
out, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, WithCodes(codes.ResourceExhausted), WithMax(5))
179178
require.NoError(s.T(), err, "the fifth invocation should succeed")
180179
require.NotNil(s.T(), out, "Pong must be not nil")
181180
require.EqualValues(s.T(), 5, s.srv.requestCount(), "five requests should have been made")
@@ -188,8 +187,8 @@ func (s *RetrySuite) TestUnary_PerCallDeadline_Succeeds() {
188187
// a retry call with a 5 millisecond deadline. The 5th one doesn't sleep and succeeds.
189188
deadlinePerCall := 5 * time.Millisecond
190189
s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall)
191-
out, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, retry.WithPerRetryTimeout(deadlinePerCall),
192-
retry.WithMax(5))
190+
out, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, WithPerRetryTimeout(deadlinePerCall),
191+
WithMax(5))
193192
require.NoError(s.T(), err, "the fifth invocation should succeed")
194193
require.NotNil(s.T(), out, "Pong must be not nil")
195194
require.EqualValues(s.T(), 5, s.srv.requestCount(), "five requests should have been made")
@@ -209,8 +208,8 @@ func (s *RetrySuite) TestUnary_PerCallDeadline_FailsOnParent() {
209208
s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall)
210209
ctx, cancel := context.WithTimeout(context.TODO(), parentDeadline)
211210
defer cancel()
212-
_, err := s.Client.Ping(ctx, testpb.GoodPing, retry.WithPerRetryTimeout(deadlinePerCall),
213-
retry.WithMax(5))
211+
_, err := s.Client.Ping(ctx, testpb.GoodPing, WithPerRetryTimeout(deadlinePerCall),
212+
WithMax(5))
214213
require.Error(s.T(), err, "the retries must fail due to context deadline exceeded")
215214
require.Equal(s.T(), codes.DeadlineExceeded, status.Code(err), "failre code must be a gRPC error of Deadline class")
216215
}
@@ -220,7 +219,7 @@ func (s *RetrySuite) TestUnary_OnRetryCallbackCalled() {
220219

221220
s.srv.resetFailingConfiguration(3, codes.Unavailable, noSleep) // see retriable_errors
222221
out, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing,
223-
retry.WithOnRetryCallback(func(ctx context.Context, attempt uint, err error) {
222+
WithOnRetryCallback(func(ctx context.Context, attempt uint, err error) {
224223
retryCallbackCount++
225224
}),
226225
)
@@ -240,7 +239,7 @@ func (s *RetrySuite) TestServerStream_SucceedsOnRetriableError() {
240239

241240
func (s *RetrySuite) TestServerStream_OverrideFromContext() {
242241
s.srv.resetFailingConfiguration(5, codes.ResourceExhausted, noSleep) // default is 3 and retriable_errors
243-
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, retry.WithCodes(codes.ResourceExhausted), retry.WithMax(5))
242+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, WithCodes(codes.ResourceExhausted), WithMax(5))
244243
require.NoError(s.T(), err, "establishing the connection must always succeed")
245244
s.assertPingListWasCorrect(stream)
246245
require.EqualValues(s.T(), 5, s.srv.requestCount(), "three requests should have been made")
@@ -253,8 +252,8 @@ func (s *RetrySuite) TestServerStream_PerCallDeadline_Succeeds() {
253252
// a retry call with a 50 millisecond deadline. The 5th one doesn't sleep and succeeds.
254253
deadlinePerCall := 100 * time.Millisecond
255254
s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall)
256-
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, retry.WithPerRetryTimeout(deadlinePerCall),
257-
retry.WithMax(5))
255+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, WithPerRetryTimeout(deadlinePerCall),
256+
WithMax(5))
258257
require.NoError(s.T(), err, "establishing the connection must always succeed")
259258
s.assertPingListWasCorrect(stream)
260259
require.EqualValues(s.T(), 5, s.srv.requestCount(), "three requests should have been made")
@@ -274,8 +273,8 @@ func (s *RetrySuite) TestServerStream_PerCallDeadline_FailsOnParent() {
274273
s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall)
275274
parentCtx, cancel := context.WithTimeout(context.TODO(), parentDeadline)
276275
defer cancel()
277-
stream, err := s.Client.PingList(parentCtx, testpb.GoodPingList, retry.WithPerRetryTimeout(deadlinePerCall),
278-
retry.WithMax(5))
276+
stream, err := s.Client.PingList(parentCtx, testpb.GoodPingList, WithPerRetryTimeout(deadlinePerCall),
277+
WithMax(5))
279278
require.NoError(s.T(), err, "establishing the connection must always succeed")
280279
_, err = stream.Recv()
281280
require.Equal(s.T(), codes.DeadlineExceeded, status.Code(err), "failre code must be a gRPC error of Deadline class")
@@ -286,7 +285,7 @@ func (s *RetrySuite) TestServerStream_OnRetryCallbackCalled() {
286285

287286
s.srv.resetFailingConfiguration(3, codes.Unavailable, noSleep) // see retriable_errors
288287
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList,
289-
retry.WithOnRetryCallback(func(ctx context.Context, attempt uint, err error) {
288+
WithOnRetryCallback(func(ctx context.Context, attempt uint, err error) {
290289
retryCallbackCount++
291290
}),
292291
)
@@ -322,7 +321,7 @@ func (s *RetrySuite) TestServerStream_CallRetrySucceeds() {
322321
restarted := s.RestartServer(retryTimeout)
323322

324323
_, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList,
325-
retry.WithMax(40),
324+
WithMax(40),
326325
)
327326

328327
assert.NoError(s.T(), err, "establishing the connection should succeed")
@@ -371,8 +370,8 @@ func TestChainedRetrySuite(t *testing.T) {
371370
InterceptorTestSuite: &testpb.InterceptorTestSuite{
372371
TestService: service,
373372
ClientOpts: []grpc.DialOption{
374-
grpc.WithUnaryInterceptor(middleware.ChainUnaryClient(preRetryInterceptor.UnaryClientInterceptor, retry.UnaryClientInterceptor(), postRetryInterceptor.UnaryClientInterceptor)),
375-
grpc.WithStreamInterceptor(middleware.ChainStreamClient(preRetryInterceptor.StreamClientInterceptor, retry.StreamClientInterceptor(), postRetryInterceptor.StreamClientInterceptor)),
373+
grpc.WithUnaryInterceptor(middleware.ChainUnaryClient(preRetryInterceptor.UnaryClientInterceptor, UnaryClientInterceptor(), postRetryInterceptor.UnaryClientInterceptor)),
374+
grpc.WithStreamInterceptor(middleware.ChainStreamClient(preRetryInterceptor.StreamClientInterceptor, StreamClientInterceptor(), postRetryInterceptor.StreamClientInterceptor)),
376375
},
377376
},
378377
}
@@ -393,7 +392,7 @@ func (s *ChainedRetrySuite) SetupTest() {
393392
}
394393

395394
func (s *ChainedRetrySuite) TestUnaryWithChainedInterceptors_NoFailure() {
396-
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, retry.WithMax(2))
395+
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, WithMax(2))
397396
require.NoError(s.T(), err, "the invocation should succeed")
398397
require.EqualValues(s.T(), 1, s.srv.requestCount(), "one request should have been made")
399398
require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once")
@@ -402,15 +401,15 @@ func (s *ChainedRetrySuite) TestUnaryWithChainedInterceptors_NoFailure() {
402401

403402
func (s *ChainedRetrySuite) TestUnaryWithChainedInterceptors_WithRetry() {
404403
s.srv.resetFailingConfiguration(2, codes.Unavailable, noSleep)
405-
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, retry.WithMax(2))
404+
_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing, WithMax(2))
406405
require.NoError(s.T(), err, "the second invocation should succeed")
407406
require.EqualValues(s.T(), 2, s.srv.requestCount(), "two requests should have been made")
408407
require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once")
409408
require.EqualValues(s.T(), 2, s.postRetryInterceptor.called, "post-retry interceptor should be called twice")
410409
}
411410

412411
func (s *ChainedRetrySuite) TestStreamWithChainedInterceptors_NoFailure() {
413-
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, retry.WithMax(2))
412+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, WithMax(2))
414413
require.NoError(s.T(), err, "the invocation should succeed")
415414
_, err = stream.Recv()
416415
require.NoError(s.T(), err, "the Recv should succeed")
@@ -421,11 +420,42 @@ func (s *ChainedRetrySuite) TestStreamWithChainedInterceptors_NoFailure() {
421420

422421
func (s *ChainedRetrySuite) TestStreamWithChainedInterceptors_WithRetry() {
423422
s.srv.resetFailingConfiguration(2, codes.Unavailable, noSleep)
424-
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, retry.WithMax(2))
423+
stream, err := s.Client.PingList(s.SimpleCtx(), testpb.GoodPingList, WithMax(2))
425424
require.NoError(s.T(), err, "the second invocation should succeed")
426425
_, err = stream.Recv()
427426
require.NoError(s.T(), err, "the Recv should succeed")
428427
require.EqualValues(s.T(), 2, s.srv.requestCount(), "two requests should have been made")
429428
require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once")
430429
require.EqualValues(s.T(), 2, s.postRetryInterceptor.called, "post-retry interceptor should be called twice")
431430
}
431+
432+
func TestJitterUp(t *testing.T) {
433+
// Arguments to jitterup.
434+
duration := 10 * time.Second
435+
variance := 0.10
436+
437+
// Bound to check.
438+
max := 11000 * time.Millisecond
439+
min := 9000 * time.Millisecond
440+
high := scaleDuration(max, 0.98)
441+
low := scaleDuration(min, 1.02)
442+
443+
highCount := 0
444+
lowCount := 0
445+
446+
for i := 0; i < 1000; i++ {
447+
out := jitterUp(duration, variance)
448+
assert.True(t, out <= max, "value %s must be <= %s", out, max)
449+
assert.True(t, out >= min, "value %s must be >= %s", out, min)
450+
451+
if out > high {
452+
highCount++
453+
}
454+
if out < low {
455+
lowCount++
456+
}
457+
}
458+
459+
assert.True(t, highCount != 0, "at least one sample should reach to >%s", high)
460+
assert.True(t, lowCount != 0, "at least one sample should to <%s", low)
461+
}

0 commit comments

Comments
 (0)