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 }