Skip to content

Commit 33a4306

Browse files
authored
aws: Refactors request retry behavior path logic (#384)
Ports v1 SDK's retry utilities to follow a consistent code path. aws.IsErrorRetryable is the primary entry point to determine if a request is retryable. Also ports support for service/kinesis to retry service specific API errors . Corrects sdk's behavior by not retrying errors with status code 501. Related to aws/aws-sdk-go#2744, aws/aws-sdk-go#2774, aws/aws-sdk-go#2751 and aws/aws-sdk-go#1826 Fixes #372; Fixes #145
1 parent 2010860 commit 33a4306

24 files changed

+828
-519
lines changed

CHANGELOG_PENDING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* 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.
1515
* Updates the underlying logic used by the default retryer to calculate jittered delay for retry. Handles int overflow for retry delay.
1616
* Fixes [#370](https://github.com/aws/aws-sdk-go-v2/issues/370)
17+
* `aws` : Refactors request retry behavior path logic ([#384](https://github.com/aws/aws-sdk-go-v2/pull/384))
18+
* Retry utilities now follow a consistent code path. aws.IsErrorRetryable is the primary entry point to determine if a request is retryable.
19+
* Corrects sdk's behavior by not retrying errors with status code 501. Adds support for retrying the Kinesis API error, LimitExceededException.
20+
* Fixes [#372](https://github.com/aws/aws-sdk-go-v2/issues/372), [#145](https://github.com/aws/aws-sdk-go-v2/issues/145)
1721

1822
### SDK Bugs
1923
* `aws`: Fixes bug in calculating throttled retry delay ([#373](https://github.com/aws/aws-sdk-go-v2/pull/373))

aws/default_retryer.go

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,21 @@ func NewDefaultRetryer(opts ...func(d *DefaultRetryer)) DefaultRetryer {
6666
// Note: RetryRules method must be a value receiver so that the
6767
// defaultRetryer is safe.
6868
func (d DefaultRetryer) RetryRules(r *Request) time.Duration {
69+
6970
minDelay := d.MinRetryDelay
71+
maxDelay := d.MaxRetryDelay
72+
7073
var initialDelay time.Duration
71-
throttle := d.shouldThrottle(r)
72-
if throttle {
74+
isThrottle := r.IsErrorThrottle()
75+
if isThrottle {
7376
if delay, ok := getRetryAfterDelay(r); ok {
7477
initialDelay = delay
7578
}
7679
minDelay = d.MinThrottleDelay
77-
}
78-
79-
retryCount := r.RetryCount
80-
81-
maxDelay := d.MaxRetryDelay
82-
if throttle {
8380
maxDelay = d.MaxThrottleDelay
8481
}
8582

83+
retryCount := r.RetryCount
8684
var delay time.Duration
8785

8886
// Logic to cap the retry count based on the minDelay provided
@@ -111,26 +109,7 @@ func (d DefaultRetryer) ShouldRetry(r *Request) bool {
111109
return *r.Retryable
112110
}
113111

114-
if r.HTTPResponse.StatusCode >= 500 {
115-
return true
116-
}
117-
return r.IsErrorRetryable() || d.shouldThrottle(r)
118-
}
119-
120-
// ShouldThrottle returns true if the request should be throttled.
121-
func (d DefaultRetryer) shouldThrottle(r *Request) bool {
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
132-
}
133-
return r.IsErrorThrottle()
112+
return r.IsErrorRetryable() || r.IsErrorThrottle()
134113
}
135114

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

aws/default_retryer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func TestRetryThrottleStatusCodes(t *testing.T) {
6060
d.NumMaxRetries = 100
6161
})
6262
for i, c := range cases {
63-
throttle := d.shouldThrottle(&c.r)
63+
throttle := c.r.IsErrorThrottle()
6464
retry := d.ShouldRetry(&c.r)
6565

6666
if e, a := c.expectThrottle, throttle; e != a {

aws/defaults/handlers.go

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ var SendHandler = aws.NamedHandler{
104104

105105
// TODO remove this complexity the SDK's built http.Request should
106106
// set Request.Body to nil, if there is no body to send. #318
107-
if aws.NoBody == r.HTTPRequest.Body {
107+
if http.NoBody == r.HTTPRequest.Body {
108108
// Strip off the request body if the NoBody reader was used as a
109109
// place holder for a request body. This prevents the SDK from
110110
// making requests with a request body when it would be invalid
@@ -158,9 +158,10 @@ func handleSendError(r *aws.Request, err error) {
158158
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
159159
}
160160
}
161-
// Catch all other request errors.
161+
162+
// Catch all request errors, and let the retryer determine
163+
// if the error is retryable.
162164
r.Error = awserr.New("RequestError", "send request failed", err)
163-
r.Retryable = aws.Bool(true) // network errors are retryable
164165

165166
// Override the error with a context canceled error, if that was canceled.
166167
ctx := r.Context()
@@ -183,34 +184,36 @@ var ValidateResponseHandler = aws.NamedHandler{Name: "core.ValidateResponseHandl
183184

184185
// AfterRetryHandler performs final checks to determine if the request should
185186
// be retried and how long to delay.
186-
var AfterRetryHandler = aws.NamedHandler{Name: "core.AfterRetryHandler", Fn: func(r *aws.Request) {
187-
// If one of the other handlers already set the retry state
188-
// we don't want to override it based on the service's state
189-
if r.Retryable == nil || r.Config.EnforceShouldRetryCheck {
190-
r.Retryable = aws.Bool(r.ShouldRetry(r))
191-
}
187+
var AfterRetryHandler = aws.NamedHandler{
188+
Name: "core.AfterRetryHandler",
189+
Fn: func(r *aws.Request) {
190+
// If one of the other handlers already set the retry state
191+
// we don't want to override it based on the service's state
192+
if r.Retryable == nil || r.Config.EnforceShouldRetryCheck {
193+
r.Retryable = aws.Bool(r.ShouldRetry(r))
194+
}
192195

193-
if r.WillRetry() {
194-
r.RetryDelay = r.RetryRules(r)
196+
if r.WillRetry() {
197+
r.RetryDelay = r.RetryRules(r)
195198

196-
if err := sdk.SleepWithContext(r.Context(), r.RetryDelay); err != nil {
197-
r.Error = awserr.New(aws.ErrCodeRequestCanceled,
198-
"request context canceled", err)
199-
r.Retryable = aws.Bool(false)
200-
return
201-
}
199+
if err := sdk.SleepWithContext(r.Context(), r.RetryDelay); err != nil {
200+
r.Error = awserr.New(aws.ErrCodeRequestCanceled,
201+
"request context canceled", err)
202+
r.Retryable = aws.Bool(false)
203+
return
204+
}
202205

203-
// when the expired token exception occurs the credentials
204-
// need to be expired locally so that the next request to
205-
// get credentials will trigger a credentials refresh.
206-
if p, ok := r.Config.Credentials.(sdk.Invalidator); ok && r.IsErrorExpired() {
207-
p.Invalidate()
208-
}
206+
// when the expired token exception occurs the credentials
207+
// need to be expired locally so that the next request to
208+
// get credentials will trigger a credentials refresh.
209+
if p, ok := r.Config.Credentials.(sdk.Invalidator); ok && r.IsErrorExpired() {
210+
p.Invalidate()
211+
}
209212

210-
r.RetryCount++
211-
r.Error = nil
212-
}
213-
}}
213+
r.RetryCount++
214+
r.Error = nil
215+
}
216+
}}
214217

215218
// ValidateEndpointHandler is a request handler to validate a request had the
216219
// appropriate Region and Endpoint set. Will set r.Error if the endpoint or

aws/defaults/handlers_1_8_test.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

aws/defaults/handlers_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,33 @@ func TestBuildContentLength_WithBody(t *testing.T) {
398398
t.Errorf("expect no error, got %v", err)
399399
}
400400
}
401+
402+
func TestSendHandler_HEADNoBody(t *testing.T) {
403+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
404+
w.WriteHeader(http.StatusOK)
405+
}))
406+
407+
cfg := defaults.Config()
408+
cfg.Region = "mock-region"
409+
cfg.EndpointResolver = aws.ResolveWithEndpointURL(server.URL)
410+
411+
svc := s3.New(cfg)
412+
svc.ForcePathStyle = true
413+
414+
req := svc.HeadObjectRequest(&s3.HeadObjectInput{
415+
Bucket: aws.String("bucketname"),
416+
Key: aws.String("keyname"),
417+
})
418+
419+
if e, a := http.NoBody, req.HTTPRequest.Body; e != a {
420+
t.Fatalf("expect %T request body, got %T", e, a)
421+
}
422+
423+
_, err := req.Send(context.Background())
424+
if err != nil {
425+
t.Fatalf("expect no error, got %v", err)
426+
}
427+
if e, a := http.StatusOK, req.HTTPResponse.StatusCode; e != a {
428+
t.Errorf("expect %d status code, got %d", e, a)
429+
}
430+
}

aws/handlers.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Handlers struct {
1818
UnmarshalError HandlerList
1919
Retry HandlerList
2020
AfterRetry HandlerList
21+
CompleteAttempt HandlerList
2122
Complete HandlerList
2223
}
2324

@@ -34,6 +35,7 @@ func (h *Handlers) Copy() Handlers {
3435
UnmarshalMeta: h.UnmarshalMeta.copy(),
3536
Retry: h.Retry.copy(),
3637
AfterRetry: h.AfterRetry.copy(),
38+
CompleteAttempt: h.CompleteAttempt.copy(),
3739
Complete: h.Complete.copy(),
3840
}
3941
}
@@ -50,6 +52,7 @@ func (h *Handlers) Clear() {
5052
h.ValidateResponse.Clear()
5153
h.Retry.Clear()
5254
h.AfterRetry.Clear()
55+
h.CompleteAttempt.Clear()
5356
h.Complete.Clear()
5457
}
5558

@@ -172,6 +175,21 @@ func (l *HandlerList) SwapNamed(n NamedHandler) (swapped bool) {
172175
return swapped
173176
}
174177

178+
// Swap will swap out all handlers matching the name passed in. The matched
179+
// handlers will be swapped in. True is returned if the handlers were swapped.
180+
func (l *HandlerList) Swap(name string, replace NamedHandler) bool {
181+
var swapped bool
182+
183+
for i := 0; i < len(l.list); i++ {
184+
if l.list[i].Name == name {
185+
l.list[i] = replace
186+
swapped = true
187+
}
188+
}
189+
190+
return swapped
191+
}
192+
175193
// SetBackNamed will replace the named handler if it exists in the handler list.
176194
// If the handler does not exist the handler will be added to the end of the list.
177195
func (l *HandlerList) SetBackNamed(n NamedHandler) {

aws/http_request_retry_test.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
// +build go1.5
2-
31
package aws_test
42

53
import (
64
"context"
7-
"errors"
8-
"fmt"
95
"strings"
106
"testing"
117
"time"
@@ -20,13 +16,10 @@ func TestRequestCancelRetry(t *testing.T) {
2016
restoreSleep := mockSleep()
2117
defer restoreSleep()
2218

23-
c := make(chan struct{})
24-
2519
reqNum := 0
2620
cfg := unit.Config()
27-
cfg.EndpointResolver = aws.ResolveWithEndpointURL("http://endpoint")
2821
cfg.Retryer = aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
29-
d.NumMaxRetries = 10
22+
d.NumMaxRetries = 1
3023
})
3124

3225
s := mock.NewMockClient(cfg)
@@ -37,15 +30,14 @@ func TestRequestCancelRetry(t *testing.T) {
3730
s.Handlers.UnmarshalError.Clear()
3831
s.Handlers.Send.PushFront(func(r *aws.Request) {
3932
reqNum++
40-
r.Error = errors.New("net/http: request canceled")
4133
})
4234
out := &testData{}
35+
ctx, cancelFn := context.WithCancel(context.Background())
4336
r := s.NewRequest(&aws.Operation{Name: "Operation"}, nil, out)
44-
r.HTTPRequest.Cancel = c
45-
close(c)
37+
r.SetContext(ctx)
38+
cancelFn() // cancelling the context associated with the request
4639

4740
err := r.Send()
48-
fmt.Println("request error", err)
4941
if e, a := "canceled", err.Error(); !strings.Contains(a, e) {
5042
t.Errorf("expect %q to be in %q", e, a)
5143
}

0 commit comments

Comments
 (0)