├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── go.mod ├── retry.go └── retry_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.6 7 | - go: 1.7 8 | - go: 1.8 9 | - go: 1.9 10 | - go: 1.10.x 11 | - go: 1.11.x 12 | - go: 1.12.x 13 | - go: 1.13.x 14 | allow_failures: 15 | - go: tip 16 | before_install: 17 | - go get github.com/mattn/goveralls 18 | script: 19 | - $GOPATH/bin/goveralls -service=travis-ci 20 | - go get -t -v ./... 21 | - diff -u <(echo -n) <(gofmt -d .) 22 | - go vet $(go list ./... | grep -v /vendor/) 23 | - go test -v -race ./... 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Must follow the guide for issues 4 | - Use the search tool before opening a new issue. 5 | - Please provide source code and stack trace if you found a bug. 6 | - Please review the existing issues and provide feedback to them 7 | 8 | ## Pull Request Process 9 | - Open your pull request against `dev` branch 10 | - It should pass all tests in the available continuous integrations systems such as TravisCI. 11 | - You should add/modify tests to cover your proposed code changes. 12 | - If your pull request contains a new feature, please document it on the README. 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Saddam H 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Retry 2 | ================== 3 | 4 | [![Build Status](https://travis-ci.org/thedevsaddam/retry.svg?branch=master)](https://travis-ci.org/thedevsaddam/retry) 5 | [![Project status](https://img.shields.io/badge/version-1.2-green.svg)](https://github.com/thedevsaddam/retry/releases) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/thedevsaddam/retry)](https://goreportcard.com/report/github.com/thedevsaddam/retry) 7 | [![Coverage Status](https://coveralls.io/repos/github/thedevsaddam/retry/badge.svg?branch=master)](https://coveralls.io/github/thedevsaddam/retry?branch=master) 8 | [![GoDoc](https://godoc.org/github.com/thedevsaddam/retry?status.svg)](https://pkg.go.dev/github.com/thedevsaddam/retry) 9 | [![License](https://img.shields.io/dub/l/vibe-d.svg)](https://github.com/thedevsaddam/retry/blob/dev/LICENSE.md) 10 | 11 | 12 | Simple and easy retry mechanism package for Go 13 | 14 | ### Installation 15 | 16 | Install the package using 17 | ```go 18 | $ go get github.com/thedevsaddam/retry 19 | ``` 20 | 21 | ### Usage 22 | 23 | To use the package import it in your `*.go` code 24 | ```go 25 | import "github.com/thedevsaddam/retry" 26 | ``` 27 | 28 | ### Example 29 | 30 | Simply retry a function to execute for max 10 times with interval of 1 second 31 | 32 | ```go 33 | 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | "time" 39 | 40 | "github.com/thedevsaddam/retry" 41 | ) 42 | 43 | func main() { 44 | i := 1 // lets assume we expect i to be a value of 8 45 | err := retry.DoFunc(10, 1*time.Second, func() error { 46 | fmt.Printf("trying for: %dth time\n", i) 47 | i++ 48 | if i > 7 { 49 | return nil 50 | } 51 | return fmt.Errorf("i = %d is still low value", i) 52 | }) 53 | 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | fmt.Println("Got our expected result: ", i) 59 | } 60 | 61 | ``` 62 | 63 | We can execute function from other package with arguments and return values 64 | 65 | ```go 66 | 67 | package main 68 | 69 | import ( 70 | "errors" 71 | "log" 72 | "time" 73 | 74 | "github.com/thedevsaddam/retry" 75 | ) 76 | 77 | func div(a, b float64) (float64, error) { 78 | if b == 0 { 79 | return 0, errors.New("Can not divide by zero") 80 | } 81 | return a / b, nil 82 | } 83 | 84 | func main() { 85 | a := 20.6 86 | b := 3.7 // if we assign 0.0 to b, it will cause an error and will retry for 3 times 87 | res, err := retry.Do(3, 5*time.Second, div, a, b) 88 | if err != nil { 89 | panic(err) 90 | } 91 | log.Println(res) 92 | } 93 | 94 | ``` 95 | 96 | ### **Contribution** 97 | If you are interested to make the package better please send pull requests or create an issue so that others can fix. Read the [contribution guide here](CONTRIBUTING.md). 98 | 99 | ### **License** 100 | The **retry** is an open-source software licensed under the [MIT License](LICENSE.md). 101 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thedevsaddam/retry 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | // Copyright @2018 Saddam Hossain. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package retry is a simple and easy retry mechanism package for Go 6 | package retry 7 | 8 | import ( 9 | "errors" 10 | "math/rand" 11 | "reflect" 12 | "time" 13 | ) 14 | 15 | func init() { 16 | rand.Seed(time.Now().UnixNano()) 17 | } 18 | 19 | // DoFunc try to execute the function, it only expect that the function will return an error only 20 | func DoFunc(attempt uint, sleep time.Duration, fn func() error) error { 21 | 22 | if err := fn(); err != nil { 23 | if attempt--; attempt > 0 { 24 | // Add jitter to prevent Thundering Herd problem (https://en.wikipedia.org/wiki/Thundering_herd_problem) 25 | sleep += (time.Duration(rand.Int63n(int64(sleep)))) / 2 26 | time.Sleep(sleep) 27 | return DoFunc(attempt, 2*sleep, fn) 28 | } 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // Do try to execute the function by its value, function can take variadic arguments and return multiple return. 36 | // You must put error as the last return value so that DoFunc can take decision that the call failed or not 37 | func Do(attempt uint, sleep time.Duration, fn interface{}, args ...interface{}) ([]interface{}, error) { 38 | 39 | if attempt == 0 { 40 | return nil, errors.New("retry: attempt should be greater than 0") 41 | } 42 | 43 | vfn := reflect.ValueOf(fn) 44 | 45 | // if the fn is not a function then return error 46 | if vfn.Type().Kind() != reflect.Func { 47 | return nil, errors.New("retry: fn is not a function") 48 | } 49 | 50 | // if the functions in not variadic then return the argument missmatch error 51 | if !vfn.Type().IsVariadic() && (vfn.Type().NumIn() != len(args)) { 52 | return nil, errors.New("retry: fn argument mismatch") 53 | } 54 | 55 | // if the function does not return anything, we can't catch if an error occur or not 56 | if vfn.Type().NumOut() <= 0 { 57 | return nil, errors.New("retry: fn return's can not empty, at least an error") 58 | } 59 | 60 | // build args for reflect value Call 61 | in := make([]reflect.Value, len(args)) 62 | for k, a := range args { 63 | in[k] = reflect.ValueOf(a) 64 | } 65 | 66 | var lastErr error 67 | for attempt > 0 { 68 | // call the fn with arguments 69 | out := []interface{}{} 70 | for _, o := range vfn.Call(in) { 71 | out = append(out, o.Interface()) 72 | } 73 | 74 | // if the last value is not error then return an error 75 | err, ok := out[len(out)-1].(error) 76 | if !ok && out[len(out)-1] != nil { 77 | return nil, errors.New("retry: fn return's right most value must be an error") 78 | } 79 | 80 | if err == nil { 81 | return out[:len(out)-1], nil 82 | } 83 | lastErr = err 84 | attempt-- 85 | // Add jitter to prevent Thundering Herd problem (https://en.wikipedia.org/wiki/Thundering_herd_problem) 86 | sleep += (time.Duration(rand.Int63n(int64(sleep)))) / 2 87 | time.Sleep(sleep) 88 | sleep *= 2 89 | } 90 | 91 | return nil, lastErr 92 | } 93 | -------------------------------------------------------------------------------- /retry_test.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDoFunc(t *testing.T) { 11 | var try = 0 12 | _ = DoFunc(5, 1*time.Nanosecond, func() error { 13 | if try < 5 { 14 | try++ 15 | return errors.New("try is not five") 16 | } 17 | return nil 18 | }) 19 | if try != 5 { 20 | t.Error("Retry failed, expected try = 5") 21 | } 22 | 23 | } 24 | 25 | func TestDoFunc_Nil(t *testing.T) { 26 | var try = 5 27 | _ = DoFunc(1, 1*time.Nanosecond, func() error { 28 | try-- 29 | return nil 30 | }) 31 | 32 | if try != 4 { 33 | t.Error("Failed to stop retry, expected try = 4") 34 | } 35 | } 36 | 37 | func TestDo(t *testing.T) { 38 | var notFunc int 39 | 40 | sum := func(nums ...int) (int, error) { 41 | var result int 42 | for _, n := range nums { 43 | result = result + n 44 | } 45 | return result, nil 46 | } 47 | 48 | div := func(a, b float64) (float64, error) { 49 | if b == 0 { 50 | return 0, errors.New("can not divide by zero") 51 | } 52 | return a / b, nil 53 | } 54 | 55 | voidFunc := func() { 56 | 57 | } 58 | 59 | noErrorFunc := func() bool { 60 | fmt.Println("I'll executed only once as I don't return any error interface") 61 | return false 62 | } 63 | 64 | multiRet := func() (int, bool, error) { 65 | return 1, false, nil 66 | } 67 | 68 | testcases := []struct { 69 | Tag string 70 | Func interface{} 71 | Args []interface{} 72 | Result interface{} 73 | Len int 74 | ExpectedError bool 75 | }{ 76 | { 77 | Tag: "Add 1 to 4 and expected result 10", 78 | Func: sum, 79 | Args: []interface{}{1, 2, 3, 4}, 80 | Result: 10, 81 | Len: 1, 82 | }, 83 | { 84 | Tag: "Add 1 to 5 and expected result 15", 85 | Func: sum, 86 | Args: []interface{}{1, 2, 3, 4, 5}, 87 | Result: 15, 88 | Len: 1, 89 | }, 90 | { 91 | Tag: "Div 9.0/3.0 and expected result 3.0", 92 | Func: div, 93 | Args: []interface{}{9.0, 3.0}, 94 | Result: 3.0, 95 | Len: 1, 96 | }, 97 | { 98 | Tag: "Div 9.0/0.0 and expected result 0 with error", 99 | Func: div, 100 | Args: []interface{}{9.0, 0.0}, 101 | Result: 0.0, 102 | ExpectedError: true, 103 | Len: 1, 104 | }, 105 | { 106 | Tag: "As div is not a variadic func, if args mismatch we expect error", 107 | Func: div, 108 | Args: []interface{}{12.0, 3.0, 4.0}, 109 | ExpectedError: true, 110 | }, 111 | { 112 | Tag: "As 'notFunc' is not a function we expect an error from retry package", 113 | Func: notFunc, 114 | ExpectedError: true, 115 | }, 116 | { 117 | Tag: "As 'voidFunc' does not return anything we expect an error from retry package", 118 | Func: voidFunc, 119 | ExpectedError: true, 120 | }, 121 | { 122 | Tag: "As 'noErrorFunc' does not return error we silently try to execute the func only once", 123 | Func: noErrorFunc, 124 | Result: false, 125 | ExpectedError: true, 126 | }, 127 | { 128 | Tag: "As 'multiRet' returns back two data we try to check length of out with error false", 129 | Func: multiRet, 130 | Result: 1, 131 | ExpectedError: false, 132 | Len: 2, 133 | }, 134 | } 135 | 136 | for _, tc := range testcases { 137 | 138 | out, err := Do(2, 1*time.Millisecond, tc.Func, tc.Args...) 139 | if err != nil && !tc.ExpectedError { 140 | t.Error(tc.Tag, err) 141 | } 142 | 143 | if !tc.ExpectedError && out != nil { 144 | if len(out) != tc.Len { 145 | t.Errorf("Failed: %s \nExpected length: %v \nGot: %v", tc.Tag, tc.Len, len(out)) 146 | } 147 | if out[0] != tc.Result { 148 | t.Errorf("failed: %s \nExpected: %v \nGot: %v", tc.Tag, tc.Result, out[0]) 149 | } 150 | } 151 | } 152 | } 153 | 154 | func TestDoAttempt(t *testing.T) { 155 | _, err := Do(0, 1*time.Millisecond, func() {}) 156 | if err == nil { 157 | t.Errorf("failed: expected attempt 0 error") 158 | } 159 | } 160 | --------------------------------------------------------------------------------