├── .gitignore ├── LICENSE ├── README.md ├── doc.go ├── try.go └── try_test.go /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mat Ryer 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # try [![GoDoc](https://godoc.org/github.com/matryer/try?status.svg)](https://godoc.org/github.com/matryer/try) [![Go Report Card](https://goreportcard.com/badge/github.com/matryer/try)](https://goreportcard.com/report/github.com/matryer/try) 2 | 3 | Idiomatic Go retry package. Thanks to [@rowland](https://github.com/rowland) for code review. 4 | 5 | ``` 6 | go get gopkg.in/matryer/try.v1 7 | or 8 | drop gopkg.in/matryer/try.v1 9 | ``` 10 | 11 | * Learn more about [Drop](https://github.com/matryer/drop) 12 | 13 | ### Usage 14 | 15 | Just call `try.Do` with the function you want to retry in the event of an error: 16 | 17 | * Call `try.Do` that returns a `bool` indicating whether to retry or not, and an `error` 18 | * The `attempt` argument will start at 1 and count up 19 | * `try.Do` blocks until you return `false`, or a `nil` error 20 | * `try.Do` returns the last error or `nil` if it was successful 21 | 22 | ``` 23 | var value string 24 | err := try.Do(func(attempt int) (bool, error) { 25 | var err error 26 | value, err = SomeFunction() 27 | return attempt < 5, err // try 5 times 28 | }) 29 | if err != nil { 30 | log.Fatalln("error:", err) 31 | } 32 | ``` 33 | 34 | In the above example the function will be called repeatedly until error is `nil`, while `attempt < 5` (i.e. try 5 times) 35 | 36 | #### Retrying panics 37 | 38 | Try supports retrying in the event of a panic. 39 | 40 | * Use named return parameters 41 | * Set `retry` first 42 | * Defer the recovery code, and set `err` manually in the case of a panic 43 | * Use empty `return` statement at the end 44 | 45 | ``` 46 | var value string 47 | err := try.Do(func(attempt int) (retry bool, err error) { 48 | retry = attempt < 5 // try 5 times 49 | defer func() { 50 | if r := recover(); r != nil { 51 | err = errors.New(fmt.Sprintf("panic: %v", r)) 52 | } 53 | }() 54 | value, err = SomeFunction() 55 | return 56 | }) 57 | if err != nil { 58 | log.Fatalln("error:", err) 59 | } 60 | ``` 61 | 62 | #### Delay between retries 63 | 64 | To introduce a delay between retries, just make a `time.Sleep` call before you return from the function if you are returning an error. 65 | 66 | ``` 67 | var value string 68 | err := try.Do(func(attempt int) (bool, error) { 69 | var err error 70 | value, err = SomeFunction() 71 | if err != nil { 72 | time.Sleep(1 * time.Minute) // wait a minute 73 | } 74 | return attempt < 5, err 75 | }) 76 | if err != nil { 77 | log.Fatalln("error:", err) 78 | } 79 | ``` 80 | 81 | #### Maximum retry limit 82 | 83 | To avoid infinite loops, Try will ensure it only makes `try.MaxRetries` attempts. By default, this value is `10`, but you can change it: 84 | 85 | ``` 86 | try.MaxRetries = 20 87 | ``` 88 | 89 | To see if a `Do` operation failed due to reaching the limit, you can check the `error` with `try.IsMaxRetries(err)`. 90 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package try provides retry functionality. 2 | // var value string 3 | // err := try.Do(func(attempt int) (bool, error) { 4 | // var err error 5 | // value, err = SomeFunction() 6 | // return attempt < 5, err // try 5 times 7 | // }) 8 | // if err != nil { 9 | // log.Fatalln("error:", err) 10 | // } 11 | package try 12 | -------------------------------------------------------------------------------- /try.go: -------------------------------------------------------------------------------- 1 | package try 2 | 3 | import "errors" 4 | 5 | // MaxRetries is the maximum number of retries before bailing. 6 | var MaxRetries = 10 7 | 8 | var errMaxRetriesReached = errors.New("exceeded retry limit") 9 | 10 | // Func represents functions that can be retried. 11 | type Func func(attempt int) (retry bool, err error) 12 | 13 | // Do keeps trying the function until the second argument 14 | // returns false, or no error is returned. 15 | func Do(fn Func) error { 16 | var err error 17 | var cont bool 18 | attempt := 1 19 | for { 20 | cont, err = fn(attempt) 21 | if !cont || err == nil { 22 | break 23 | } 24 | attempt++ 25 | if attempt > MaxRetries { 26 | return errMaxRetriesReached 27 | } 28 | } 29 | return err 30 | } 31 | 32 | // IsMaxRetries checks whether the error is due to hitting the 33 | // maximum number of retries or not. 34 | func IsMaxRetries(err error) bool { 35 | return err == errMaxRetriesReached 36 | } 37 | -------------------------------------------------------------------------------- /try_test.go: -------------------------------------------------------------------------------- 1 | package try_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "testing" 8 | 9 | "github.com/cheekybits/is" 10 | "github.com/matryer/try" 11 | ) 12 | 13 | func TestTryExample(t *testing.T) { 14 | try.MaxRetries = 20 15 | SomeFunction := func() (string, error) { 16 | return "", nil 17 | } 18 | var value string 19 | err := try.Do(func(attempt int) (bool, error) { 20 | var err error 21 | value, err = SomeFunction() 22 | return attempt < 5, err // try 5 times 23 | }) 24 | if err != nil { 25 | log.Fatalln("error:", err) 26 | } 27 | } 28 | 29 | func TestTryExamplePanic(t *testing.T) { 30 | SomeFunction := func() (string, error) { 31 | panic("something went badly wrong") 32 | } 33 | var value string 34 | err := try.Do(func(attempt int) (retry bool, err error) { 35 | retry = attempt < 5 // try 5 times 36 | defer func() { 37 | if r := recover(); r != nil { 38 | err = errors.New(fmt.Sprintf("panic: %v", r)) 39 | } 40 | }() 41 | value, err = SomeFunction() 42 | return 43 | }) 44 | if err != nil { 45 | //log.Fatalln("error:", err) 46 | } 47 | } 48 | 49 | func TestTryDoSuccessful(t *testing.T) { 50 | is := is.New(t) 51 | callCount := 0 52 | err := try.Do(func(attempt int) (bool, error) { 53 | callCount++ 54 | return attempt < 5, nil 55 | }) 56 | is.NoErr(err) 57 | is.Equal(callCount, 1) 58 | } 59 | 60 | func TestTryDoFailed(t *testing.T) { 61 | is := is.New(t) 62 | theErr := errors.New("something went wrong") 63 | callCount := 0 64 | err := try.Do(func(attempt int) (bool, error) { 65 | callCount++ 66 | return attempt < 5, theErr 67 | }) 68 | is.Equal(err, theErr) 69 | is.Equal(callCount, 5) 70 | } 71 | 72 | func TestTryPanics(t *testing.T) { 73 | is := is.New(t) 74 | theErr := errors.New("something went wrong") 75 | callCount := 0 76 | err := try.Do(func(attempt int) (retry bool, err error) { 77 | retry = attempt < 5 78 | defer func() { 79 | if r := recover(); r != nil { 80 | err = errors.New(fmt.Sprintf("panic: %v", r)) 81 | } 82 | }() 83 | callCount++ 84 | if attempt > 2 { 85 | panic("I don't like three") 86 | } 87 | err = theErr 88 | return 89 | }) 90 | is.Equal(err.Error(), "panic: I don't like three") 91 | is.Equal(callCount, 5) 92 | } 93 | 94 | func TestRetryLimit(t *testing.T) { 95 | is := is.New(t) 96 | err := try.Do(func(attempt int) (bool, error) { 97 | return true, errors.New("nope") 98 | }) 99 | is.OK(err) 100 | is.Equal(try.IsMaxRetries(err), true) 101 | } 102 | --------------------------------------------------------------------------------