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