Skip to content

Commit

Permalink
Refactor SDK's request retry, and error handling behavior (#487)
Browse files Browse the repository at this point in the history
This update refactors the SDK's request retry behavior by updating the SDK's implementation to a model that will be the standard across the SDKs. This model simplifies retry behavior, and will retry all connection errors. Not just write connection reset errors.

This update also refactors the SDK's error wrapping and handling. The SDK's error types such as awserr.Error implement the Unwrap method allowing the nested errors to be more easily extracted using the Go 1.13 errors.As, Is, and Unwrap functions.

This update also provides more information about why a request failed, and was retried, but exhausted attempts. The aws.MaxAttemptsError will wrap the connection or service API error.
  • Loading branch information
jasdel committed Mar 11, 2020
1 parent 9fc62ee commit dc3c876
Show file tree
Hide file tree
Showing 327 changed files with 4,342 additions and 2,198 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Breaking Change
---

* Update SDK retry behavior
* Significant updates were made the SDK's retry behavior. The SDK will now retry all connections error. In addition, to changing what errors are retried the SDK's retry behavior not distinguish the difference between throttling errors, and regular retryable errors. All errors will be retried with the same backoff jitter delay scaling.
* The SDK will attempt an operation request 3 times by default. This is one less than the previous initial request with 3 retires.
* New helper functions in the new `aws/retry` package allow wrapping a `Retrier` with custom behavior, overriding the base `Retrier`, (e.g. `AddWithErrorCodes`, and `AddWithMaxAttempts`)
* Update SDK error handling
* Updates the SDK's handling of errors to take advantage of Go 1.13's new `errors.As`, `Is`, and `Unwrap`. The SDK's errors were updated to satisfy the `Unwrap` interface, returning the underlying error.
* With this update, you can now more easily access the SDK's layered errors, and meaningful state such as, `Timeout`, `Temporary`, and other states added to the SDK such as `CanceledError`.
* Bump SDK minimum supported version from Go 1.12 to Go 1.13
* The SDK's minimum supported version is bumped to take advantage of Go 1.13's updated `errors` package.

Services
---

Expand All @@ -14,6 +24,10 @@ SDK Features
* Fixes [#338](https://github.com/aws/aws-sdk-go-v2/issues/338)
* Adds Support for `credential_source`
* Fixes [#274](https://github.com/aws/aws-sdk-go-v2/issues/274)
* `aws/awserr`: Adds support for Go 1.13's `errors.Unwrap` ([#487](https://github.com/aws/aws-sdk-go-v2/pull/487))
* `aws`: Updates SDK retry behavior ([#487](https://github.com/aws/aws-sdk-go-v2/pull/487))
* `aws/retry`: New package defining logic to determine if a request should be retried, and backoff.
* `aws/ratelimit`: New package defining rate limit logic such as token bucket used by the `retry.Standard` retrier.

SDK Enhancements
---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

`aws-sdk-go-v2` is the **Developer Preview** (aka **beta**) for the v2 AWS SDK for the Go programming language. This Developer Preview is provided to receive feedback from the language community on SDK changes prior to the final release. As such users should expect the SDK to release minor version releases that break backwards compatability. The release notes for the breaking change will include information about the breaking change, and how you can migrate to the latest version.

Check out the [Issues] and [Projects] for design and updates being made to the SDK. The v2 SDK requires a minimum version of `Go 1.12`.
Check out the [Issues] and [Projects] for design and updates being made to the SDK. The v2 SDK requires a minimum version of `Go 1.13`.

We'll be expanding out the [Issues] and [Projects] sections with additional changes to the SDK based on your feedback, and SDK's core's improvements. Check the the SDK's [CHANGE_LOG] for information about the latest updates to the SDK.

Expand Down
46 changes: 9 additions & 37 deletions aws/awserr/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ package awserr
// // Get error details
// log.Println("Error:", awsErr.Code(), awsErr.Message())
//
// // Prints out full error message, including original error if there was one.
// log.Println("Error:", awsErr.Error())
// // Prints out full error message, including original error if
// // there was one.
// log.Println("Error:", awsErr)
//
// // Get original error
// if origErr := awsErr.OrigErr(); origErr != nil {
// if origErr := errors.Unwrap(awsErr); origErr != nil {
// // operate on original error.
// }
// } else {
// fmt.Println(err.Error())
// fmt.Println(err)
// }
// }
//
Expand All @@ -37,60 +38,31 @@ type Error interface {

// Returns the error details message.
Message() string

// Returns the original error if one was set. Nil is returned if not set.
OrigErr() error
}

// BatchError is a batch of errors which also wraps lower level errors with
// code, message, and original errors. Calling Error() will include all errors
// that occurred in the batch.
//
// Deprecated: Replaced with BatchedErrors. Only defined for backwards
// compatibility.
type BatchError interface {
// Satisfy the generic error interface.
error

// Returns the short phrase depicting the classification of the error.
Code() string

// Returns the error details message.
Message() string

// Returns the original error if one was set. Nil is returned if not set.
OrigErrs() []error
}

// BatchedErrors is a batch of errors which also wraps lower level errors with
// code, message, and original errors. Calling Error() will include all errors
// that occurred in the batch.
//
// Replaces BatchError
type BatchedErrors interface {
// Satisfy the base Error interface.
Error

// Returns the original error if one was set. Nil is returned if not set.
OrigErrs() []error
Errs() []error
}

// New returns an Error object described by the code, message, and origErr.
//
// If origErr satisfies the Error interface it will not be wrapped within a new
// Error object and will instead be returned.
func New(code, message string, origErr error) Error {
var errs []error
if origErr != nil {
errs = append(errs, origErr)
}
return newBaseError(code, message, errs)
func New(code, message string, err error) Error {
return newBaseError(code, message, err)
}

// NewBatchError returns an BatchedErrors with a collection of errors as an
// array of errors.
func NewBatchError(code, message string, errs []error) BatchedErrors {
return newBaseError(code, message, errs)
return newBatchError(code, message, errs)
}

// A RequestFailure is an interface to extract request failure information from
Expand Down
81 changes: 44 additions & 37 deletions aws/awserr/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package awserr

import "fmt"
import (
"errors"
"fmt"
)

// SprintError returns a string of the formatted error code.
//
Expand All @@ -12,7 +15,7 @@ func SprintError(code, message, extra string, origErr error) string {
msg = fmt.Sprintf("%s\n\t%s", msg, extra)
}
if origErr != nil {
msg = fmt.Sprintf("%s\ncaused by: %s", msg, origErr.Error())
msg = fmt.Sprintf("%s\ncaused by: %v", msg, origErr)
}
return msg
}
Expand All @@ -31,7 +34,7 @@ type baseError struct {

// Optional original error this error is based off of. Allows building
// chained errors.
errs []error
err error
}

// newBaseError returns an error object for the code, message, and errors.
Expand All @@ -44,11 +47,11 @@ type baseError struct {
//
// origErrs is the error objects which will be nested under the new errors to
// be returned.
func newBaseError(code, message string, origErrs []error) *baseError {
func newBaseError(code, message string, err error) *baseError {
b := &baseError{
code: code,
message: message,
errs: origErrs,
err: err,
}

return b
Expand All @@ -60,12 +63,7 @@ func newBaseError(code, message string, origErrs []error) *baseError {
//
// Satisfies the error interface.
func (b baseError) Error() string {
size := len(b.errs)
if size > 0 {
return SprintError(b.code, b.message, "", errorList(b.errs))
}

return SprintError(b.code, b.message, "", nil)
return SprintError(b.code, b.message, "", b.err)
}

// String returns the string representation of the error.
Expand All @@ -84,27 +82,37 @@ func (b baseError) Message() string {
return b.message
}

// OrigErr returns the original error if one was set. Nil is returned if no
// Unwrap returns the original error if one was set. Nil is returned if no
// error was set. This only returns the first element in the list. If the full
// list is needed, use BatchedErrors.
func (b baseError) OrigErr() error {
switch len(b.errs) {
case 0:
return nil
case 1:
return b.errs[0]
default:
if err, ok := b.errs[0].(Error); ok {
return NewBatchError(err.Code(), err.Message(), b.errs[1:])
}
return NewBatchError("BatchedErrors",
"multiple errors occurred", b.errs)
func (b baseError) Unwrap() error {
return b.err
}

type batchError struct {
*baseError
errs []error
}

func newBatchError(code, message string, errs []error) *batchError {
return &batchError{
baseError: newBaseError(code, message, nil),
errs: errs,
}
}

func (b batchError) Error() string {
size := len(b.errs)
if size > 0 {
return SprintError(b.code, b.message, "", errorList(b.errs))
}

return SprintError(b.code, b.message, "", nil)
}

// OrigErrs returns the original errors if one was set. An empty slice is
// returned if no error was set.
func (b baseError) OrigErrs() []error {
// Errs returns the original errors if one was set. Nil is returned if no error
// was set.
func (b batchError) Errs() []error {
return b.errs
}

Expand Down Expand Up @@ -142,13 +150,11 @@ func newRequestError(err Error, statusCode int, requestID string) *requestError
func (r requestError) Error() string {
extra := fmt.Sprintf("status code: %d, request id: %s",
r.statusCode, r.requestID)
return SprintError(r.Code(), r.Message(), extra, r.OrigErr())
return SprintError(r.Code(), r.Message(), extra, r.Unwrap())
}

// String returns the string representation of the error.
// Alias for Error to satisfy the stringer interface.
func (r requestError) String() string {
return r.Error()
func (r requestError) Unwrap() error {
return errors.Unwrap(r.awsError)
}

// StatusCode returns the wrapped status code for the error
Expand All @@ -161,13 +167,14 @@ func (r requestError) RequestID() string {
return r.requestID
}

// OrigErrs returns the original errors if one was set. An empty slice is
// returned if no error was set.
func (r requestError) OrigErrs() []error {
// Errs returns the original errors if one was set. Nil is returned if no error
// is set.
func (r requestError) Errs() []error {
if b, ok := r.awsError.(BatchedErrors); ok {
return b.OrigErrs()
return b.Errs()
}
return []error{r.OrigErr()}

return nil
}

// An error list that satisfies the golang interface
Expand Down
5 changes: 0 additions & 5 deletions aws/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ func NewClient(cfg Config, metadata Metadata) *Client {
svc.Config.HTTPClient = wrapWithoutRedirect(c)
}

retryer := cfg.Retryer
if retryer == nil {
retryer = NewDefaultRetryer()
}
svc.Retryer = retryer
svc.AddDebugHandlers()
return svc
}
Expand Down
14 changes: 2 additions & 12 deletions aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,8 @@ type Config struct {
Handlers Handlers

// Retryer guides how HTTP requests should be retried in case of
// recoverable failures.
//
// When nil or the value does not implement the request.Retryer interface,
// the client.DefaultRetryer will be used.
//
// When both Retryer and MaxRetries are non-nil, the former is used and
// the latter ignored.
//
// To set the Retryer field in a type-safe manner and with chaining, use
// the request.WithRetryer helper function:
//
// cfg := request.WithRetryer(aws.NewConfig(), myRetryer)
// recoverable failures. When nil the API client will use a default
// retryer.
Retryer Retryer

// An integer value representing the logging level. The default log level
Expand Down
19 changes: 0 additions & 19 deletions aws/connection_reset_error.go

This file was deleted.

11 changes: 0 additions & 11 deletions aws/connection_reset_error_other.go

This file was deleted.

9 changes: 0 additions & 9 deletions aws/connection_reset_error_other_test.go

This file was deleted.

11 changes: 0 additions & 11 deletions aws/connection_reset_error_test.go

This file was deleted.

25 changes: 0 additions & 25 deletions aws/context.go

This file was deleted.

Loading

0 comments on commit dc3c876

Please sign in to comment.