├── codeship-services.yml ├── .dockerignore ├── codeship-steps.yml ├── Dockerfile ├── .gitignore ├── LICENSE ├── examples ├── simple │ └── simple_example.go └── network │ └── network_example.go ├── README.md ├── retro.go └── retro_test.go /codeship-services.yml: -------------------------------------------------------------------------------- 1 | test: 2 | build: ./ 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | LICENSE 3 | *sublime* 4 | *yml 5 | *.aes 6 | .git 7 | -------------------------------------------------------------------------------- /codeship-steps.yml: -------------------------------------------------------------------------------- 1 | - service: test 2 | type: serial 3 | steps: 4 | - command: go test ./... 5 | - command: go vet ./... 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | COPY ./ /go/src/github.com/codeship/retro/ 4 | WORKDIR /go/src/github.com/codeship/retro 5 | 6 | RUN go get ./... 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.aes 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Codeship 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/simple/simple_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/codeship/go-retro" 9 | "github.com/satori/go.uuid" 10 | ) 11 | 12 | var ( 13 | // ErrNilMap is returned when an uninitialized data map is provided 14 | ErrNilMap = errors.New("error: data map was nil") 15 | 16 | // ErrKeyExists is returned when a key is re-used. This error is retryable and will retry 5 times without sleeping 17 | ErrKeyExists = retro.NewStaticRetryableError(errors.New("error: key exists"), 5, 0) 18 | ) 19 | 20 | // Stores all ints from 0-99 under random unique keys in a map 21 | func main() { 22 | data := map[string]int{} 23 | 24 | for i := 0; i < 100; i++ { 25 | err := retro.DoWithRetry(func() error { 26 | return storeInt(data, i) 27 | }) 28 | if err != nil { 29 | fmt.Printf("FATAL: Failed to store %d: %s\n", i, err.Error()) 30 | os.Exit(1) 31 | } 32 | } 33 | 34 | } 35 | 36 | func storeInt(data map[string]int, i int) error { 37 | if data == nil { 38 | return ErrNilMap 39 | } 40 | 41 | key := uuid.NewV4().String() 42 | _, ok := data[key] 43 | if ok { 44 | return ErrKeyExists 45 | } 46 | data[key] = i 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /examples/network/network_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | 9 | "github.com/codeship/go-retro" 10 | ) 11 | 12 | var ( 13 | // ErrInvalidVersion is returned when the wrong version is provided. This error is not retryable 14 | ErrInvalidVersion = errors.New("error: invalid version") 15 | 16 | // ErrNetwork is returned when a network error occurs. This error is retryable and will retry 5 times sleeping for 3 seconds each time 17 | ErrNetwork = retro.NewStaticRetryableError(errors.New("error: failed to connect"), 5, 3) 18 | 19 | // ErrNotReady is returned when a resource is not ready. This error is retryable and will retry 5 times with an incremental backoff 20 | ErrNotReady = retro.NewBackoffRetryableError(errors.New("error: resource not ready"), 5) 21 | ) 22 | 23 | // Gets a server version by id, and tries to use it until it is ready 24 | func main() { 25 | serverID := "abc123" 26 | var version string 27 | err := retro.DoWithRetry(func() error { 28 | var e error 29 | version, e = getServer(serverID) 30 | if e != nil { 31 | return e 32 | } 33 | fmt.Printf("Server version %s\n", version) 34 | return nil 35 | }) 36 | if err != nil { 37 | fmt.Printf("FATAL: Failed to get server info %s\n", err.Error()) 38 | os.Exit(1) 39 | } 40 | 41 | err = retro.DoWithRetry(func() error { 42 | return useServer(serverID, version) 43 | }) 44 | if err != nil { 45 | fmt.Printf("FATAL: Failed to use server %s\n", err.Error()) 46 | os.Exit(1) 47 | } 48 | 49 | } 50 | 51 | func getServer(id string) (string, error) { 52 | // return makeCurlRequestTODO() 53 | if maybeFail() { 54 | // return a intermittent network error which we can retry 55 | return "", ErrNetwork 56 | } else if maybeFail() { 57 | // return an incompatibility error which we cannot 58 | return "", ErrInvalidVersion 59 | } 60 | return "1", nil 61 | } 62 | func useServer(id, version string) error { 63 | // return makeCurlRequestTODO() 64 | if maybeFail() { 65 | // return an error which should resolve after an unknown amount of time 66 | return ErrNotReady 67 | } 68 | return nil 69 | } 70 | 71 | func maybeFail() bool { 72 | return rand.Intn(5) == 0 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Retro - Retryable errors in Golang 2 | 3 | :warning: **Note:** This library is no longer maintained :warning: 4 | 5 | [ ![Codeship Status for codeship/go-retro](https://codeship.com/projects/7a5b0350-1b79-0134-e8c7-265a91e3d879/status?branch=master)](https://codeship.com/projects/159666) 6 | 7 | Retry allows you to wrap failure prone sections of code in retry blocks, and conditionally retry different types of errors. To retry an error within a block, simply return a custom error type matching the provided interface. By default, wrapping code in a retro block will have no effect. 8 | 9 | ### Usage 10 | 11 | ``` 12 | // Wrap your code block with retro 13 | finalErr := retro.DoWithRetry(func() error{ 14 | // Hit an external API 15 | return DoMyFunc(args) 16 | }) 17 | ``` 18 | 19 | Any errors matching the `RetryableError` interface bubbled up will automatically retry the code block. 20 | 21 | 22 | For more detailed examples see the [examples folder](https://github.com/codeship/go-retro/blob/master/examples). 23 | 24 | Independent sections of code within a chain can be wrapped conditionally in retro blocks as needed. You should always try and keep retry blocks as small as possible to reduce code and requests being re-run. 25 | 26 | ### Retrying 27 | 28 | You can use two types of provided error retry patterns, or create your own: 29 | 30 | #### Static retryable errors 31 | 32 | These errors retry X times every Y seconds. To create a static retryable error use `retro.NewStaticRetryableError(err error, maxAttempts, waitSeconds int)`. For generic errors you can reference a shared variable, for dynamic errors you can create an error with the relevant base error message each time. Error retrying state is stored in the retro loop, not the RetryableError. 33 | 34 | #### Backoff retryable errors 35 | 36 | These errors use quadrilateral equation based on Sidekiq retries to increasingly space out subsequent attempts. To create a backoff retryable error use `retro.NewBackoffRetryableError(err error, maxAttempts int)`. For generic errors you can reference a shared variable, for dynamic errors you can create an error with the relevant base error message each time. Error retrying state is stored in the retro loop, not the RetryableError. 37 | 38 | ### Retry attempts 39 | 40 | The retro retry loop will keep track of how many times it has looped. Any time it gets an error it compares the state against the allowed retry conditions for the latest error. This means that should the loop initially retry with an error allowing ten retries, if the second error indicates only two retries or fewer are allowed, then the loop will no longer retry since it has already retried twice. 41 | -------------------------------------------------------------------------------- /retro.go: -------------------------------------------------------------------------------- 1 | package retro 2 | 3 | import ( 4 | "math" 5 | "regexp" 6 | "time" 7 | ) 8 | 9 | // RetryableError is an interface for any kind of error allowing retries 10 | type RetryableError interface { 11 | Error() string 12 | MaxAttempts() int 13 | Wait(int) 14 | } 15 | 16 | // ErrorCreator is a type of function that creates retryable errors from standard errors 17 | type ErrorCreator func(error) RetryableError 18 | 19 | type backoffRetryableError struct { 20 | error 21 | maxAttempts int 22 | } 23 | 24 | func (err *backoffRetryableError) MaxAttempts() int { 25 | return err.maxAttempts 26 | } 27 | 28 | func (err *backoffRetryableError) Wait(count int) { 29 | backoffInt := int(math.Pow(float64(count), 4.0)) + count + 10 30 | time.Sleep(time.Duration(backoffInt) * time.Second) 31 | } 32 | 33 | // NewBackoffRetryableError creates a RetryableError which will retry up to maxAttempts times 34 | // using an exponential backoff 35 | func NewBackoffRetryableError(err error, maxAttempts int) RetryableError { 36 | return &backoffRetryableError{ 37 | err, 38 | maxAttempts, 39 | } 40 | } 41 | 42 | type staticRetryableError struct { 43 | error 44 | maxAttempts int 45 | waitSeconds time.Duration 46 | } 47 | 48 | func (err *staticRetryableError) MaxAttempts() int { 49 | return err.maxAttempts 50 | } 51 | 52 | func (err *staticRetryableError) Wait(count int) { 53 | time.Sleep(err.waitSeconds * time.Second) 54 | } 55 | 56 | // NewStaticRetryableError creates a RetryableError which will retry up 57 | // to maxAttempts times sleeping waitSeconds in between tries 58 | func NewStaticRetryableError(err error, maxAttempts, waitSeconds int) RetryableError { 59 | return &staticRetryableError{err, maxAttempts, time.Duration(waitSeconds)} 60 | } 61 | 62 | type logarithmicRetryableError struct { 63 | error 64 | maxAttempts int 65 | baseOffset float64 66 | scale float64 67 | power float64 68 | } 69 | 70 | func (err *logarithmicRetryableError) MaxAttempts() int { 71 | return err.maxAttempts 72 | } 73 | 74 | func (err *logarithmicRetryableError) Wait(count int) { 75 | backoffFloat := err.scale*math.Log(math.Pow(float64(count), err.power)) + err.baseOffset 76 | time.Sleep(time.Duration(backoffFloat) * time.Second) 77 | } 78 | 79 | // NewLogarithmicRetryableError creates a RetryableError which will retry up 80 | // to maxAttempts times, with a sleep pattern defined by a logarithmic curve 81 | func NewLogarithmicRetryableError(err error, maxAttempts int, baseOffset, scale, power float64) RetryableError { 82 | return &logarithmicRetryableError{err, maxAttempts, baseOffset, scale, power} 83 | } 84 | 85 | // DoWithRetry will execute a function as many times as is dictated by any 86 | // retryable errors propagated by the function 87 | func DoWithRetry(f func() error) error { 88 | var err error 89 | try := true 90 | handler := &retryHandler{} 91 | for try { 92 | try, err = handler.Try(f) 93 | } 94 | return err 95 | } 96 | 97 | type retryHandler struct { 98 | attempts int 99 | } 100 | 101 | func (handler *retryHandler) Try(f func() error) (bool, error) { 102 | err := f() 103 | if errRetry, ok := err.(RetryableError); ok { 104 | retrying := handler.attempts < errRetry.MaxAttempts() 105 | if retrying { 106 | errRetry.Wait(handler.attempts) 107 | } 108 | handler.attempts++ 109 | return retrying, errRetry 110 | } 111 | return false, err 112 | } 113 | 114 | // WrapRetryableError takes an error, and given a set of retryable error regexes, returns 115 | // a suitable error type, either a standard error or a retryable error 116 | func WrapRetryableError(err error, errorList []*regexp.Regexp, errorCreator ErrorCreator) error { 117 | if err == nil { 118 | return nil 119 | } 120 | for _, errorRegex := range errorList { 121 | if errorRegex.MatchString(err.Error()) { 122 | return errorCreator(err) 123 | } 124 | } 125 | return err 126 | } 127 | -------------------------------------------------------------------------------- /retro_test.go: -------------------------------------------------------------------------------- 1 | package retro 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "testing" 7 | ) 8 | 9 | type stubRetryableError struct { 10 | error 11 | maxAttempts int 12 | sleepFunc func(int) 13 | } 14 | 15 | func (err *stubRetryableError) MaxAttempts() int { 16 | return err.maxAttempts 17 | } 18 | 19 | func (err *stubRetryableError) Wait(count int) { 20 | err.sleepFunc(count) 21 | } 22 | 23 | func TestRetryHandlerTryWithSuccess(t *testing.T) { 24 | handler := &retryHandler{} 25 | successfulFunc := func() error { 26 | return nil 27 | } 28 | retry, err := handler.Try(successfulFunc) 29 | if retry { 30 | t.Errorf("retry was true, expected false") 31 | } 32 | if err != nil { 33 | t.Errorf("error was %s, expected nil", err.Error()) 34 | } 35 | } 36 | 37 | func TestRetryHandlerTryNonRetryable(t *testing.T) { 38 | handler := &retryHandler{} 39 | nonRetryableError := errors.New("testErr") 40 | nonRetryableFunc := func() error { 41 | return nonRetryableError 42 | } 43 | retry, err := handler.Try(nonRetryableFunc) 44 | if retry { 45 | t.Errorf("retry was true, expected false") 46 | } 47 | if err != nonRetryableError { 48 | t.Errorf("error was %s, expected %s", err.Error(), nonRetryableError.Error()) 49 | } 50 | } 51 | 52 | func TestRetryHandlerTryRetryable(t *testing.T) { 53 | handler := &retryHandler{} 54 | receivedCounts := []int{} 55 | baseError := errors.New("foobar") 56 | retryableError := &stubRetryableError{ 57 | baseError, 58 | 2, 59 | func(count int) { 60 | receivedCounts = append(receivedCounts, count) 61 | }, 62 | } 63 | retryableFunc := func() error { 64 | return retryableError 65 | } 66 | 67 | retry, err := handler.Try(retryableFunc) 68 | if !retry { 69 | t.Errorf("retry was false, expected true") 70 | } 71 | if receivedCounts[0] != 0 { 72 | t.Errorf("receivedCounts[0] was %d, expected %d", receivedCounts[0], 0) 73 | } 74 | if len(receivedCounts) != 1 { 75 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 1) 76 | } 77 | if err != retryableError { 78 | t.Errorf("error was %s, expected %s", err.Error(), retryableError.Error()) 79 | } 80 | retry, err = handler.Try(retryableFunc) 81 | if !retry { 82 | t.Errorf("retry was false, expected true") 83 | } 84 | if receivedCounts[1] != 1 { 85 | t.Errorf("receivedCounts[1] was %d, expected %d", receivedCounts[1], 1) 86 | } 87 | if len(receivedCounts) != 2 { 88 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 2) 89 | } 90 | if err != retryableError { 91 | t.Errorf("error was %s, expected %s", err.Error(), retryableError.Error()) 92 | } 93 | retry, err = handler.Try(retryableFunc) 94 | if retry { 95 | t.Errorf("retry was true, expected false") 96 | } 97 | 98 | if len(receivedCounts) != 2 { 99 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 2) 100 | } 101 | if err != retryableError { 102 | t.Errorf("error was %s, expected %s", err.Error(), retryableError.Error()) 103 | } 104 | } 105 | 106 | func TestRetryHandlerTryRetryableEventualSuccess(t *testing.T) { 107 | handler := &retryHandler{} 108 | receivedCounts := []int{} 109 | baseError := errors.New("foobar") 110 | retryableError := &stubRetryableError{ 111 | baseError, 112 | 2, 113 | func(count int) { 114 | receivedCounts = append(receivedCounts, count) 115 | }, 116 | } 117 | returnIndex := 0 118 | returns := []error{retryableError, nil} 119 | retryableFunc := func() error { 120 | returnIndex++ 121 | return returns[returnIndex-1] 122 | } 123 | 124 | retry, err := handler.Try(retryableFunc) 125 | if !retry { 126 | t.Errorf("retry was false, expected true") 127 | } 128 | if receivedCounts[0] != 0 { 129 | t.Errorf("receivedCounts[0] was %d, expected %d", receivedCounts[0], 0) 130 | } 131 | 132 | if len(receivedCounts) != 1 { 133 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 1) 134 | } 135 | if err != retryableError { 136 | t.Errorf("error was %s, expected %s", err.Error(), retryableError.Error()) 137 | } 138 | retry, err = handler.Try(retryableFunc) 139 | if retry { 140 | t.Errorf("retry was true, expected false") 141 | } 142 | if len(receivedCounts) != 1 { 143 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 1) 144 | } 145 | if err != nil { 146 | t.Errorf("error was %s, expected nil", err.Error()) 147 | } 148 | } 149 | 150 | func TestRetryHandlerTryRetryableEventualFailure(t *testing.T) { 151 | handler := &retryHandler{} 152 | receivedCounts := []int{} 153 | baseError := errors.New("foobar") 154 | retryableError := &stubRetryableError{ 155 | baseError, 156 | 2, 157 | func(count int) { 158 | receivedCounts = append(receivedCounts, count) 159 | }, 160 | } 161 | returnIndex := 0 162 | returns := []error{retryableError, baseError} 163 | retryableFunc := func() error { 164 | returnIndex++ 165 | return returns[returnIndex-1] 166 | } 167 | 168 | retry, err := handler.Try(retryableFunc) 169 | if !retry { 170 | t.Errorf("retry was false, expected true") 171 | } 172 | if receivedCounts[0] != 0 { 173 | t.Errorf("receivedCounts[0] was %d, expected %d", receivedCounts[0], 0) 174 | } 175 | if len(receivedCounts) != 1 { 176 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 1) 177 | } 178 | if err != retryableError { 179 | t.Errorf("error was %s, expected %s", err.Error(), retryableError.Error()) 180 | } 181 | retry, err = handler.Try(retryableFunc) 182 | if retry { 183 | t.Errorf("retry was true, expected false") 184 | } 185 | if len(receivedCounts) != 1 { 186 | t.Errorf("len(receivedCounts) was %d, expected %d", len(receivedCounts), 1) 187 | } 188 | if err != baseError { 189 | t.Errorf("error was %s, expected %s", err.Error(), baseError.Error()) 190 | } 191 | } 192 | 193 | func TestDoWithRetry(t *testing.T) { 194 | timesCalled := 0 195 | retryableFunc := func() error { 196 | timesCalled++ 197 | return nil 198 | } 199 | 200 | err := DoWithRetry(retryableFunc) 201 | if err != nil { 202 | t.Errorf("error was %s, expected nil", err.Error()) 203 | } 204 | if timesCalled != 1 { 205 | t.Errorf("timesCalled was %d, expected %d", timesCalled, 1) 206 | } 207 | } 208 | 209 | func TestDoWithRetryEventualFailure(t *testing.T) { 210 | baseError := errors.New("foobar") 211 | retryableError := &stubRetryableError{ 212 | baseError, 213 | 2, 214 | func(count int) {}, 215 | } 216 | returnIndex := 0 217 | returns := []error{retryableError, baseError} 218 | retryableFunc := func() error { 219 | returnIndex++ 220 | return returns[returnIndex-1] 221 | } 222 | 223 | err := DoWithRetry(retryableFunc) 224 | if err != baseError { 225 | t.Errorf("error was %s, expected %s", err.Error(), baseError.Error()) 226 | } 227 | } 228 | 229 | func TestDoWithRetryEventualSuccess(t *testing.T) { 230 | baseError := errors.New("foobar") 231 | retryableError := &stubRetryableError{ 232 | baseError, 233 | 2, 234 | func(count int) {}, 235 | } 236 | returnIndex := 0 237 | returns := []error{retryableError, nil} 238 | retryableFunc := func() error { 239 | returnIndex++ 240 | return returns[returnIndex-1] 241 | } 242 | 243 | err := DoWithRetry(retryableFunc) 244 | if err != nil { 245 | t.Errorf("error was %s, expected nil", err.Error()) 246 | } 247 | } 248 | 249 | func TestWrapRetryableErrors(t *testing.T) { 250 | err := errors.New("foobar") 251 | errorList := []*regexp.Regexp{ 252 | regexp.MustCompile("foo"), 253 | } 254 | err1 := WrapRetryableError(err, errorList, func(e error) RetryableError { 255 | return NewBackoffRetryableError(e, 1) 256 | }) 257 | 258 | _, ok := err1.(RetryableError) 259 | if err.Error() != err1.Error() { 260 | t.Errorf("error was %s, expected %s", err.Error(), err1.Error()) 261 | } 262 | if !ok { 263 | t.Errorf("ok was false, expected true") 264 | } 265 | } 266 | 267 | func TestWrapRetryableFailure(t *testing.T) { 268 | err := errors.New("foobar") 269 | errorList := []*regexp.Regexp{ 270 | regexp.MustCompile("fooop"), 271 | } 272 | err1 := WrapRetryableError(err, errorList, func(e error) RetryableError { 273 | return NewBackoffRetryableError(e, 1) 274 | }) 275 | 276 | _, ok := err1.(RetryableError) 277 | if err != err1 { 278 | t.Errorf("error was %s, expected %s", err.Error(), err1.Error()) 279 | } 280 | if ok { 281 | t.Errorf("ok was true, expected false") 282 | } 283 | } 284 | 285 | func TestWrapRetryableNil(t *testing.T) { 286 | errorList := []*regexp.Regexp{ 287 | regexp.MustCompile("fooop"), 288 | } 289 | err1 := WrapRetryableError(nil, errorList, func(e error) RetryableError { 290 | return NewBackoffRetryableError(e, 1) 291 | }) 292 | 293 | if err1 != nil { 294 | t.Errorf("error was %s, expected nil", err1.Error()) 295 | } 296 | } 297 | 298 | func TestBackoffRetryableError(t *testing.T) { 299 | baseErr := errors.New("foobar") 300 | maxAttempts := 10 301 | err := NewBackoffRetryableError(baseErr, maxAttempts) 302 | if err == nil { 303 | t.Error("retryable error was nil") 304 | } 305 | if maxAttempts != err.MaxAttempts() { 306 | t.Errorf("Incorrect MaxAttempts returned, expected %d, received %d", maxAttempts, err.MaxAttempts()) 307 | } 308 | } 309 | 310 | func TestStaticRetryableError(t *testing.T) { 311 | baseErr := errors.New("foobar") 312 | maxAttempts := 10 313 | err := NewStaticRetryableError(baseErr, maxAttempts, 4) 314 | if err == nil { 315 | t.Error("retryable error was nil") 316 | } 317 | if maxAttempts != err.MaxAttempts() { 318 | t.Errorf("Incorrect MaxAttempts returned, expected %d, received %d", maxAttempts, err.MaxAttempts()) 319 | } 320 | } 321 | 322 | func TestLogarithmicRetryableError(t *testing.T) { 323 | baseErr := errors.New("foobar") 324 | maxAttempts := 10 325 | err := NewLogarithmicRetryableError(baseErr, maxAttempts, 1, 2, 3) 326 | if err == nil { 327 | t.Error("retryable error was nil") 328 | } 329 | if maxAttempts != err.MaxAttempts() { 330 | t.Errorf("Incorrect MaxAttempts returned, expected %d, received %d", maxAttempts, err.MaxAttempts()) 331 | } 332 | } 333 | --------------------------------------------------------------------------------