You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.1 KiB
187 lines
5.1 KiB
package retry
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
)
|
|
|
|
// IsErrorRetryable provides the interface of an implementation to determine if
|
|
// a error as the result of an operation is retryable.
|
|
type IsErrorRetryable interface {
|
|
IsErrorRetryable(error) aws.Ternary
|
|
}
|
|
|
|
// IsErrorRetryables is a collection of checks to determine of the error is
|
|
// retryable. Iterates through the checks and returns the state of retryable
|
|
// if any check returns something other than unknown.
|
|
type IsErrorRetryables []IsErrorRetryable
|
|
|
|
// IsErrorRetryable returns if the error is retryable if any of the checks in
|
|
// the list return a value other than unknown.
|
|
func (r IsErrorRetryables) IsErrorRetryable(err error) aws.Ternary {
|
|
for _, re := range r {
|
|
if v := re.IsErrorRetryable(err); v != aws.UnknownTernary {
|
|
return v
|
|
}
|
|
}
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
// IsErrorRetryableFunc wraps a function with the IsErrorRetryable interface.
|
|
type IsErrorRetryableFunc func(error) aws.Ternary
|
|
|
|
// IsErrorRetryable returns if the error is retryable.
|
|
func (fn IsErrorRetryableFunc) IsErrorRetryable(err error) aws.Ternary {
|
|
return fn(err)
|
|
}
|
|
|
|
// RetryableError is an IsErrorRetryable implementation which uses the
|
|
// optional interface Retryable on the error value to determine if the error is
|
|
// retryable.
|
|
type RetryableError struct{}
|
|
|
|
// IsErrorRetryable returns if the error is retryable if it satisfies the
|
|
// Retryable interface, and returns if the attempt should be retried.
|
|
func (RetryableError) IsErrorRetryable(err error) aws.Ternary {
|
|
var v interface{ RetryableError() bool }
|
|
|
|
if !errors.As(err, &v) {
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
return aws.BoolTernary(v.RetryableError())
|
|
}
|
|
|
|
// NoRetryCanceledError detects if the error was an request canceled error and
|
|
// returns if so.
|
|
type NoRetryCanceledError struct{}
|
|
|
|
// IsErrorRetryable returns the error is not retryable if the request was
|
|
// canceled.
|
|
func (NoRetryCanceledError) IsErrorRetryable(err error) aws.Ternary {
|
|
var v interface{ CanceledError() bool }
|
|
|
|
if !errors.As(err, &v) {
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
if v.CanceledError() {
|
|
return aws.FalseTernary
|
|
}
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
// RetryableConnectionError determines if the underlying error is an HTTP
|
|
// connection and returns if it should be retried.
|
|
//
|
|
// Includes errors such as connection reset, connection refused, net dial,
|
|
// temporary, and timeout errors.
|
|
type RetryableConnectionError struct{}
|
|
|
|
// IsErrorRetryable returns if the error is caused by and HTTP connection
|
|
// error, and should be retried.
|
|
func (r RetryableConnectionError) IsErrorRetryable(err error) aws.Ternary {
|
|
if err == nil {
|
|
return aws.UnknownTernary
|
|
}
|
|
var retryable bool
|
|
|
|
var conErr interface{ ConnectionError() bool }
|
|
var tempErr interface{ Temporary() bool }
|
|
var timeoutErr interface{ Timeout() bool }
|
|
var urlErr *url.Error
|
|
var netOpErr *net.OpError
|
|
|
|
switch {
|
|
case errors.As(err, &conErr) && conErr.ConnectionError():
|
|
retryable = true
|
|
|
|
case strings.Contains(err.Error(), "connection reset"):
|
|
retryable = true
|
|
|
|
case errors.As(err, &urlErr):
|
|
// Refused connections should be retried as the service may not yet be
|
|
// running on the port. Go TCP dial considers refused connections as
|
|
// not temporary.
|
|
if strings.Contains(urlErr.Error(), "connection refused") {
|
|
retryable = true
|
|
} else {
|
|
return r.IsErrorRetryable(errors.Unwrap(urlErr))
|
|
}
|
|
|
|
case errors.As(err, &netOpErr):
|
|
// Network dial, or temporary network errors are always retryable.
|
|
if strings.EqualFold(netOpErr.Op, "dial") || netOpErr.Temporary() {
|
|
retryable = true
|
|
} else {
|
|
return r.IsErrorRetryable(errors.Unwrap(netOpErr))
|
|
}
|
|
|
|
case errors.As(err, &tempErr) && tempErr.Temporary():
|
|
// Fallback to the generic temporary check, with temporary errors
|
|
// retryable.
|
|
retryable = true
|
|
|
|
case errors.As(err, &timeoutErr) && timeoutErr.Timeout():
|
|
// Fallback to the generic timeout check, with timeout errors
|
|
// retryable.
|
|
retryable = true
|
|
|
|
default:
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
return aws.BoolTernary(retryable)
|
|
|
|
}
|
|
|
|
// RetryableHTTPStatusCode provides a IsErrorRetryable based on HTTP status
|
|
// codes.
|
|
type RetryableHTTPStatusCode struct {
|
|
Codes map[int]struct{}
|
|
}
|
|
|
|
// IsErrorRetryable return if the passed in error is retryable based on the
|
|
// HTTP status code.
|
|
func (r RetryableHTTPStatusCode) IsErrorRetryable(err error) aws.Ternary {
|
|
var v interface{ HTTPStatusCode() int }
|
|
|
|
if !errors.As(err, &v) {
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
_, ok := r.Codes[v.HTTPStatusCode()]
|
|
if !ok {
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
return aws.TrueTernary
|
|
}
|
|
|
|
// RetryableErrorCode determines if an attempt should be retried based on the
|
|
// API error code.
|
|
type RetryableErrorCode struct {
|
|
Codes map[string]struct{}
|
|
}
|
|
|
|
// IsErrorRetryable return if the error is retryable based on the error codes.
|
|
// Returns unknown if the error doesn't have a code or it is unknown.
|
|
func (r RetryableErrorCode) IsErrorRetryable(err error) aws.Ternary {
|
|
var v interface{ ErrorCode() string }
|
|
|
|
if !errors.As(err, &v) {
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
_, ok := r.Codes[v.ErrorCode()]
|
|
if !ok {
|
|
return aws.UnknownTernary
|
|
}
|
|
|
|
return aws.TrueTernary
|
|
}
|