Skip to content

Commit e57892e

Browse files
skotambkarjasdel
authored andcommitted
aws : Adds configurations to the default retryer (#375)
Provides more retryer customization options by adding a constructor for default Retryer which accepts functional options. Adds NoOpRetryer to support no retry behavior. Exposes members of default retryer. Updates the underlying logic used by the default retryer to calculate jittered delay for retry. Also updates the custom retry logic for service/ec2. Handles int overflow for retry delay. Includes changes for PR #373 . The changes will be squashed once the PR #373 is merged in. Fixes #370
1 parent cb8cad4 commit e57892e

29 files changed

+427
-153
lines changed

CHANGELOG_PENDING.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
* `aws/ec2metadata`: Add marketplaceProductCodes to EC2 Instance Identity Document ([#374](https://github.com/aws/aws-sdk-go-v2/pull/374))
1111
* Adds `MarketplaceProductCodes` to the EC2 Instance Metadata's Identity Document. The ec2metadata client will now retrieve these values if they are available.
1212
* Related to: [aws/aws-sdk-go#2781](https://github.com/aws/aws-sdk-go/issues/2781)
13-
13+
* `aws`: Adds configurations to the default retryer ([#375](https://github.com/aws/aws-sdk-go-v2/pull/375))
14+
* Provides more customization options for retryer by adding a constructor for default Retryer which accepts functional options. Adds NoOpRetryer to support no retry behavior. Exposes members of default retryer.
15+
* Updates the underlying logic used by the default retryer to calculate jittered delay for retry. Handles int overflow for retry delay.
16+
* Fixes [#370](https://github.com/aws/aws-sdk-go-v2/issues/370)
17+
1418
### SDK Bugs
1519
* `aws`: Fixes bug in calculating throttled retry delay ([#373](https://github.com/aws/aws-sdk-go-v2/pull/373))
1620
* The `Retry-After` duration specified in the request is now added to the Retry delay for throttled exception. Adds test for retry delays for throttled exceptions. Fixes bug where the throttled retry's math was off.
1721
* Fixes [#45](https://github.com/aws/aws-sdk-go-v2/issues/45)
18-
* `aws` : Adds missing sdk error checking when seeking readers [#379](https://github.com/aws/aws-sdk-go-v2/pull/379).
22+
* `aws` : Adds missing sdk error checking when seeking readers ([#379](https://github.com/aws/aws-sdk-go-v2/pull/379))
1923
* Adds support for nonseekable io.Reader. Adds support for streamed payloads for unsigned body request.
2024
* Fixes [#371](https://github.com/aws/aws-sdk-go-v2/issues/371)
25+

aws/client.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,10 @@ func NewClient(cfg Config, metadata Metadata) *Client {
6363

6464
retryer := cfg.Retryer
6565
if retryer == nil {
66-
// TODO need better way of specifing default num retries
67-
retryer = DefaultRetryer{NumMaxRetries: 3}
66+
retryer = NewDefaultRetryer()
6867
}
6968
svc.Retryer = retryer
70-
7169
svc.AddDebugHandlers()
72-
7370
return svc
7471
}
7572

aws/default_retryer.go

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
package aws
22

33
import (
4+
"math"
45
"math/rand"
56
"strconv"
67
"sync"
78
"time"
89
)
910

1011
// DefaultRetryer implements basic retry logic using exponential backoff for
11-
// most services. If you want to implement custom retry logic, implement the
12-
// Retryer interface or create a structure type that composes this
13-
// struct and override the specific methods. For example, to override only
14-
// the MaxRetries method:
15-
//
16-
// type retryer struct {
17-
// client.DefaultRetryer
18-
// }
19-
//
20-
// // This implementation always has 100 max retries
21-
// func (d retryer) MaxRetries() int { return 100 }
12+
// most services. You can implement your own custom retryer by implementing
13+
// retryer interface.
2214
type DefaultRetryer struct {
23-
NumMaxRetries int
15+
NumMaxRetries int
16+
MinRetryDelay time.Duration
17+
MinThrottleDelay time.Duration
18+
MaxRetryDelay time.Duration
19+
MaxThrottleDelay time.Duration
2420
}
2521

22+
const (
23+
// DefaultRetryerMaxNumRetries sets maximum number of retries
24+
DefaultRetryerMaxNumRetries = 3
25+
26+
// DefaultRetryerMinRetryDelay sets minimum retry delay
27+
DefaultRetryerMinRetryDelay = 30 * time.Millisecond
28+
29+
// DefaultRetryerMinThrottleDelay sets minimum delay when throttled
30+
DefaultRetryerMinThrottleDelay = 500 * time.Millisecond
31+
32+
// DefaultRetryerMaxRetryDelay sets maximum retry delay
33+
DefaultRetryerMaxRetryDelay = 300 * time.Second
34+
35+
// DefaultRetryerMaxThrottleDelay sets maximum delay when throttled
36+
DefaultRetryerMaxThrottleDelay = 300 * time.Second
37+
)
38+
2639
// MaxRetries returns the number of maximum returns the service will use to make
2740
// an individual API
2841
func (d DefaultRetryer) MaxRetries() int {
@@ -31,30 +44,63 @@ func (d DefaultRetryer) MaxRetries() int {
3144

3245
var seededRand = rand.New(&lockedSource{src: rand.NewSource(time.Now().UnixNano())})
3346

47+
// NewDefaultRetryer returns a retryer initialized with default values and optionally takes function
48+
// to override values for default retryer.
49+
func NewDefaultRetryer(opts ...func(d *DefaultRetryer)) DefaultRetryer {
50+
d := DefaultRetryer{
51+
NumMaxRetries: DefaultRetryerMaxNumRetries,
52+
MinRetryDelay: DefaultRetryerMinRetryDelay,
53+
MinThrottleDelay: DefaultRetryerMinThrottleDelay,
54+
MaxRetryDelay: DefaultRetryerMaxRetryDelay,
55+
MaxThrottleDelay: DefaultRetryerMaxThrottleDelay,
56+
}
57+
58+
for _, opt := range opts {
59+
opt(&d)
60+
}
61+
return d
62+
}
63+
3464
// RetryRules returns the delay duration before retrying this request again
65+
//
66+
// Note: RetryRules method must be a value receiver so that the
67+
// defaultRetryer is safe.
3568
func (d DefaultRetryer) RetryRules(r *Request) time.Duration {
36-
// Set the upper limit of delay in retrying at ~five minutes
37-
var minTime int64 = 30
69+
minDelay := d.MinRetryDelay
3870
var initialDelay time.Duration
39-
4071
throttle := d.shouldThrottle(r)
4172
if throttle {
4273
if delay, ok := getRetryAfterDelay(r); ok {
4374
initialDelay = delay
4475
}
45-
46-
minTime = 500
76+
minDelay = d.MinThrottleDelay
4777
}
4878

4979
retryCount := r.RetryCount
50-
if throttle && retryCount > 8 {
51-
retryCount = 8
52-
} else if retryCount > 12 {
53-
retryCount = 12
80+
81+
maxDelay := d.MaxRetryDelay
82+
if throttle {
83+
maxDelay = d.MaxThrottleDelay
84+
}
85+
86+
var delay time.Duration
87+
88+
// Logic to cap the retry count based on the minDelay provided
89+
actualRetryCount := int(math.Log2(float64(minDelay))) + 1
90+
if actualRetryCount < 63-retryCount {
91+
delay = time.Duration(1<<uint64(retryCount)) * getJitterDelay(minDelay)
92+
if delay > maxDelay {
93+
delay = getJitterDelay(maxDelay / 2)
94+
}
95+
} else {
96+
delay = getJitterDelay(maxDelay / 2)
5497
}
98+
return delay + initialDelay
99+
}
55100

56-
delay := (1 << uint(retryCount)) * (seededRand.Int63n(minTime) + minTime)
57-
return (time.Duration(delay) * time.Millisecond) + initialDelay
101+
// getJitterDelay returns a jittered delay for retry
102+
func getJitterDelay(duration time.Duration) time.Duration {
103+
return time.Duration(seededRand.Int63n(int64(duration)) + int64(duration))
58104
}
59105

60106
// ShouldRetry returns true if the request should be retried.
@@ -73,16 +119,18 @@ func (d DefaultRetryer) ShouldRetry(r *Request) bool {
73119

74120
// ShouldThrottle returns true if the request should be throttled.
75121
func (d DefaultRetryer) shouldThrottle(r *Request) bool {
76-
switch r.HTTPResponse.StatusCode {
77-
case 429:
78-
case 502:
79-
case 503:
80-
case 504:
81-
default:
82-
return r.IsErrorThrottle()
122+
if r.HTTPResponse != nil {
123+
switch r.HTTPResponse.StatusCode {
124+
case 429:
125+
case 502:
126+
case 503:
127+
case 504:
128+
default:
129+
return r.IsErrorThrottle()
130+
}
131+
return true
83132
}
84-
85-
return true
133+
return r.IsErrorThrottle()
86134
}
87135

88136
// This will look in the Retry-After header, RFC 7231, for how long

aws/default_retryer_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ func TestRetryThrottleStatusCodes(t *testing.T) {
5656
},
5757
}
5858

59-
d := DefaultRetryer{NumMaxRetries: 10}
59+
d := NewDefaultRetryer(func(d *DefaultRetryer) {
60+
d.NumMaxRetries = 100
61+
})
6062
for i, c := range cases {
6163
throttle := d.shouldThrottle(&c.r)
6264
retry := d.ShouldRetry(&c.r)
@@ -71,7 +73,7 @@ func TestRetryThrottleStatusCodes(t *testing.T) {
7173
}
7274
}
7375

74-
func TestCanUseRetryAfter(t *testing.T) {
76+
func TestGetRetryAfterDelay(t *testing.T) {
7577
cases := []struct {
7678
r Request
7779
e bool
@@ -164,7 +166,9 @@ func TestGetRetryDelay(t *testing.T) {
164166
}
165167

166168
func TestRetryDelay(t *testing.T) {
167-
d := DefaultRetryer{100}
169+
d := NewDefaultRetryer(func(d *DefaultRetryer) {
170+
d.NumMaxRetries = 100
171+
})
168172
r := Request{}
169173
for i := 0; i < 100; i++ {
170174
rTemp := r
@@ -190,7 +194,7 @@ func TestRetryDelay(t *testing.T) {
190194
rTemp.RetryCount = 1
191195
rTemp.HTTPResponse = &http.Response{StatusCode: 503, Header: http.Header{"Retry-After": []string{"300"}}}
192196
a := d.RetryRules(&rTemp)
193-
if a < 5*time.Minute{
197+
if a < 5*time.Minute {
194198
t.Errorf("retry delay should not be less than retry-after duration, received %s for retrycount %d", a, 1)
195199
}
196200
}

aws/http_request_retry_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ func TestRequestCancelRetry(t *testing.T) {
2525
reqNum := 0
2626
cfg := unit.Config()
2727
cfg.EndpointResolver = aws.ResolveWithEndpointURL("http://endpoint")
28-
cfg.Retryer = aws.DefaultRetryer{NumMaxRetries: 10}
28+
cfg.Retryer = aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
29+
d.NumMaxRetries = 10
30+
})
2931

3032
s := mock.NewMockClient(cfg)
3133

aws/no_op_retryer.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package aws
2+
3+
import "time"
4+
5+
// NoOpRetryer provides a retryer that performs no retries.
6+
// It should be used when we do not want retries to be performed.
7+
type NoOpRetryer struct{}
8+
9+
// MaxRetries returns the number of maximum returns the service will use to make
10+
// an individual API; For NoOpRetryer the MaxRetries will always be zero.
11+
func (d NoOpRetryer) MaxRetries() int {
12+
return 0
13+
}
14+
15+
// ShouldRetry will always return false for NoOpRetryer, as it should never retry.
16+
func (d NoOpRetryer) ShouldRetry(_ *Request) bool {
17+
return false
18+
}
19+
20+
// RetryRules returns the delay duration before retrying this request again;
21+
// since NoOpRetryer does not retry, RetryRules always returns 0.
22+
func (d NoOpRetryer) RetryRules(_ *Request) time.Duration {
23+
return 0
24+
}

aws/no_op_retryer_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package aws
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestNoOpRetryer(t *testing.T) {
10+
cases := []struct {
11+
r Request
12+
expectMaxRetries int
13+
expectRetryDelay time.Duration
14+
expectRetry bool
15+
}{
16+
{
17+
r: Request{
18+
HTTPResponse: &http.Response{StatusCode: 200},
19+
},
20+
expectMaxRetries: 0,
21+
expectRetryDelay: 0,
22+
expectRetry: false,
23+
},
24+
}
25+
26+
d := NoOpRetryer{}
27+
for i, c := range cases {
28+
maxRetries := d.MaxRetries()
29+
retry := d.ShouldRetry(&c.r)
30+
retryDelay := d.RetryRules(&c.r)
31+
32+
if e, a := c.expectMaxRetries, maxRetries; e != a {
33+
t.Errorf("%d: expected %v, but received %v for number of max retries", i, e, a)
34+
}
35+
36+
if e, a := c.expectRetry, retry; e != a {
37+
t.Errorf("%d: expected %v, but received %v for should retry", i, e, a)
38+
}
39+
40+
if e, a := c.expectRetryDelay, retryDelay; e != a {
41+
t.Errorf("%d: expected %v, but received %v as retry delay", i, e, a)
42+
}
43+
}
44+
}

aws/request_1_6_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestRequestInvalidEndpoint(t *testing.T) {
2121
cfg,
2222
aws.Metadata{},
2323
cfg.Handlers,
24-
aws.DefaultRetryer{},
24+
aws.NewDefaultRetryer(),
2525
&aws.Operation{},
2626
nil,
2727
nil,

aws/request_pagination_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ func TestPagination(t *testing.T) {
5858
},
5959
}
6060

61-
retryer := aws.DefaultRetryer{NumMaxRetries: 2}
61+
retryer := aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
62+
d.NumMaxRetries = 2
63+
})
6264
op := aws.Operation{
6365
Name: "Operation",
6466
Paginator: &aws.Paginator{
@@ -160,7 +162,9 @@ func TestPaginationTruncation(t *testing.T) {
160162
}
161163

162164
reqNum := 0
163-
retryer := aws.DefaultRetryer{NumMaxRetries: 2}
165+
retryer := aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
166+
d.NumMaxRetries = 2
167+
})
164168
ops := []aws.Operation{
165169
{
166170
Name: "Operation",
@@ -271,7 +275,9 @@ func BenchmarkPagination(b *testing.B) {
271275
{aws.String("3"), aws.String("")},
272276
}
273277

274-
retryer := aws.DefaultRetryer{NumMaxRetries: 2}
278+
retryer := aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
279+
d.NumMaxRetries = 2
280+
})
275281
op := aws.Operation{
276282
Name: "Operation",
277283
Paginator: &aws.Paginator{
@@ -339,7 +345,9 @@ func TestPaginationWithContextCancel(t *testing.T) {
339345
},
340346
}
341347

342-
retryer := aws.DefaultRetryer{NumMaxRetries: 2}
348+
retryer := aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
349+
d.NumMaxRetries = 2
350+
})
343351
op := aws.Operation{
344352
Name: "Operation",
345353
Paginator: &aws.Paginator{
@@ -350,7 +358,7 @@ func TestPaginationWithContextCancel(t *testing.T) {
350358

351359
for _, c := range cases {
352360
input := c.input
353-
inValues := []string{}
361+
var inValues []string
354362
p := aws.Pager{
355363
NewRequest: func(ctx context.Context) (*aws.Request, error) {
356364
h := defaults.Handlers()
@@ -380,7 +388,7 @@ func TestPaginationWithContextCancel(t *testing.T) {
380388
ctx, cancelFn := context.WithCancel(context.Background())
381389
cancelFn()
382390

383-
results := []*string{}
391+
var results []*string
384392
for p.Next(ctx) {
385393
page := p.CurrentPage()
386394
output := page.(*mockOutput)

0 commit comments

Comments
 (0)