git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/retry/retry.go (about) 1 /* 2 Simple library for retry mechanism 3 4 slightly inspired by [Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry) 5 6 # SYNOPSIS 7 8 http get with retry: 9 10 url := "http://example.com" 11 var body []byte 12 13 err := retry.Do( 14 func() error { 15 resp, err := http.Get(url) 16 if err != nil { 17 return err 18 } 19 defer resp.Body.Close() 20 body, err = ioutil.ReadAll(resp.Body) 21 if err != nil { 22 return err 23 } 24 25 return nil 26 }, 27 ) 28 29 fmt.Println(body) 30 31 [next examples](https://git.sr.ht/~pingoo/stdx/retrytree/master/examples) 32 33 # SEE ALSO 34 35 * [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly complicated interface. 36 37 * [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for http calls with retries and backoff 38 39 * [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java. Really complicated interface. 40 41 * [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, slightly similar as this package, don't have 'simple' `Retry` method 42 43 * [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me) 44 45 # BREAKING CHANGES 46 47 * 4.0.0 48 - infinity retry is possible by set `Attempts(0)` by PR [#49](https://git.sr.ht/~pingoo/stdx/retrypull/49) 49 50 * 3.0.0 51 - `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go). 52 53 * 1.0.2 -> 2.0.0 54 - argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore) 55 - function `retry.Units` are removed 56 - [more about this breaking change](https://git.sr.ht/~pingoo/stdx/retryissues/7) 57 58 * 0.3.0 -> 1.0.0 59 - `retry.Retry` function are changed to `retry.Do` function 60 - `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`) 61 */ 62 package retry 63 64 import ( 65 "context" 66 "fmt" 67 "strings" 68 "time" 69 ) 70 71 // Function signature of retryable function 72 type RetryableFunc func() error 73 74 func Do(retryableFunc RetryableFunc, opts ...Option) error { 75 var n uint 76 77 // default 78 config := newDefaultRetryConfig() 79 80 // apply opts 81 for _, opt := range opts { 82 opt(config) 83 } 84 85 if err := config.context.Err(); err != nil { 86 return err 87 } 88 89 // Setting attempts to 0 means we'll retry until we succeed 90 if config.attempts == 0 { 91 for err := retryableFunc(); err != nil; err = retryableFunc() { 92 n++ 93 94 config.onRetry(n, err) 95 96 <-time.After(delay(config, n, err)) 97 } 98 99 return nil 100 } 101 102 var errorLog Error 103 if !config.lastErrorOnly { 104 errorLog = make(Error, config.attempts) 105 } else { 106 errorLog = make(Error, 1) 107 } 108 109 lastErrIndex := n 110 for n < config.attempts { 111 err := retryableFunc() 112 113 if err != nil { 114 errorLog[lastErrIndex] = unpackUnrecoverable(err) 115 116 if !config.retryIf(err) { 117 break 118 } 119 120 config.onRetry(n, err) 121 122 // if this is last attempt - don't wait 123 if n == config.attempts-1 { 124 break 125 } 126 127 select { 128 case <-time.After(delay(config, n, err)): 129 case <-config.context.Done(): 130 if config.lastErrorOnly { 131 return config.context.Err() 132 } 133 errorLog[n] = config.context.Err() 134 return errorLog 135 } 136 137 } else { 138 return nil 139 } 140 141 n++ 142 if !config.lastErrorOnly { 143 lastErrIndex = n 144 } 145 } 146 147 if config.lastErrorOnly { 148 return errorLog[lastErrIndex] 149 } 150 return errorLog 151 } 152 153 func newDefaultRetryConfig() *Config { 154 return &Config{ 155 attempts: uint(10), 156 delay: 100 * time.Millisecond, 157 maxJitter: 100 * time.Millisecond, 158 onRetry: func(n uint, err error) {}, 159 retryIf: IsRecoverable, 160 delayType: CombineDelay(BackOffDelay, RandomDelay), 161 lastErrorOnly: false, 162 context: context.Background(), 163 } 164 } 165 166 // Error type represents list of errors in retry 167 type Error []error 168 169 // Error method return string representation of Error 170 // It is an implementation of error interface 171 func (e Error) Error() string { 172 logWithNumber := make([]string, lenWithoutNil(e)) 173 for i, l := range e { 174 if l != nil { 175 logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error()) 176 } 177 } 178 179 return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n")) 180 } 181 182 func lenWithoutNil(e Error) (count int) { 183 for _, v := range e { 184 if v != nil { 185 count++ 186 } 187 } 188 189 return 190 } 191 192 // WrappedErrors returns the list of errors that this Error is wrapping. 193 // It is an implementation of the `errwrap.Wrapper` interface 194 // in package [errwrap](https://github.com/hashicorp/errwrap) so that 195 // `retry.Error` can be used with that library. 196 func (e Error) WrappedErrors() []error { 197 return e 198 } 199 200 type unrecoverableError struct { 201 error 202 } 203 204 // Unrecoverable wraps an error in `unrecoverableError` struct 205 func Unrecoverable(err error) error { 206 return unrecoverableError{err} 207 } 208 209 // IsRecoverable checks if error is an instance of `unrecoverableError` 210 func IsRecoverable(err error) bool { 211 _, isUnrecoverable := err.(unrecoverableError) 212 return !isUnrecoverable 213 } 214 215 func unpackUnrecoverable(err error) error { 216 if unrecoverable, isUnrecoverable := err.(unrecoverableError); isUnrecoverable { 217 return unrecoverable.error 218 } 219 220 return err 221 } 222 223 func delay(config *Config, n uint, err error) time.Duration { 224 delayTime := config.delayType(n, err, config) 225 if config.maxDelay > 0 && delayTime > config.maxDelay { 226 delayTime = config.maxDelay 227 } 228 229 return delayTime 230 }