src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

retryable_error.go (5581B)


      1 package retry
      2 
      3 import (
      4 	"errors"
      5 	"net"
      6 	"net/url"
      7 	"strings"
      8 
      9 	"github.com/aws/aws-sdk-go-v2/aws"
     10 )
     11 
     12 // IsErrorRetryable provides the interface of an implementation to determine if
     13 // a error as the result of an operation is retryable.
     14 type IsErrorRetryable interface {
     15 	IsErrorRetryable(error) aws.Ternary
     16 }
     17 
     18 // IsErrorRetryables is a collection of checks to determine of the error is
     19 // retryable.  Iterates through the checks and returns the state of retryable
     20 // if any check returns something other than unknown.
     21 type IsErrorRetryables []IsErrorRetryable
     22 
     23 // IsErrorRetryable returns if the error is retryable if any of the checks in
     24 // the list return a value other than unknown.
     25 func (r IsErrorRetryables) IsErrorRetryable(err error) aws.Ternary {
     26 	for _, re := range r {
     27 		if v := re.IsErrorRetryable(err); v != aws.UnknownTernary {
     28 			return v
     29 		}
     30 	}
     31 	return aws.UnknownTernary
     32 }
     33 
     34 // IsErrorRetryableFunc wraps a function with the IsErrorRetryable interface.
     35 type IsErrorRetryableFunc func(error) aws.Ternary
     36 
     37 // IsErrorRetryable returns if the error is retryable.
     38 func (fn IsErrorRetryableFunc) IsErrorRetryable(err error) aws.Ternary {
     39 	return fn(err)
     40 }
     41 
     42 // RetryableError is an IsErrorRetryable implementation which uses the
     43 // optional interface Retryable on the error value to determine if the error is
     44 // retryable.
     45 type RetryableError struct{}
     46 
     47 // IsErrorRetryable returns if the error is retryable if it satisfies the
     48 // Retryable interface, and returns if the attempt should be retried.
     49 func (RetryableError) IsErrorRetryable(err error) aws.Ternary {
     50 	var v interface{ RetryableError() bool }
     51 
     52 	if !errors.As(err, &v) {
     53 		return aws.UnknownTernary
     54 	}
     55 
     56 	return aws.BoolTernary(v.RetryableError())
     57 }
     58 
     59 // NoRetryCanceledError detects if the error was an request canceled error and
     60 // returns if so.
     61 type NoRetryCanceledError struct{}
     62 
     63 // IsErrorRetryable returns the error is not retryable if the request was
     64 // canceled.
     65 func (NoRetryCanceledError) IsErrorRetryable(err error) aws.Ternary {
     66 	var v interface{ CanceledError() bool }
     67 
     68 	if !errors.As(err, &v) {
     69 		return aws.UnknownTernary
     70 	}
     71 
     72 	if v.CanceledError() {
     73 		return aws.FalseTernary
     74 	}
     75 	return aws.UnknownTernary
     76 }
     77 
     78 // RetryableConnectionError determines if the underlying error is an HTTP
     79 // connection and returns if it should be retried.
     80 //
     81 // Includes errors such as connection reset, connection refused, net dial,
     82 // temporary, and timeout errors.
     83 type RetryableConnectionError struct{}
     84 
     85 // IsErrorRetryable returns if the error is caused by and HTTP connection
     86 // error, and should be retried.
     87 func (r RetryableConnectionError) IsErrorRetryable(err error) aws.Ternary {
     88 	if err == nil {
     89 		return aws.UnknownTernary
     90 	}
     91 	var retryable bool
     92 
     93 	var conErr interface{ ConnectionError() bool }
     94 	var tempErr interface{ Temporary() bool }
     95 	var timeoutErr interface{ Timeout() bool }
     96 	var urlErr *url.Error
     97 	var netOpErr *net.OpError
     98 	var dnsError *net.DNSError
     99 
    100 	if errors.As(err, &dnsError) {
    101 		// NXDOMAIN errors should not be retried
    102 		if dnsError.IsNotFound {
    103 			return aws.BoolTernary(false)
    104 		}
    105 
    106 		// if !dnsError.Temporary(), error may or may not be temporary,
    107 		// (i.e. !Temporary() =/=> !retryable) so we should fall through to
    108 		// remaining checks
    109 		if dnsError.Temporary() {
    110 			return aws.BoolTernary(true)
    111 		}
    112 	}
    113 
    114 	switch {
    115 	case errors.As(err, &conErr) && conErr.ConnectionError():
    116 		retryable = true
    117 
    118 	case strings.Contains(err.Error(), "connection reset"):
    119 		retryable = true
    120 
    121 	case errors.As(err, &urlErr):
    122 		// Refused connections should be retried as the service may not yet be
    123 		// running on the port. Go TCP dial considers refused connections as
    124 		// not temporary.
    125 		if strings.Contains(urlErr.Error(), "connection refused") {
    126 			retryable = true
    127 		} else {
    128 			return r.IsErrorRetryable(errors.Unwrap(urlErr))
    129 		}
    130 
    131 	case errors.As(err, &netOpErr):
    132 		// Network dial, or temporary network errors are always retryable.
    133 		if strings.EqualFold(netOpErr.Op, "dial") || netOpErr.Temporary() {
    134 			retryable = true
    135 		} else {
    136 			return r.IsErrorRetryable(errors.Unwrap(netOpErr))
    137 		}
    138 
    139 	case errors.As(err, &tempErr) && tempErr.Temporary():
    140 		// Fallback to the generic temporary check, with temporary errors
    141 		// retryable.
    142 		retryable = true
    143 
    144 	case errors.As(err, &timeoutErr) && timeoutErr.Timeout():
    145 		// Fallback to the generic timeout check, with timeout errors
    146 		// retryable.
    147 		retryable = true
    148 
    149 	default:
    150 		return aws.UnknownTernary
    151 	}
    152 
    153 	return aws.BoolTernary(retryable)
    154 
    155 }
    156 
    157 // RetryableHTTPStatusCode provides a IsErrorRetryable based on HTTP status
    158 // codes.
    159 type RetryableHTTPStatusCode struct {
    160 	Codes map[int]struct{}
    161 }
    162 
    163 // IsErrorRetryable return if the passed in error is retryable based on the
    164 // HTTP status code.
    165 func (r RetryableHTTPStatusCode) IsErrorRetryable(err error) aws.Ternary {
    166 	var v interface{ HTTPStatusCode() int }
    167 
    168 	if !errors.As(err, &v) {
    169 		return aws.UnknownTernary
    170 	}
    171 
    172 	_, ok := r.Codes[v.HTTPStatusCode()]
    173 	if !ok {
    174 		return aws.UnknownTernary
    175 	}
    176 
    177 	return aws.TrueTernary
    178 }
    179 
    180 // RetryableErrorCode determines if an attempt should be retried based on the
    181 // API error code.
    182 type RetryableErrorCode struct {
    183 	Codes map[string]struct{}
    184 }
    185 
    186 // IsErrorRetryable return if the error is retryable based on the error codes.
    187 // Returns unknown if the error doesn't have a code or it is unknown.
    188 func (r RetryableErrorCode) IsErrorRetryable(err error) aws.Ternary {
    189 	var v interface{ ErrorCode() string }
    190 
    191 	if !errors.As(err, &v) {
    192 		return aws.UnknownTernary
    193 	}
    194 
    195 	_, ok := r.Codes[v.ErrorCode()]
    196 	if !ok {
    197 		return aws.UnknownTernary
    198 	}
    199 
    200 	return aws.TrueTernary
    201 }