├── go.mod ├── .gitignore ├── .travis.yml ├── go.sum ├── CHANGELOG.md ├── errors_test.go ├── LICENSE ├── examples ├── heartbeat_with_error_timeout │ └── main.go ├── heartbeat │ └── main.go ├── backoff │ └── main.go └── backoff_with_timeout │ └── main.go ├── wrappers.go ├── errors.go ├── delay_test.go ├── wrappers_test.go ├── backoff_test.go ├── delay.go ├── operations.go ├── repeat.go ├── operations_test.go ├── README.md ├── backoff.go └── repeat_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ssgreg/repeat 2 | 3 | require github.com/stretchr/testify v1.3.0 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.9.2 5 | 6 | before_install: 7 | - go get -v github.com/golang/lint/golint 8 | 9 | install: 10 | - go install -race -v std 11 | - go get -race -t -v ./... 12 | - go install -race -v ./... 13 | 14 | script: 15 | - go vet ./... 16 | - $HOME/gopath/bin/golint . 17 | - go test -cpu=2 -race -v ./... 18 | - go test -cpu=2 -covermode=atomic -v ./... 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | * initial version, all tests passed 4 | 5 | # 1.0.0 6 | 7 | * public release 8 | * add: new interface Repeater 9 | * add: new concept - OpWrapper 10 | * add: new function to create Repeater with wrapping operations 11 | * add: new WithContext - a repeater that checks for context errors before operation call 12 | 13 | # 1.1 14 | 15 | * add: IsTemporary and IsStop to errors 16 | * add: FnOnError to operations 17 | * add: missing tests to operations 18 | 19 | # 1.2 20 | 21 | * add: Nope to operations 22 | * add: new function to create Repeater with constructor and destructor - Cpp 23 | 24 | # 1.3 25 | 26 | * add: Once and FnRepeat to Repeater and global functions 27 | 28 | # 1.4 29 | 30 | * fix: Once calls global compose 31 | * ref: some changes in Cpp concept. It is transparent for input errors now, it also panics if D fails 32 | * add: Done, FnDone, FnOnlyOnce 33 | * add: 100% test coverage 34 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStopError(t *testing.T) { 11 | e := HintStop(nil) 12 | require.False(t, IsStop(nil)) 13 | require.False(t, IsStop(errors.New("test"))) 14 | require.False(t, IsStop(HintTemporary(nil))) 15 | require.True(t, IsStop(e)) 16 | require.Nil(t, Cause(e)) 17 | 18 | require.EqualError(t, e, "repeat.stop") 19 | require.EqualError(t, HintStop(errors.New("internal")), "repeat.stop: internal") 20 | } 21 | 22 | func TestTemporaryError(t *testing.T) { 23 | e := HintTemporary(nil) 24 | require.False(t, IsTemporary(nil)) 25 | require.False(t, IsTemporary(errors.New("test"))) 26 | require.False(t, IsTemporary(HintStop(nil))) 27 | require.True(t, IsTemporary(e)) 28 | require.Nil(t, Cause(e)) 29 | 30 | require.EqualError(t, e, "repeat.temporary") 31 | require.EqualError(t, HintTemporary(errors.New("internal")), "repeat.temporary: internal") 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Grigory Zubankov 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/heartbeat_with_error_timeout/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/ssgreg/repeat" 9 | ) 10 | 11 | func printInfo(attempt int, last *time.Time) { 12 | tmp := *last 13 | *last = time.Now() 14 | if attempt == 0 { 15 | tmp = *last 16 | } 17 | fmt.Printf("Attempt #%d, Delay %v\n", attempt, last.Sub(tmp)) 18 | } 19 | 20 | // Output: 21 | // 22 | // Attempt #0, Delay 0s 23 | // Attempt #1, Delay 1.001634616s 24 | // Attempt #2, Delay 1.004912408s 25 | // Attempt #3, Delay 1.001021358s 26 | // Attempt #4, Delay 1.001249459s 27 | // Attempt #5, Delay 1.004320833s 28 | // Repetition process is finished with: can't connect to a server 29 | // 30 | 31 | func main() { 32 | 33 | // An example operation that do heartbeat. 34 | // It fails 5 times after 3 successful tries. 35 | var last time.Time 36 | op := func(c int) error { 37 | printInfo(c, &last) 38 | if c > 3 && c < 8 { 39 | return repeat.HintTemporary(errors.New("can't connect to a server")) 40 | } 41 | return nil 42 | } 43 | 44 | err := repeat.Repeat( 45 | // Heartbeating op. 46 | repeat.FnWithCounter(op), 47 | // Delay with fixed backoff and error timeout. 48 | repeat.WithDelay( 49 | repeat.FixedBackoff(time.Second).Set(), 50 | repeat.SetErrorsTimeout(3*time.Second), 51 | ), 52 | ) 53 | 54 | fmt.Printf("Repetition process is finished with: %v\n", err) 55 | } 56 | -------------------------------------------------------------------------------- /wrappers.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // OpWrapper is the type of function for repetition. 8 | type OpWrapper func(Operation) Operation 9 | 10 | // WrStopOnContextError stops an operation in case of context error. 11 | func WrStopOnContextError(ctx context.Context) OpWrapper { 12 | return func(op Operation) Operation { 13 | return func(e error) error { 14 | if ctx.Err() != nil { 15 | switch e.(type) { 16 | case nil: 17 | return HintStop(ctx.Err()) 18 | case *StopError: 19 | return e 20 | case *TemporaryError: 21 | return HintStop(e) 22 | default: 23 | return e 24 | } 25 | } 26 | 27 | return op(e) 28 | } 29 | } 30 | } 31 | 32 | // WrWith returns wrapper that calls C (constructor) at first, then ops, 33 | // then D (destructor). D will be called in any case if C returns nil. 34 | func WrWith(c, d Operation) OpWrapper { 35 | return func(op Operation) Operation { 36 | return func(e error) (err error) { 37 | err = c(e) 38 | if err != nil { 39 | // If C failed with temporary error, stop error or any other 40 | // error: stop compose with this error. 41 | return err 42 | } 43 | defer func() { 44 | // Note: handle error using D wrapper. 45 | err = d(err) 46 | }() 47 | 48 | return op(e) 49 | } 50 | } 51 | } 52 | 53 | // Forward returns the passed operation. 54 | func Forward(op Operation) Operation { 55 | return op 56 | } 57 | -------------------------------------------------------------------------------- /examples/heartbeat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/ssgreg/repeat" 9 | ) 10 | 11 | func printInfo(attempt int, last *time.Time) { 12 | tmp := *last 13 | *last = time.Now() 14 | if attempt == 0 { 15 | tmp = *last 16 | } 17 | fmt.Printf("Attempt #%d, Delay %v\n", attempt, last.Sub(tmp)) 18 | } 19 | 20 | // Output: 21 | // 22 | // Attempt #0, Delay 0s 23 | // Attempt #1, Delay 1.001129426s 24 | // Attempt #2, Delay 1.000155727s 25 | // Attempt #3, Delay 1.001131014s 26 | // Attempt #4, Delay 1.000500428s 27 | // Attempt #5, Delay 1.0008985s 28 | // Attempt #6, Delay 1.000417057s 29 | // Repetition process is finished with: context canceled 30 | // 31 | 32 | func main() { 33 | 34 | // An example operation that do heartbeat. 35 | var last time.Time 36 | op := func(c int) error { 37 | printInfo(c, &last) 38 | return nil 39 | } 40 | 41 | // A context with cancel. 42 | // Repetition will be cancelled in 7 seconds. 43 | ctx, cancelFunc := context.WithCancel(context.Background()) 44 | go func() { 45 | time.Sleep(7 * time.Second) 46 | cancelFunc() 47 | }() 48 | 49 | err := repeat.Repeat( 50 | // Heartbeating op. 51 | repeat.FnWithCounter(op), 52 | // Delay with fixed backoff and context. 53 | repeat.WithDelay( 54 | repeat.FixedBackoff(time.Second).Set(), 55 | repeat.SetContext(ctx), 56 | ), 57 | ) 58 | 59 | fmt.Printf("Repetition process is finished with: %v\n", err) 60 | } 61 | -------------------------------------------------------------------------------- /examples/backoff/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/ssgreg/repeat" 9 | ) 10 | 11 | func printInfo(attempt int, last *time.Time) { 12 | tmp := *last 13 | *last = time.Now() 14 | if attempt == 0 { 15 | tmp = *last 16 | } 17 | fmt.Printf("Attempt #%d, Delay %v\n", attempt, last.Sub(tmp)) 18 | } 19 | 20 | // Example of output: 21 | // 22 | // Attempt #0, Delay 0s 23 | // Attempt #1, Delay 373.617912ms 24 | // Attempt #2, Delay 668.004225ms 25 | // Attempt #3, Delay 1.220076558s 26 | // Attempt #4, Delay 2.716156336s 27 | // Attempt #5, Delay 6.458431017s 28 | // Repetition process is finished with: 29 | // 30 | 31 | func main() { 32 | 33 | // An example operation that do some useful stuff. 34 | // It fails five first times. 35 | var last time.Time 36 | op := func(c int) error { 37 | printInfo(c, &last) 38 | if c < 5 { 39 | return repeat.HintTemporary(errors.New("can't connect to a server")) 40 | } 41 | return nil 42 | } 43 | 44 | // Repeat op on any error, with 10 retries, with a backoff. 45 | err := repeat.Repeat( 46 | // Our op with additional call counter. 47 | repeat.FnWithCounter(op), 48 | // Force the repetition to stop in case the previous operation 49 | // returns nil. 50 | repeat.StopOnSuccess(), 51 | // 10 retries max. 52 | repeat.LimitMaxTries(10), 53 | // Specify a delay that uses a backoff. 54 | repeat.WithDelay( 55 | repeat.FullJitterBackoff(500*time.Millisecond).Set(), 56 | ), 57 | ) 58 | 59 | fmt.Printf("Repetition process is finished with: %v\n", err) 60 | } 61 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | // TemporaryError allows not to stop repetitions process right now. 4 | // 5 | // This error never returns to the caller as is, only wrapped error. 6 | type TemporaryError struct { 7 | Cause error 8 | } 9 | 10 | func (e *TemporaryError) Error() string { 11 | r := "repeat.temporary" 12 | if e.Cause != nil { 13 | r += ": " + e.Cause.Error() 14 | } 15 | 16 | return r 17 | } 18 | 19 | // HintTemporary makes a TemporaryError. 20 | func HintTemporary(e error) error { 21 | return &TemporaryError{Cause(e)} 22 | } 23 | 24 | // IsTemporary checks if passed error is TemporaryError. 25 | func IsTemporary(e error) bool { 26 | switch e.(type) { 27 | case *TemporaryError: 28 | return true 29 | default: 30 | return false 31 | } 32 | } 33 | 34 | // StopError allows to stop repetition process without specifying a 35 | // separate error. 36 | // 37 | // This error never returns to the caller as is, only wrapped error. 38 | type StopError struct { 39 | Cause error 40 | } 41 | 42 | func (e *StopError) Error() string { 43 | r := "repeat.stop" 44 | if e.Cause != nil { 45 | r += ": " + e.Cause.Error() 46 | } 47 | 48 | return r 49 | } 50 | 51 | // HintStop makes a StopError. 52 | func HintStop(e error) error { 53 | return &StopError{Cause(e)} 54 | } 55 | 56 | // IsStop checks if passed error is StopError. 57 | func IsStop(e error) bool { 58 | switch e.(type) { 59 | case *StopError: 60 | return true 61 | default: 62 | return false 63 | } 64 | } 65 | 66 | // Cause extracts the cause error from TemporaryError and StopError 67 | // or return the passed one. 68 | func Cause(err error) error { 69 | switch e := err.(type) { 70 | case *TemporaryError: 71 | return e.Cause 72 | case *StopError: 73 | return e.Cause 74 | default: 75 | return err 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/backoff_with_timeout/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/ssgreg/repeat" 10 | ) 11 | 12 | func printInfo(attempt int, last *time.Time) { 13 | tmp := *last 14 | *last = time.Now() 15 | if attempt == 0 { 16 | tmp = *last 17 | } 18 | fmt.Printf("Attempt #%d, Delay %v\n", attempt, last.Sub(tmp)) 19 | } 20 | 21 | // Example of output: 22 | // 23 | // Attempt #0, Delay 0s 24 | // Attempt #1, Delay 358.728046ms 25 | // Attempt #2, Delay 845.361787ms 26 | // Attempt #3, Delay 61.527485ms 27 | // Repetition process is finished with: context canceled 28 | // 29 | 30 | func backoff(ctx context.Context) repeat.Operation { 31 | return repeat.Compose( 32 | // Force the repetition to stop in case the previous operation 33 | // returns nil. 34 | repeat.StopOnSuccess(), 35 | // 10 retries max. 36 | repeat.LimitMaxTries(10), 37 | // Specify a delay that uses a backoff. 38 | repeat.WithDelay( 39 | repeat.FullJitterBackoff(500*time.Millisecond).Set(), 40 | repeat.SetContext(ctx), 41 | ), 42 | ) 43 | } 44 | 45 | func main() { 46 | 47 | // An example operation that do some useful stuff. 48 | // It fails five first times. 49 | var last time.Time 50 | op := func(c int) error { 51 | printInfo(c, &last) 52 | if c < 5 { 53 | return repeat.HintTemporary(errors.New("can't connect to a server")) 54 | } 55 | return nil 56 | } 57 | 58 | // A context with cancel. 59 | // Repetition will be cancelled in 3 seconds. 60 | ctx, cancelFunc := context.WithCancel(context.Background()) 61 | go func() { 62 | time.Sleep(3 * time.Second) 63 | cancelFunc() 64 | }() 65 | 66 | // Repeat op on any error, with 10 retries, with a backoff. 67 | err := repeat.Repeat(repeat.FnWithCounter(op), backoff(ctx)) 68 | 69 | fmt.Printf("Repetition process is finished with: %v\n", err) 70 | } 71 | -------------------------------------------------------------------------------- /delay_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // var backoff100 = &ConstantBackoff{100 * time.Millisecond} 15 | // var backoff10 = &ConstantBackoff{10 * time.Millisecond} 16 | 17 | func TestDelay(t *testing.T) { 18 | hb := WithDelay(FixedBackoff(10 * time.Millisecond).Set()) 19 | 20 | for i := 0; i < 10; i++ { 21 | InRange(t, GetDelay(t, hb, nil), time.Millisecond*5, time.Millisecond*15) 22 | } 23 | } 24 | 25 | func TestDelayErrorTimeout(t *testing.T) { 26 | hb := WithDelay(FixedBackoff(10*time.Millisecond).Set(), SetErrorsTimeout(27*time.Millisecond)) 27 | 28 | InRange(t, GetDelay(t, hb, nil), time.Millisecond*10, time.Millisecond*15) 29 | InRange(t, GetDelay(t, hb, nil), time.Millisecond*10, time.Millisecond*15) 30 | InRange(t, GetDelay(t, hb, nil), time.Millisecond*10, time.Millisecond*15) 31 | InRange(t, GetDelay(t, hb, errors.New("error")), time.Millisecond*10, time.Millisecond*15) 32 | InRange(t, GetDelay(t, hb, errors.New("error")), time.Millisecond, time.Millisecond*10) 33 | } 34 | 35 | func TestDelayCancel(t *testing.T) { 36 | ctx, cancelFunc := context.WithCancel(context.Background()) 37 | hb := WithDelay(FixedBackoff(100*time.Millisecond).Set(), SetContext(ctx)) 38 | InRange(t, GetDelay(t, hb, nil), time.Millisecond*50, time.Millisecond*150) 39 | 40 | go func() { 41 | time.Sleep(time.Millisecond * 50) 42 | cancelFunc() 43 | }() 44 | InRange(t, GetDelay(t, hb, errors.New("context canceled")), time.Millisecond*50, time.Millisecond*60) 45 | } 46 | 47 | func GetDelay(t *testing.T, o Operation, result error) time.Duration { 48 | start := time.Now() 49 | if result == nil { 50 | assert.NoError(t, o(nil)) 51 | } else { 52 | assert.Error(t, o(result), result.Error()) 53 | } 54 | return time.Now().Sub(start) 55 | } 56 | 57 | func InRange(t *testing.T, delay time.Duration, min time.Duration, max time.Duration) { 58 | fmt.Println(delay, min, max) 59 | require.True(t, delay >= min && delay <= max) 60 | } 61 | -------------------------------------------------------------------------------- /wrappers_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var ( 14 | errGolden = errors.New("golden") 15 | ) 16 | 17 | func TestWrStopOnContextError_CallReturnOp(t *testing.T) { 18 | opNilError := func(e error) error { 19 | require.NoError(t, e) 20 | return e 21 | } 22 | 23 | require.NoError(t, WrStopOnContextError(context.Background())(opNilError)(nil)) 24 | 25 | opError := func(e error) error { 26 | require.EqualError(t, Cause(e), errGolden.Error()) 27 | return e 28 | } 29 | 30 | require.Error(t, WrStopOnContextError(context.Background())(opError)(HintTemporary(errGolden))) 31 | } 32 | 33 | func TestWrStopOnContextError_CancelOp(t *testing.T) { 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | 36 | op := func(e error) error { 37 | require.Fail(t, "should be never called") 38 | return nil 39 | } 40 | 41 | cancel() 42 | require.EqualError(t, WrStopOnContextError(ctx)(op)(nil), "repeat.stop: context canceled") 43 | } 44 | 45 | func TestWrStopOnContextError_CancelOpWithError(t *testing.T) { 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | 48 | op := func(e error) error { 49 | require.Fail(t, "should be never called") 50 | return nil 51 | } 52 | 53 | cancel() 54 | require.EqualError(t, WrStopOnContextError(ctx)(op)(HintTemporary(errGolden)), "repeat.stop: golden") 55 | require.EqualError(t, WrStopOnContextError(ctx)(op)(HintStop(errGolden)), "repeat.stop: golden") 56 | require.EqualError(t, WrStopOnContextError(ctx)(op)(errGolden), "golden") 57 | } 58 | 59 | func TestWrStopOnContextError_SuccessIfDone(t *testing.T) { 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | 62 | called := false 63 | op := func(e error) error { 64 | time.Sleep(time.Millisecond * 10) 65 | called = true 66 | return nil 67 | } 68 | 69 | go func() { 70 | time.Sleep(time.Millisecond * 5) 71 | cancel() 72 | }() 73 | 74 | require.NoError(t, WrStopOnContextError(ctx)(op)(HintTemporary(errGolden))) 75 | require.True(t, called) 76 | } 77 | 78 | func TestForward(t *testing.T) { 79 | op := func(e error) error { return nil } 80 | 81 | require.Nil(t, Forward(nil)) 82 | require.Equal(t, reflect.ValueOf(op).Pointer(), reflect.ValueOf(op).Pointer()) 83 | } 84 | -------------------------------------------------------------------------------- /backoff_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConstantBackoff(t *testing.T) { 12 | fn77 := FixedBackoffAlgorithm(77) 13 | assert.EqualValues(t, fn77(), 77) 14 | assert.EqualValues(t, fn77(), 77) 15 | } 16 | 17 | func TestFullJitterBackoffDefaults(t *testing.T) { 18 | do := &DelayOptions{} 19 | FullJitterBackoff(time.Second).Set()(do) 20 | 21 | for i := 0; i < 20; i++ { 22 | c := int64(math.Pow(2, float64(i))) 23 | InRange(t, do.Backoff(), 0, time.Duration(c)*time.Second) 24 | } 25 | } 26 | 27 | func TestFullJitterBackoff(t *testing.T) { 28 | do := &DelayOptions{} 29 | FullJitterBackoff(1).WithMaxDelay(30).Set()(do) 30 | 31 | for i := 0; i < 50; i++ { 32 | c := int64(math.Pow(2, float64(i))) 33 | if c > 30 { 34 | c = 30 35 | } 36 | InRange(t, do.Backoff(), 0, time.Duration(c)) 37 | } 38 | } 39 | 40 | var floatSecond = float64(time.Second) 41 | 42 | func TestExponentialBackoffDefaults(t *testing.T) { 43 | do := &DelayOptions{} 44 | ExponentialBackoff(time.Second).Set()(do) 45 | 46 | for i := 0; i < 30; i++ { 47 | c := math.Pow(2, float64(i)) 48 | InRange(t, do.Backoff(), time.Duration(c*floatSecond), time.Duration(c*floatSecond)) 49 | } 50 | } 51 | 52 | func TestExponentialBackoffJitter(t *testing.T) { 53 | do := &DelayOptions{} 54 | ExponentialBackoff(time.Second).WithJitter(.5).Set()(do) 55 | 56 | for i := 0; i < 30; i++ { 57 | c := math.Pow(2, float64(i)) 58 | fi := .5 * c 59 | InRange(t, do.Backoff(), time.Duration((c-fi)*floatSecond), time.Duration((c+fi)*floatSecond)) 60 | } 61 | } 62 | 63 | func TestExponentialBackoffJitterAndMultiplier(t *testing.T) { 64 | do := &DelayOptions{} 65 | ExponentialBackoff(time.Second).WithJitter(.1).WithMultiplier(1.74).Set()(do) 66 | 67 | for i := 0; i < 30; i++ { 68 | c := math.Pow(1.74, float64(i)) 69 | fi := .1 * c 70 | InRange(t, do.Backoff(), time.Duration((c-fi)*floatSecond), time.Duration((c+fi)*floatSecond)) 71 | } 72 | } 73 | 74 | func TestExponentialBackoff(t *testing.T) { 75 | do := &DelayOptions{} 76 | ExponentialBackoff(354 * time.Millisecond).WithJitter(.9).WithMultiplier(1.12).WithMaxDelay(5 * time.Second).Set()(do) 77 | 78 | initDelay := float64(354 * time.Millisecond) 79 | for i := 0; i < 300; i++ { 80 | c := math.Pow(1.12, float64(i)) * initDelay 81 | if c > float64(5*time.Second) { 82 | c = float64(5 * time.Second) 83 | } 84 | fi := .9 * c 85 | InRange(t, do.Backoff(), time.Duration(c-fi), time.Duration(c+fi)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /delay.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // SetErrorsTimeout specifies the maximum timeout for repetition 9 | // in case of error. This timeout is reset each time when the 10 | // repetition operation is successfully completed. 11 | // 12 | // Default value is maximum time.Duration value. 13 | func SetErrorsTimeout(t time.Duration) func(*DelayOptions) { 14 | return func(do *DelayOptions) { 15 | do.ErrorsTimeout = t 16 | } 17 | } 18 | 19 | // SetContext allows to set a context instead of default one. 20 | func SetContext(ctx context.Context) func(*DelayOptions) { 21 | return func(do *DelayOptions) { 22 | do.Context = ctx 23 | } 24 | } 25 | 26 | // SetContextHintStop instructs to use HintStop(nil) 27 | // instead of context error in case of context expiration. 28 | func SetContextHintStop() func(*DelayOptions) { 29 | return func(do *DelayOptions) { 30 | do.ContextHintStop = true 31 | } 32 | } 33 | 34 | // WithDelay constructs HeartbeatPredicate. 35 | func WithDelay(options ...func(hb *DelayOptions)) Operation { 36 | do := applyOptions(applyOptions(&DelayOptions{}, defaultOptions()), options) 37 | 38 | shift := func() time.Time { 39 | return time.Now().Add(do.ErrorsTimeout) 40 | } 41 | 42 | deadline := shift() 43 | 44 | return func(e error) error { 45 | // Shift the deadline in case of success. 46 | if e == nil { 47 | deadline = shift() 48 | } 49 | 50 | delayT := time.NewTimer(do.Backoff()) 51 | defer delayT.Stop() 52 | deadlineT := time.NewTimer(deadline.Sub(time.Now())) 53 | defer deadlineT.Stop() 54 | 55 | select { 56 | case <-do.Context.Done(): 57 | // Let out caller know that the op is cancelled. 58 | if do.ContextHintStop { 59 | return HintStop(nil) 60 | } 61 | 62 | return do.Context.Err() 63 | 64 | case <-deadlineT.C: 65 | // The reason of a deadline is the previous error. Let our 66 | // caller to take care of it. 67 | return Cause(e) 68 | 69 | case <-delayT.C: 70 | return e 71 | } 72 | } 73 | } 74 | 75 | // DelayOptions holds parameters for a heartbeat process. 76 | type DelayOptions struct { 77 | ErrorsTimeout time.Duration 78 | Backoff func() time.Duration 79 | Context context.Context 80 | ContextHintStop bool 81 | } 82 | 83 | func defaultOptions() []func(hb *DelayOptions) { 84 | return []func(do *DelayOptions){ 85 | SetContext(context.Background()), 86 | SetErrorsTimeout(1<<63 - 1), 87 | FixedBackoff(time.Second).Set(), 88 | } 89 | } 90 | 91 | func applyOptions(do *DelayOptions, options []func(*DelayOptions)) *DelayOptions { 92 | for _, o := range options { 93 | o(do) 94 | } 95 | return do 96 | } 97 | -------------------------------------------------------------------------------- /operations.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | // Operation is the type of function for repetition. 4 | type Operation func(error) error 5 | 6 | // LimitMaxTries returns true if attempt number is less then max. 7 | func LimitMaxTries(max int) Operation { 8 | return FnWithErrorAndCounter(func(e error, c int) error { 9 | if c < max { 10 | return e 11 | } 12 | 13 | return HintStop(e) 14 | }) 15 | } 16 | 17 | // StopOnSuccess returns true in case of error is nil. 18 | func StopOnSuccess() Operation { 19 | return func(e error) error { 20 | if e != nil { 21 | return e 22 | } 23 | 24 | return HintStop(e) 25 | } 26 | } 27 | 28 | // FnOnSuccess executes operation in case of error is nil. 29 | func FnOnSuccess(op Operation) Operation { 30 | return func(e error) error { 31 | if e != nil { 32 | return e 33 | } 34 | 35 | return op(e) 36 | } 37 | } 38 | 39 | // FnOnError executes operation in case error is NOT nil. 40 | func FnOnError(op Operation) Operation { 41 | return func(e error) error { 42 | if e == nil { 43 | return e 44 | } 45 | 46 | return op(e) 47 | } 48 | } 49 | 50 | // FnHintTemporary hints all operation errors as temporary. 51 | func FnHintTemporary(op Operation) Operation { 52 | return func(e error) error { 53 | err := op(e) 54 | switch err.(type) { 55 | case nil: 56 | case *TemporaryError: 57 | case *StopError: 58 | default: 59 | err = HintTemporary(err) 60 | } 61 | 62 | return err 63 | } 64 | } 65 | 66 | // FnHintStop hints all operation errors as StopError. 67 | func FnHintStop(op Operation) Operation { 68 | return func(e error) error { 69 | err := op(e) 70 | switch err.(type) { 71 | case *TemporaryError: 72 | case *StopError: 73 | default: 74 | err = HintStop(err) 75 | } 76 | 77 | return err 78 | } 79 | } 80 | 81 | // FnPanic panics if op returns any error other than nil, TemporaryError 82 | // and StopError. 83 | func FnPanic(op Operation) Operation { 84 | return func(e error) error { 85 | err := op(e) 86 | switch err.(type) { 87 | case nil: 88 | case *TemporaryError: 89 | case *StopError: 90 | default: 91 | panic(err) 92 | } 93 | 94 | return err 95 | } 96 | } 97 | 98 | // FnWithErrorAndCounter wraps operation and adds call counter. 99 | func FnWithErrorAndCounter(op func(error, int) error) Operation { 100 | c := 0 101 | return func(e error) error { 102 | defer func() { c++ }() 103 | return op(e, c) 104 | } 105 | } 106 | 107 | // FnWithCounter wraps operation with counter only. 108 | func FnWithCounter(op func(int) error) Operation { 109 | return FnWithErrorAndCounter(func(_ error, c int) error { 110 | return op(c) 111 | }) 112 | } 113 | 114 | // Fn wraps operation with no arguments. 115 | func Fn(op func() error) Operation { 116 | return func(_ error) error { 117 | return op() 118 | } 119 | } 120 | 121 | // FnS wraps operation with no arguments and return value. 122 | func FnS(op func()) Operation { 123 | return func(e error) error { 124 | op() 125 | return e 126 | } 127 | } 128 | 129 | // FnES wraps operation with no return value. 130 | func FnES(op func(error)) Operation { 131 | return func(e error) error { 132 | op(e) 133 | return e 134 | } 135 | } 136 | 137 | // Nope does nothing, returns input error. 138 | func Nope(e error) error { 139 | return e 140 | } 141 | 142 | // FnNope does not call pass op, returns input error. 143 | func FnNope(op Operation) Operation { 144 | return func(e error) error { 145 | return Nope(e) 146 | } 147 | } 148 | 149 | // Done does nothing, returns nil. 150 | func Done(e error) error { 151 | return nil 152 | } 153 | 154 | // FnDone returns nil even if wrapped op returns an error. 155 | func FnDone(op Operation) Operation { 156 | return func(e error) error { 157 | return Done(op(e)) 158 | } 159 | } 160 | 161 | // FnOnlyOnce executes op only once permanently. 162 | func FnOnlyOnce(op Operation) Operation { 163 | once := false 164 | return func(e error) error { 165 | if once { 166 | return e 167 | } 168 | 169 | once = true 170 | return op(e) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /repeat.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var ( 8 | def = NewRepeater() 9 | ) 10 | 11 | // Once composes the operations and executes the result once. 12 | // 13 | // It is guaranteed that the first op will be called at least once. 14 | func Once(ops ...Operation) error { 15 | return def.Once(ops...) 16 | } 17 | 18 | // Repeat repeat operations until one of them stops the repetition. 19 | // 20 | // It is guaranteed that the first op will be called at least once. 21 | func Repeat(ops ...Operation) error { 22 | return def.Repeat(ops...) 23 | } 24 | 25 | // FnRepeat is a Repeat operation. 26 | func FnRepeat(ops ...Operation) Operation { 27 | return def.FnRepeat(ops...) 28 | } 29 | 30 | // Compose composes all passed operations into a single one. 31 | func Compose(ops ...Operation) Operation { 32 | return def.Compose(ops...) 33 | } 34 | 35 | // WithContext repeat operations until one of them stops the 36 | // repetition or context will be canceled. 37 | // 38 | // It is guaranteed that the first op will be called at least once. 39 | func WithContext(ctx context.Context) Repeater { 40 | return Wrap(WrStopOnContextError(ctx)) 41 | } 42 | 43 | // Repeater represents general package concept. 44 | type Repeater interface { 45 | Once(...Operation) error 46 | Repeat(...Operation) error 47 | Compose(...Operation) Operation 48 | FnRepeat(...Operation) Operation 49 | } 50 | 51 | type stdRepeater struct { 52 | opw OpWrapper 53 | copw OpWrapper 54 | } 55 | 56 | // NewRepeater sets up everything to be able to repeat operations. 57 | func NewRepeater() Repeater { 58 | return NewRepeaterExt(Forward, Forward) 59 | } 60 | 61 | // Wrap returns object that wraps all repeating ops with passed OpWrapper. 62 | func Wrap(opw OpWrapper) Repeater { 63 | return NewRepeaterExt(opw, Forward) 64 | } 65 | 66 | // WrapOnce returns object that wraps all repeating ops combined into a single 67 | // op with passed OpWrapper calling it once. 68 | func WrapOnce(copw OpWrapper) Repeater { 69 | return NewRepeaterExt(Forward, copw) 70 | } 71 | 72 | // NewRepeaterExt returns object that wraps all ops with with the given opw 73 | // and wraps composed operation with the given copw. 74 | func NewRepeaterExt(opw, copw OpWrapper) Repeater { 75 | return &stdRepeater{opw, copw} 76 | } 77 | 78 | // Cpp returns object that calls C (constructor) at first, then ops, 79 | // then D (destructor). D will be called in any case if C returns nil. 80 | // 81 | // Note! Cpp panics if D returns non nil error. Wrap it using Done if 82 | // you log D's error or handle it somehow else. 83 | // 84 | func Cpp(c, d Operation) Repeater { 85 | return NewRepeaterExt(Forward, WrWith(c, func(e error) error { 86 | _ = FnPanic(d)(e) 87 | 88 | return e 89 | })) 90 | } 91 | 92 | // With returns object that calls C (constructor) at first, then ops, 93 | // then D (destructor). D will be called in any case if C returns nil. 94 | // 95 | // Note! D is able to hide original error an return nil or return error 96 | // event if the original error is nil. 97 | func With(c, d Operation) Repeater { 98 | return NewRepeaterExt(Forward, WrWith(c, d)) 99 | } 100 | 101 | // Once composes the operations and executes the result once. 102 | // 103 | // It is guaranteed that the first op will be called at least once. 104 | func (w *stdRepeater) Once(ops ...Operation) error { 105 | return Cause(w.Compose(ops...)(nil)) 106 | } 107 | 108 | // Repeat repeat operations until one of them stops the repetition. 109 | // 110 | // It is guaranteed that the first op will be called at least once. 111 | func (w *stdRepeater) Repeat(ops ...Operation) error { 112 | return Cause(w.FnRepeat(ops...)(nil)) 113 | } 114 | 115 | // FnRepeat is a Repeat operation. 116 | func (w *stdRepeater) FnRepeat(ops ...Operation) Operation { 117 | return func(e error) (err error) { 118 | op := w.Compose(ops...) 119 | 120 | for { 121 | err = op(e) 122 | switch typedError := err.(type) { 123 | case nil: 124 | e = nil 125 | case *TemporaryError: 126 | e = err 127 | case *StopError: 128 | switch typedError.Cause { 129 | case nil: 130 | return nil 131 | default: 132 | return err 133 | } 134 | default: 135 | return err 136 | } 137 | } 138 | } 139 | } 140 | 141 | // Compose wraps ops with wop and composes all passed operations info 142 | // a single one. 143 | func (w *stdRepeater) Compose(ops ...Operation) Operation { 144 | return w.copw(func(e error) (err error) { 145 | for _, op := range ops { 146 | err = w.opw(op)(e) 147 | switch err.(type) { 148 | // Replace last E with nil. 149 | case nil: 150 | e = nil 151 | // Replace last E with new temporary error. 152 | case *TemporaryError: 153 | e = err 154 | // Stop. 155 | case *StopError: 156 | return err 157 | // Stop. 158 | default: 159 | return err 160 | } 161 | } 162 | 163 | return e 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /operations_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNope(t *testing.T) { 13 | // Nope should be transparent for all kinds of errors. 14 | require.NoError(t, Nope(nil)) 15 | require.EqualError(t, Nope(errors.New("kiwi")), "kiwi") 16 | require.EqualError(t, Nope(HintTemporary(errors.New("kiwi"))), "repeat.temporary: kiwi") 17 | } 18 | 19 | func TestFnNope(t *testing.T) { 20 | // Nope should be transparent for all kinds of errors. 21 | require.NoError(t, FnNope(Nope)(nil)) 22 | require.EqualError(t, FnNope(Nope)(errors.New("kiwi")), "kiwi") 23 | require.EqualError(t, FnNope(Nope)(HintTemporary(errors.New("kiwi"))), "repeat.temporary: kiwi") 24 | } 25 | 26 | func TestDone(t *testing.T) { 27 | // Nope should be transparent for all kinds of errors. 28 | require.NoError(t, Done(nil)) 29 | require.NoError(t, Done(errors.New("kiwi"))) 30 | require.NoError(t, Done(HintTemporary(errors.New("kiwi")))) 31 | } 32 | 33 | func TestFnDone(t *testing.T) { 34 | // Nope should be transparent for all kinds of errors. 35 | require.NoError(t, FnDone(Nope)(nil)) 36 | require.NoError(t, FnDone(Nope)(errors.New("kiwi"))) 37 | require.NoError(t, FnDone(Nope)(HintTemporary(errors.New("kiwi")))) 38 | } 39 | 40 | func TestFnES(t *testing.T) { 41 | opNil := func(e error) { 42 | } 43 | opErr := func(e error) { 44 | require.EqualError(t, e, "kiwi") 45 | } 46 | require.NoError(t, FnES(opNil)(nil)) 47 | require.EqualError(t, FnES(opErr)(errors.New("kiwi")), "kiwi") 48 | } 49 | 50 | func TestFnS(t *testing.T) { 51 | op := func() { 52 | } 53 | require.NoError(t, FnS(op)(nil)) 54 | require.EqualError(t, FnS(op)(errors.New("kiwi")), "kiwi") 55 | } 56 | 57 | func TestFn(t *testing.T) { 58 | opNil := func() error { 59 | return nil 60 | } 61 | opErr := func() error { 62 | return errors.New("kiwi") 63 | } 64 | require.NoError(t, Fn(opNil)(nil)) 65 | require.EqualError(t, Fn(opErr)(errors.New("apple")), "kiwi") 66 | } 67 | 68 | func TestLimitMaxTries(t *testing.T) { 69 | fn := LimitMaxTries(5) 70 | fn(nil) 71 | fn(nil) 72 | fn(nil) 73 | fn(nil) 74 | assert.True(t, fn(nil) == nil) 75 | assert.False(t, fn(nil) == nil) 76 | } 77 | 78 | func TestStopOnSuccess(t *testing.T) { 79 | fn := StopOnSuccess() 80 | assert.True(t, fn(fn(nil)) != nil) 81 | assert.False(t, fn(fn(errors.New("error"))) == nil) 82 | } 83 | 84 | func TestFnOnSuccess_CalledOnNil(t *testing.T) { 85 | require.EqualError(t, FnOnSuccess(func(e error) error { 86 | require.NoError(t, e) 87 | return errors.New("called") 88 | })(nil), "called") 89 | } 90 | 91 | func TestFnOnSuccess_NotCalledOnError(t *testing.T) { 92 | require.EqualError(t, FnOnSuccess(func(e error) error { 93 | require.Fail(t, "must not be called") 94 | return e 95 | })(errors.New("not called")), "not called") 96 | } 97 | 98 | func TestFnOnError_NotCalledOnNil(t *testing.T) { 99 | require.NoError(t, FnOnError(func(e error) error { 100 | require.Fail(t, "must not be called") 101 | return e 102 | })(nil)) 103 | } 104 | 105 | func TestFnOnError_CalledOnError(t *testing.T) { 106 | require.EqualError(t, FnOnError(func(e error) error { 107 | require.EqualError(t, e, "calling error") 108 | return errors.New("called") 109 | })(errors.New("calling error")), "called") 110 | } 111 | 112 | func TestFnWithErrorAndCounter(t *testing.T) { 113 | cc := 0 114 | op := func(e error, c int) error { 115 | require.Equal(t, cc, c) 116 | return e 117 | } 118 | fn := FnWithErrorAndCounter(op) 119 | 120 | // tick 121 | require.NoError(t, fn(nil)) 122 | // tick 123 | cc++ 124 | require.NoError(t, fn(nil)) 125 | // tick 126 | cc++ 127 | require.EqualError(t, fn(errors.New("passed")), "passed") 128 | } 129 | 130 | func TestFnWithCounter(t *testing.T) { 131 | cc := 0 132 | op := func(c int) error { 133 | require.Equal(t, cc, c) 134 | return nil 135 | } 136 | fn := FnWithCounter(op) 137 | 138 | // tick 139 | require.NoError(t, fn(nil)) 140 | // tick 141 | cc++ 142 | require.NoError(t, fn(nil)) 143 | } 144 | 145 | func TestFnHintTemporary(t *testing.T) { 146 | fn := FnHintTemporary(Nope) 147 | 148 | // No action on nil 149 | require.NoError(t, fn(nil)) 150 | // No action on temporary error. 151 | te := fn(HintTemporary(nil)) 152 | require.EqualError(t, te, "repeat.temporary") 153 | // No action on stop error. 154 | se := fn(HintStop(errors.New("stop"))) 155 | require.EqualError(t, se, "repeat.stop: stop") 156 | 157 | // Hint common error as temporary. 158 | ce := fn(errors.New("common")) 159 | require.EqualError(t, ce, "repeat.temporary: common") 160 | } 161 | 162 | func TestFnHintStop(t *testing.T) { 163 | fn := FnHintStop(Nope) 164 | 165 | // Hint nil as StopError. 166 | require.EqualError(t, fn(nil), "repeat.stop") 167 | // No action on temporary error. 168 | te := fn(HintTemporary(nil)) 169 | require.EqualError(t, te, "repeat.temporary") 170 | // No action on stop error. 171 | se := fn(HintStop(errors.New("stop"))) 172 | require.EqualError(t, se, "repeat.stop: stop") 173 | // Hint common error as StopError. 174 | ce := fn(errors.New("common")) 175 | require.EqualError(t, ce, "repeat.stop: common") 176 | } 177 | 178 | func TestFnPanic(t *testing.T) { 179 | fn := FnPanic(Nope) 180 | 181 | // No action on nil 182 | require.NoError(t, fn(nil)) 183 | // No action on temporary error. 184 | te := fn(HintTemporary(nil)) 185 | require.EqualError(t, te, "repeat.temporary") 186 | // No action on stop error. 187 | se := fn(HintStop(errors.New("stop"))) 188 | require.EqualError(t, se, "repeat.stop: stop") 189 | 190 | // Panic 191 | require.Panics(t, func() { fn(errors.New("common")) }) 192 | } 193 | 194 | func TestFnOnlyOnce(t *testing.T) { 195 | c := 0 196 | op := func(e error) error { 197 | c++ 198 | return e 199 | } 200 | 201 | fn := FnOnlyOnce(op) 202 | fn(nil) 203 | fn(nil) 204 | fn(nil) 205 | 206 | require.Equal(t, 1, c) 207 | } 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repeat 2 | 3 | ![Alt text](https://user-images.githubusercontent.com/1574981/33310621-48c78416-d433-11e7-9a80-36d2381901d0.png "repeat") 4 | [![GoDoc](https://godoc.org/github.com/ssgreg/repeat?status.svg)](https://godoc.org/github.com/ssgreg/repeat) 5 | [![Build Status](https://travis-ci.org/ssgreg/repeat.svg?branch=master)](https://travis-ci.org/ssgreg/repeat) 6 | [![Go Report Status](https://goreportcard.com/badge/github.com/ssgreg/repeat)](https://goreportcard.com/report/github.com/ssgreg/repeat) 7 | [![GoCover](https://gocover.io/_badge/github.com/ssgreg/repeat)](https://gocover.io/github.com/ssgreg/repeat) 8 | 9 | Go implementation of different backoff strategies useful for retrying operations and heartbeating. 10 | 11 | ## Examples 12 | 13 | ### Backoff 14 | 15 | Let's imagine that we need to do a REST call on remote server but it could fail with a bunch of different issues. We can repeat failed operation using exponential backoff policies. 16 | 17 | > *Exponential backoff* is an algorithm that uses feedback to multiplicatively decrease the rate of some process, in order to gradually find an acceptable rate. 18 | 19 | The example below tries to repeat operation 10 times using a full jitter backoff. [See algorithm details here.](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) 20 | 21 | ```go 22 | // An example operation that do some useful stuff. 23 | // It fails five first times. 24 | var last time.Time 25 | op := func(c int) error { 26 | printInfo(c, &last) 27 | if c < 5 { 28 | return repeat.HintTemporary(errors.New("can't connect to a server")) 29 | } 30 | return nil 31 | } 32 | 33 | // Repeat op on any error, with 10 retries, with a backoff. 34 | err := repeat.Repeat( 35 | // Our op with additional call counter. 36 | repeat.FnWithCounter(op), 37 | // Force the repetition to stop in case the previous operation 38 | // returns nil. 39 | repeat.StopOnSuccess(), 40 | // 10 retries max. 41 | repeat.LimitMaxTries(10), 42 | // Specify a delay that uses a backoff. 43 | repeat.WithDelay( 44 | repeat.FullJitterBackoff(500*time.Millisecond).Set(), 45 | ), 46 | ) 47 | ``` 48 | 49 | The example of output: 50 | 51 | ``` 52 | Attempt #0, Delay 0s 53 | Attempt #1, Delay 373.617912ms 54 | Attempt #2, Delay 668.004225ms 55 | Attempt #3, Delay 1.220076558s 56 | Attempt #4, Delay 2.716156336s 57 | Attempt #5, Delay 6.458431017s 58 | Repetition process is finished with: 59 | ``` 60 | 61 | ### Backoff with timeout 62 | 63 | The example below is almost the same as the previous one. It adds one important feature - possibility to cancel operation repetition using context's timeout. 64 | 65 | ```go 66 | // A context with cancel. 67 | // Repetition will be cancelled in 3 seconds. 68 | ctx, cancelFunc := context.WithCancel(context.Background()) 69 | go func() { 70 | time.Sleep(3 * time.Second) 71 | cancelFunc() 72 | }() 73 | 74 | // Repeat op on any error, with 10 retries, with a backoff. 75 | err := repeat.Repeat( 76 | ... 77 | // Specify a delay that uses a backoff. 78 | repeat.WithDelay( 79 | repeat.FullJitterBackoff(500*time.Millisecond).Set(), 80 | repeat.SetContext(ctx), 81 | ), 82 | ... 83 | ) 84 | ``` 85 | 86 | The example of output: 87 | 88 | ``` 89 | Attempt #0, Delay 0s 90 | Attempt #1, Delay 358.728046ms 91 | Attempt #2, Delay 845.361787ms 92 | Attempt #3, Delay 61.527485ms 93 | Repetition process is finished with: context canceled 94 | ``` 95 | 96 | ### Heartbeating 97 | 98 | Let's imagine we need to periodically report execution progress to remote server. The example below repeats the operation each second until it will be cancelled using passed context. 99 | 100 | ```go 101 | // An example operation that do heartbeat. 102 | var last time.Time 103 | op := func(c int) error { 104 | printInfo(c, &last) 105 | return nil 106 | } 107 | 108 | // A context with cancel. 109 | // Repetition will be cancelled in 7 seconds. 110 | ctx, cancelFunc := context.WithCancel(context.Background()) 111 | go func() { 112 | time.Sleep(7 * time.Second) 113 | cancelFunc() 114 | }() 115 | 116 | err := repeat.Repeat( 117 | // Heartbeating op. 118 | repeat.FnWithCounter(op), 119 | // Delay with fixed backoff and context. 120 | repeat.WithDelay( 121 | repeat.FixedBackoff(time.Second).Set(), 122 | repeat.SetContext(ctx), 123 | ), 124 | ) 125 | ``` 126 | 127 | The example of output: 128 | 129 | ``` 130 | Attempt #0, Delay 0s 131 | Attempt #1, Delay 1.001129426s 132 | Attempt #2, Delay 1.000155727s 133 | Attempt #3, Delay 1.001131014s 134 | Attempt #4, Delay 1.000500428s 135 | Attempt #5, Delay 1.0008985s 136 | Attempt #6, Delay 1.000417057s 137 | Repetition process is finished with: context canceled 138 | ``` 139 | 140 | ### Heartbeating with error timeout 141 | 142 | The example below is almost the same as the previous one but it will be cancelled using special error timeout. This timeout resets each time the operations return nil. 143 | 144 | ```go 145 | // An example operation that do heartbeat. 146 | // It fails 5 times after 3 successfull tries. 147 | var last time.Time 148 | op := func(c int) error { 149 | printInfo(c, &last) 150 | if c > 3 && c < 8 { 151 | return repeat.HintTemporary(errors.New("can't connect to a server")) 152 | } 153 | return nil 154 | } 155 | 156 | err := repeat.Repeat( 157 | // Heartbeating op. 158 | repeat.FnWithCounter(op), 159 | // Delay with fixed backoff and error timeout. 160 | repeat.WithDelay( 161 | repeat.FixedBackoff(time.Second).Set(), 162 | repeat.SetErrorsTimeout(3*time.Second), 163 | ), 164 | ) 165 | ``` 166 | 167 | The example of output: 168 | 169 | ``` 170 | Attempt #0, Delay 0s 171 | Attempt #1, Delay 1.001634616s 172 | Attempt #2, Delay 1.004912408s 173 | Attempt #3, Delay 1.001021358s 174 | Attempt #4, Delay 1.001249459s 175 | Attempt #5, Delay 1.004320833s 176 | Repetition process is finished with: can't connect to a server 177 | ``` 178 | -------------------------------------------------------------------------------- /backoff.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // FixedBackoffAlgorithm implements backoff with a fixed delay. 10 | func FixedBackoffAlgorithm(delay time.Duration) func() time.Duration { 11 | return func() time.Duration { 12 | return delay 13 | } 14 | } 15 | 16 | // FixedBackoffBuilder is an option builder. 17 | type FixedBackoffBuilder struct { 18 | // Delay specifyes fixed delay value. 19 | Delay time.Duration 20 | } 21 | 22 | // Set creates a Delay' option. 23 | func (s *FixedBackoffBuilder) Set() func(*DelayOptions) { 24 | return func(do *DelayOptions) { 25 | do.Backoff = FixedBackoffAlgorithm(s.Delay) 26 | } 27 | } 28 | 29 | // FixedBackoff create a builder for Delay's option. 30 | func FixedBackoff(delay time.Duration) *FixedBackoffBuilder { 31 | return &FixedBackoffBuilder{Delay: delay} 32 | } 33 | 34 | // FullJitterBackoffAlgorithm implements caped exponential backoff 35 | // with jitter. Algorithm is fast because it does not use floating 36 | // point arithmetics. 37 | // 38 | // Details: 39 | // https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ 40 | // 41 | // Example (BaseDelay=1, maxDelay=30): 42 | // Call Delay 43 | // ------- ---------------- 44 | // 1 random [0...1] 45 | // 2 random [0...2] 46 | // 3 random [0...4] 47 | // 4 random [0...8] 48 | // 5 random [0...16] 49 | // 6 random [0...30] 50 | // 7 random [0...30] 51 | // 52 | func FullJitterBackoffAlgorithm(baseDelay time.Duration, maxDelay time.Duration) func() time.Duration { 53 | rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 54 | delay := baseDelay 55 | 56 | return func() time.Duration { 57 | defer func() { 58 | delay = delay << 1 59 | if delay > maxDelay { 60 | delay = maxDelay 61 | } 62 | }() 63 | return time.Duration(rnd.Int63n(int64(delay))) 64 | } 65 | } 66 | 67 | // FullJitterBackoffBuilder is an option builder. 68 | type FullJitterBackoffBuilder struct { 69 | // MaxDelay specifies maximum value of a delay calculated by the 70 | // algorithm. 71 | // 72 | // Default value is maximum time.Duration value. 73 | MaxDelay time.Duration 74 | 75 | // BaseDelay specifies base of an exponent. 76 | BaseDelay time.Duration 77 | } 78 | 79 | // WithMaxDelay allows to set MaxDelay. 80 | // 81 | // MaxDelay specifies the maximum value of a delay calculated by the 82 | // algorithm. 83 | // 84 | // Default value is maximum time.Duration value. 85 | func (s *FullJitterBackoffBuilder) WithMaxDelay(d time.Duration) *FullJitterBackoffBuilder { 86 | s.MaxDelay = d 87 | return s 88 | } 89 | 90 | // WithBaseDelay allows to set BaseDelay. 91 | // 92 | // BaseDelay specifies base of an exponent. 93 | func (s *FullJitterBackoffBuilder) WithBaseDelay(d time.Duration) *FullJitterBackoffBuilder { 94 | s.BaseDelay = d 95 | return s 96 | } 97 | 98 | // Set creates a Delay' option. 99 | func (s *FullJitterBackoffBuilder) Set() func(*DelayOptions) { 100 | return func(do *DelayOptions) { 101 | do.Backoff = FullJitterBackoffAlgorithm(s.BaseDelay, s.MaxDelay) 102 | } 103 | } 104 | 105 | // FullJitterBackoff create a builder for Delay's option. 106 | func FullJitterBackoff(baseDelay time.Duration) *FullJitterBackoffBuilder { 107 | return (&FullJitterBackoffBuilder{}). 108 | WithBaseDelay(baseDelay). 109 | WithMaxDelay(1<<63 - 1) 110 | } 111 | 112 | // ExponentialBackoffAlgorithm implements classic caped exponential backoff. 113 | // 114 | // Example (initialDelay=1, maxDelay=30, Multiplier=2, Jitter=0.5): 115 | // Attempt Delay 116 | // ------- -------------------------- 117 | // 0 1 + random [-0.5...0.5] 118 | // 1 2 + random [-1...1] 119 | // 2 4 + random [-2...2] 120 | // 3 8 + random [-4...4] 121 | // 4 16 + random [-8...8] 122 | // 5 32 + random [-16...16] 123 | // 6 64 + random [-32...32] = 30 124 | // 125 | func ExponentialBackoffAlgorithm(initialDelay time.Duration, maxDelay time.Duration, multiplier float64, jitter float64) func() time.Duration { 126 | rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 127 | nextDelay := float64(initialDelay) 128 | limit := float64(maxDelay) 129 | 130 | return func() time.Duration { 131 | delay := nextDelay 132 | if nextDelay < limit { 133 | nextDelay = nextDelay * multiplier 134 | } 135 | if nextDelay > limit { 136 | nextDelay = limit 137 | } 138 | 139 | // Fix delay according to jitter. 140 | delay += delay * jitter * (2*rnd.Float64() - 1) 141 | 142 | return time.Duration(delay) 143 | } 144 | } 145 | 146 | // ExponentialBackoffBuilder is an option builder. 147 | type ExponentialBackoffBuilder struct { 148 | // MaxDelay specifies maximum value of a delay calculated by the 149 | // algorithm. 150 | // 151 | // Default value is maximum time.Duration value. 152 | MaxDelay time.Duration 153 | 154 | // InitialDelay specifies an initial delay for the algorithm. 155 | // 156 | // Default value is equal to 1 second. 157 | InitialDelay time.Duration 158 | 159 | // Miltiplier specifies a multiplier for the last calculated 160 | // or specified delay. 161 | // 162 | // Default value is 2. 163 | Multiplier float64 164 | 165 | // Jitter specifies randomization factor [0..1]. 166 | // 167 | // Default value is 0. 168 | Jitter float64 169 | 170 | nextDelay float64 171 | maxDelay float64 172 | rnd *rand.Rand 173 | } 174 | 175 | // WithMaxDelay allows to set MaxDelay. 176 | // 177 | // MaxDelay specifies the maximum value of a delay calculated by the 178 | // algorithm. 179 | // 180 | // Default value is maximum time.Duration value. 181 | func (s *ExponentialBackoffBuilder) WithMaxDelay(d time.Duration) *ExponentialBackoffBuilder { 182 | s.MaxDelay = d 183 | return s 184 | } 185 | 186 | // WithInitialDelay allows to set InitialDelay. 187 | // 188 | // InitialDelay specifies an initial delay for the algorithm. 189 | func (s *ExponentialBackoffBuilder) WithInitialDelay(d time.Duration) *ExponentialBackoffBuilder { 190 | s.InitialDelay = d 191 | return s 192 | } 193 | 194 | // WithMultiplier allows to set Multiplier. 195 | // 196 | // Miltiplier specifies a multiplier for the last calculated 197 | // 198 | // Default value is 2. 199 | func (s *ExponentialBackoffBuilder) WithMultiplier(m float64) *ExponentialBackoffBuilder { 200 | s.Multiplier = m 201 | return s 202 | } 203 | 204 | // WithJitter allows to set Jitter. 205 | // 206 | // Jitter specifies randomization factor [0..1]. 207 | // 208 | // Default value is 0. 209 | func (s *ExponentialBackoffBuilder) WithJitter(j float64) *ExponentialBackoffBuilder { 210 | if j < 0 || j > 1 { 211 | panic(fmt.Sprintf(`repeat: jitter "%f" should in range [0..1]`, j)) 212 | } 213 | 214 | s.Jitter = j 215 | return s 216 | } 217 | 218 | // Set creates a Delay' option. 219 | func (s *ExponentialBackoffBuilder) Set() func(*DelayOptions) { 220 | return func(do *DelayOptions) { 221 | do.Backoff = ExponentialBackoffAlgorithm(s.InitialDelay, s.MaxDelay, s.Multiplier, s.Jitter) 222 | } 223 | } 224 | 225 | // ExponentialBackoff create a builder for Delay's option. 226 | func ExponentialBackoff(initialDelay time.Duration) *ExponentialBackoffBuilder { 227 | return (&ExponentialBackoffBuilder{}). 228 | WithInitialDelay(initialDelay). 229 | WithMaxDelay(1<<63 - 1). 230 | WithMultiplier(2). 231 | WithJitter(0) 232 | } 233 | -------------------------------------------------------------------------------- /repeat_test.go: -------------------------------------------------------------------------------- 1 | package repeat 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestCompose_NilInNilOut(t *testing.T) { 12 | require.NoError(t, Compose(func(e error) error { 13 | require.NoError(t, e) 14 | return e 15 | })(nil)) 16 | } 17 | 18 | func TestCompose_ErrInErrOut(t *testing.T) { 19 | require.EqualError(t, Compose(func(e error) error { 20 | require.EqualError(t, e, "oil") 21 | return e 22 | })(errors.New("oil")), "oil") 23 | } 24 | 25 | func TestCompose_TemporaryErrInTemporaryErrOut(t *testing.T) { 26 | require.EqualError(t, Compose(func(e error) error { 27 | require.EqualError(t, e, "repeat.temporary: cat") 28 | return HintTemporary(e) 29 | })(HintTemporary(errors.New("cat"))), "repeat.temporary: cat") 30 | } 31 | 32 | func TestCompose_ErrInStopErrOut(t *testing.T) { 33 | require.EqualError(t, Compose(func(e error) error { 34 | return HintStop(e) 35 | })(errors.New("bob")), "repeat.stop: bob") 36 | } 37 | 38 | func TestCompose_NillOverridesTemporaryError(t *testing.T) { 39 | require.NoError(t, Compose( 40 | func(e error) error { 41 | return HintTemporary(e) 42 | }, 43 | Done, 44 | )(errors.New("ann"))) 45 | } 46 | 47 | func TestCompose_OverrideTemporaryError(t *testing.T) { 48 | require.EqualError(t, Compose( 49 | func(e error) error { 50 | return HintTemporary(e) 51 | }, 52 | func(e error) error { 53 | return HintTemporary(errors.New("pong")) 54 | }, 55 | )(errors.New("ping")), "repeat.temporary: pong") 56 | } 57 | 58 | func TestOnce_NonTemporaryErrOut(t *testing.T) { 59 | require.EqualError(t, Once(func(e error) error { 60 | return HintTemporary(errors.New("zed")) 61 | }), "zed") 62 | } 63 | 64 | func TestOnce_NonStopErrOut(t *testing.T) { 65 | require.NoError(t, Once(func(e error) error { 66 | return HintStop(nil) 67 | })) 68 | } 69 | 70 | func TestOnce_ErrOut(t *testing.T) { 71 | require.EqualError(t, Once(func(e error) error { 72 | return errors.New("aim") 73 | }), "aim") 74 | } 75 | 76 | func TestFnRepeat_NilInStopErrWithNilOut(t *testing.T) { 77 | require.NoError(t, FnRepeat( 78 | func(e error) error { 79 | require.NoError(t, e) 80 | return e 81 | }, 82 | StopOnSuccess(), 83 | )(nil)) 84 | } 85 | 86 | func TestFnRepeat_ErrInErrOut(t *testing.T) { 87 | require.EqualError(t, FnRepeat( 88 | func(e error) error { 89 | require.EqualError(t, e, "oil") 90 | return e 91 | }, 92 | )(errors.New("oil")), "oil") 93 | } 94 | 95 | func TestFnRepeat_TemporaryErrInSameStopErrOut(t *testing.T) { 96 | require.EqualError(t, FnRepeat( 97 | LimitMaxTries(1), 98 | func(e error) error { 99 | require.EqualError(t, e, "repeat.temporary: cat") 100 | return HintTemporary(e) 101 | }, 102 | )(HintTemporary(errors.New("cat"))), "repeat.stop: cat") 103 | } 104 | 105 | func TestFnRepeat_ErrInStopErrOut(t *testing.T) { 106 | require.EqualError(t, FnRepeat(func(e error) error { 107 | return HintStop(e) 108 | })(errors.New("bob")), "repeat.stop: bob") 109 | } 110 | 111 | func TestRepeat_WithNoErrors(t *testing.T) { 112 | cn := 0 113 | require.NoError(t, Repeat( 114 | LimitMaxTries(3), 115 | FnWithErrorAndCounter(func(e error, c int) error { 116 | defer func() { cn++ }() 117 | require.Equal(t, cn, c, "should be equal on every") 118 | 119 | switch { 120 | case c < 3: 121 | require.NoError(t, e, "no error on every call") 122 | default: 123 | require.Fail(t, "cant be here, only three tries") 124 | } 125 | 126 | return nil 127 | }), 128 | )) 129 | require.Equal(t, 3, cn) 130 | } 131 | 132 | func TestRepeat_WithTemporaryErrors(t *testing.T) { 133 | cn := 0 134 | require.EqualError(t, Repeat( 135 | LimitMaxTries(3), 136 | // Should be called three times until LimitMaxTries stops the execution. 137 | FnWithErrorAndCounter(func(e error, c int) error { 138 | defer func() { cn++ }() 139 | require.Equal(t, cn, c, "should be equal on every") 140 | 141 | switch c { 142 | case 0: 143 | require.NoError(t, e, 144 | "no error on first call (started with no error)") 145 | case 1, 2: 146 | require.EqualError(t, e, "repeat.temporary: my temporary", 147 | "the same error func returns") 148 | default: 149 | require.Fail(t, "cant be here") 150 | } 151 | 152 | return HintTemporary(errors.New("my temporary")) 153 | }), 154 | ), "my temporary") 155 | require.Equal(t, 3, cn) 156 | } 157 | 158 | func TestRepeat_WithErrors(t *testing.T) { 159 | cn := 0 160 | require.EqualError(t, Repeat( 161 | FnWithErrorAndCounter(func(e error, c int) error { 162 | defer func() { cn++ }() 163 | require.Equal(t, cn, c, "should be equal on every") 164 | 165 | if c == 2 { 166 | return errors.New("my real") 167 | } 168 | 169 | return HintTemporary(errors.New("my temporary")) 170 | }), 171 | ), "my real") 172 | require.Equal(t, 3, cn) 173 | } 174 | 175 | func TestRepeat_WithStopErrors(t *testing.T) { 176 | cn := 0 177 | require.EqualError(t, Repeat( 178 | FnWithErrorAndCounter(func(e error, c int) error { 179 | defer func() { cn++ }() 180 | require.Equal(t, cn, c, "should be equal on every") 181 | 182 | if c == 2 { 183 | return HintStop(errors.New("my real")) 184 | } 185 | 186 | return HintTemporary(errors.New("my temporary")) 187 | }), 188 | ), "my real") 189 | require.Equal(t, 3, cn) 190 | } 191 | 192 | func TestWrap(t *testing.T) { 193 | c := 0 194 | 195 | wr := func(op Operation) Operation { 196 | c++ 197 | return func(e error) error { 198 | return op(e) 199 | } 200 | } 201 | 202 | require.NoError(t, Wrap(wr).Compose(Nope, Nope)(nil)) 203 | require.Equal(t, 2, c, "wr called two times according to number of ops in Compose") 204 | } 205 | 206 | func TestCpp_C_D(t *testing.T) { 207 | c := 0 208 | 209 | cd := func(e error) error { 210 | c++ 211 | return e 212 | } 213 | 214 | require.NoError(t, Cpp(cd, cd).Compose(Nope)(nil)) 215 | require.Equal(t, 2, c) 216 | } 217 | 218 | func TestCpp_C_NoD(t *testing.T) { 219 | c := 0 220 | 221 | cd := func(e error) error { 222 | c++ 223 | return e 224 | } 225 | 226 | require.EqualError(t, Cpp(cd, cd).Compose(Nope)(errGolden), errGolden.Error()) 227 | require.Equal(t, 1, c) 228 | } 229 | 230 | func TestCpp_C_ErrOP_D(t *testing.T) { 231 | c := 0 232 | 233 | cd := func(e error) error { 234 | c++ 235 | return e 236 | } 237 | 238 | errOp := func(error) error { 239 | return errGolden 240 | } 241 | 242 | require.EqualError(t, Cpp(cd, FnDone(cd)).Compose(errOp)(nil), errGolden.Error()) 243 | require.Equal(t, 2, c) 244 | } 245 | 246 | func TestCpp_C_PanicOP_D(t *testing.T) { 247 | c := 0 248 | 249 | cd := func(e error) error { 250 | c++ 251 | return e 252 | } 253 | 254 | errOp := func(error) error { 255 | panic(errGolden) 256 | } 257 | 258 | require.Panics(t, func() { 259 | Cpp(cd, cd).Compose(errOp)(nil) 260 | }) 261 | require.Equal(t, 2, c) 262 | } 263 | 264 | func TestCpp_C_Op_ErrorD(t *testing.T) { 265 | c := 0 266 | 267 | cc := func(e error) error { 268 | c++ 269 | return e 270 | } 271 | 272 | dd := func(error) error { 273 | c++ 274 | return errGolden 275 | } 276 | 277 | require.Panics(t, func() { 278 | Cpp(cc, dd).Compose(Nope)(nil) 279 | }) 280 | require.Equal(t, 2, c) 281 | } 282 | 283 | func TestCpp_TransparentC(t *testing.T) { 284 | c := 0 285 | 286 | cc := func(e error) error { 287 | c++ 288 | require.EqualError(t, Cause(e), errGolden.Error()) 289 | return nil 290 | } 291 | 292 | dd := func(e error) error { 293 | c++ 294 | require.NoError(t, Cause(e)) 295 | return nil 296 | } 297 | 298 | op := func(e error) error { 299 | c++ 300 | require.EqualError(t, Cause(e), errGolden.Error()) 301 | return nil 302 | } 303 | 304 | require.NoError(t, Cause( 305 | Cpp(cc, dd).Compose(op)(HintTemporary(errGolden)), 306 | )) 307 | require.Equal(t, 3, c) 308 | } 309 | 310 | func TestWith_C_D(t *testing.T) { 311 | c := 0 312 | 313 | cd := func(e error) error { 314 | c++ 315 | return e 316 | } 317 | 318 | require.NoError(t, With(cd, cd).Compose(Nope)(nil)) 319 | require.Equal(t, 2, c) 320 | } 321 | 322 | func TestWith_C_NoD(t *testing.T) { 323 | c := 0 324 | 325 | cd := func(e error) error { 326 | c++ 327 | return e 328 | } 329 | 330 | require.EqualError(t, Cpp(cd, cd).Compose(Nope)(errGolden), errGolden.Error()) 331 | require.Equal(t, 1, c) 332 | } 333 | 334 | func TestWith_C_PanicOP_D(t *testing.T) { 335 | c := 0 336 | 337 | cd := func(e error) error { 338 | c++ 339 | return e 340 | } 341 | 342 | errOp := func(error) error { 343 | panic(errGolden) 344 | } 345 | 346 | require.Panics(t, func() { 347 | With(cd, cd).Compose(errOp)(nil) 348 | }) 349 | require.Equal(t, 2, c) 350 | } 351 | 352 | func TestWith_C_ErrOP_DoneD(t *testing.T) { 353 | c := 0 354 | 355 | cd := func(e error) error { 356 | c++ 357 | return e 358 | } 359 | 360 | errOp := func(error) error { 361 | return errGolden 362 | } 363 | 364 | require.NoError(t, With(cd, FnDone(cd)).Compose(errOp)(nil)) 365 | require.Equal(t, 2, c) 366 | } 367 | 368 | func TestWith_C_DoneOP_ErrD(t *testing.T) { 369 | counter := 0 370 | 371 | c := func(e error) error { 372 | counter++ 373 | return e 374 | } 375 | 376 | d := func(error) error { 377 | counter++ 378 | return errGolden 379 | } 380 | 381 | require.EqualError(t, With(c, d).Compose(Nope)(nil), errGolden.Error()) 382 | require.Equal(t, 2, counter) 383 | } 384 | 385 | func TestRepeatWithContext(t *testing.T) { 386 | ctx, cancel := context.WithCancel(context.Background()) 387 | cancel() 388 | 389 | require.EqualError(t, WithContext(ctx).Once(Nope), "context canceled") 390 | } 391 | --------------------------------------------------------------------------------