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

### SDK Bugs
* `aws`: Fixes bug in calculating throttled retry delay ([#373](https://github.com/aws/aws-sdk-go-v2/pull/373))
Expand Down
34 changes: 8 additions & 26 deletions aws/default_retryer.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,21 @@ func NewDefaultRetryer(opts ...func(d *DefaultRetryer)) DefaultRetryer {
// Note: RetryRules method must be a value receiver so that the
// defaultRetryer is safe.
func (d DefaultRetryer) RetryRules(r *Request) time.Duration {

minDelay := d.MinRetryDelay
maxDelay := d.MaxRetryDelay

var initialDelay time.Duration
throttle := d.shouldThrottle(r)
if throttle {
isThrottle := r.IsErrorThrottle()
if isThrottle {
if delay, ok := getRetryAfterDelay(r); ok {
initialDelay = delay
}
minDelay = d.MinThrottleDelay
}

retryCount := r.RetryCount

maxDelay := d.MaxRetryDelay
if throttle {
maxDelay = d.MaxThrottleDelay
}

retryCount := r.RetryCount
var delay time.Duration

// Logic to cap the retry count based on the minDelay provided
Expand Down Expand Up @@ -111,26 +109,10 @@ func (d DefaultRetryer) ShouldRetry(r *Request) bool {
return *r.Retryable
}

if r.HTTPResponse.StatusCode >= 500 {
return true
}
return r.IsErrorRetryable() || d.shouldThrottle(r)
}

// ShouldThrottle returns true if the request should be throttled.
func (d DefaultRetryer) shouldThrottle(r *Request) bool {
if r.HTTPResponse != nil {
switch r.HTTPResponse.StatusCode {
case 429:
case 502:
case 503:
case 504:
default:
return r.IsErrorThrottle()
}
if r.HTTPResponse.StatusCode >= 500 && r.HTTPResponse.StatusCode != 501 {
return true
}
return r.IsErrorThrottle()
return r.IsErrorRetryable() || r.IsErrorThrottle()
}

// This will look in the Retry-After header, RFC 7231, for how long
Expand Down
2 changes: 1 addition & 1 deletion aws/default_retryer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestRetryThrottleStatusCodes(t *testing.T) {
d.NumMaxRetries = 100
})
for i, c := range cases {
throttle := d.shouldThrottle(&c.r)
throttle := c.r.IsErrorThrottle()
retry := d.ShouldRetry(&c.r)

if e, a := c.expectThrottle, throttle; e != a {
Expand Down
55 changes: 29 additions & 26 deletions aws/defaults/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,10 @@ func handleSendError(r *aws.Request, err error) {
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
}
}
// Catch all other request errors.

// Catch all request errors, and let the default retrier determine
// if the error is retryable.
r.Error = awserr.New("RequestError", "send request failed", err)
r.Retryable = aws.Bool(true) // network errors are retryable

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

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

if r.WillRetry() {
r.RetryDelay = r.RetryRules(r)
if r.WillRetry() {
r.RetryDelay = r.RetryRules(r)

if err := sdk.SleepWithContext(r.Context(), r.RetryDelay); err != nil {
r.Error = awserr.New(aws.ErrCodeRequestCanceled,
"request context canceled", err)
r.Retryable = aws.Bool(false)
return
}
if err := sdk.SleepWithContext(r.Context(), r.RetryDelay); err != nil {
r.Error = awserr.New(aws.ErrCodeRequestCanceled,
"request context canceled", err)
r.Retryable = aws.Bool(false)
return
}

// when the expired token exception occurs the credentials
// need to be expired locally so that the next request to
// get credentials will trigger a credentials refresh.
if p, ok := r.Config.Credentials.(sdk.Invalidator); ok && r.IsErrorExpired() {
p.Invalidate()
}
// when the expired token exception occurs the credentials
// need to be expired locally so that the next request to
// get credentials will trigger a credentials refresh.
if p, ok := r.Config.Credentials.(sdk.Invalidator); ok && r.IsErrorExpired() {
p.Invalidate()
}

r.RetryCount++
r.Error = nil
}
}}
r.RetryCount++
r.Error = nil
}
}}

// ValidateEndpointHandler is a request handler to validate a request had the
// appropriate Region and Endpoint set. Will set r.Error if the endpoint or
Expand Down
18 changes: 18 additions & 0 deletions aws/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Handlers struct {
UnmarshalError HandlerList
Retry HandlerList
AfterRetry HandlerList
CompleteAttempt HandlerList
Complete HandlerList
}

Expand All @@ -34,6 +35,7 @@ func (h *Handlers) Copy() Handlers {
UnmarshalMeta: h.UnmarshalMeta.copy(),
Retry: h.Retry.copy(),
AfterRetry: h.AfterRetry.copy(),
CompleteAttempt: h.CompleteAttempt.copy(),
Complete: h.Complete.copy(),
}
}
Expand All @@ -50,6 +52,7 @@ func (h *Handlers) Clear() {
h.ValidateResponse.Clear()
h.Retry.Clear()
h.AfterRetry.Clear()
h.CompleteAttempt.Clear()
h.Complete.Clear()
}

Expand Down Expand Up @@ -172,6 +175,21 @@ func (l *HandlerList) SwapNamed(n NamedHandler) (swapped bool) {
return swapped
}

// Swap will swap out all handlers matching the name passed in. The matched
// handlers will be swapped in. True is returned if the handlers were swapped.
func (l *HandlerList) Swap(name string, replace NamedHandler) bool {
var swapped bool

for i := 0; i < len(l.list); i++ {
if l.list[i].Name == name {
l.list[i] = replace
swapped = true
}
}

return swapped
}

// SetBackNamed will replace the named handler if it exists in the handler list.
// If the handler does not exist the handler will be added to the end of the list.
func (l *HandlerList) SetBackNamed(n NamedHandler) {
Expand Down
16 changes: 4 additions & 12 deletions aws/http_request_retry_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
// +build go1.5

package aws_test

import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
Expand All @@ -20,13 +16,10 @@ func TestRequestCancelRetry(t *testing.T) {
restoreSleep := mockSleep()
defer restoreSleep()

c := make(chan struct{})

reqNum := 0
cfg := unit.Config()
cfg.EndpointResolver = aws.ResolveWithEndpointURL("http://endpoint")
cfg.Retryer = aws.NewDefaultRetryer(func(d *aws.DefaultRetryer) {
d.NumMaxRetries = 10
d.NumMaxRetries = 1
})

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

err := r.Send()
fmt.Println("request error", err)
if e, a := "canceled", err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q to be in %q", e, a)
}
Expand Down
Loading