├── go.mod ├── sandbox ├── docs.go ├── go.mod └── strategy.go ├── examples ├── go.mod └── docs.go ├── docs.go ├── context.go ├── errors.go ├── LICENSE ├── backoff └── backoff.go ├── jitter └── jitter.go ├── retry.go └── strategy └── strategy.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kamilsk/retry/v5 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /sandbox/docs.go: -------------------------------------------------------------------------------- 1 | // Package exp contains experimental unstable 2 | // features. 3 | package sandbox 4 | -------------------------------------------------------------------------------- /sandbox/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kamilsk/retry/sandbox 2 | 3 | go 1.11 4 | 5 | require github.com/kamilsk/retry/v5 v5.0.0-rc8 6 | 7 | replace github.com/kamilsk/retry/v5 => ../ 8 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kamilsk/retry/examples 2 | 3 | go 1.13 4 | 5 | require github.com/kamilsk/retry/v5 v5.0.0-rc8 6 | 7 | replace github.com/kamilsk/retry/v5 => ../ 8 | -------------------------------------------------------------------------------- /examples/docs.go: -------------------------------------------------------------------------------- 1 | // Package examples contains extended documentation 2 | // for github.com/kamilsk/retry/v5 module. 3 | // 4 | // It contains examples of usage with additional 5 | // dependencies that are not needed by the module. 6 | package examples 7 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | // Package retry provides the most advanced interruptible mechanism 2 | // to perform actions repetitively until successful. 3 | // 4 | // The retry based on https://github.com/Rican7/retry but fully reworked 5 | // and focused on integration with the https://github.com/kamilsk/breaker 6 | // and the built-in https://pkg.go.dev/context package. 7 | package retry 8 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import "context" 4 | 5 | func convert(breaker Breaker) context.Context { 6 | ctx, is := breaker.(context.Context) 7 | if !is { 8 | ctx = lite{context.Background(), breaker} 9 | } 10 | return ctx 11 | } 12 | 13 | type lite struct { 14 | context.Context 15 | breaker Breaker 16 | } 17 | 18 | func (ctx lite) Done() <-chan struct{} { return ctx.breaker.Done() } 19 | func (ctx lite) Err() error { return ctx.breaker.Err() } 20 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | const internal Error = "have no any try" 4 | 5 | // Error defines a string-based error without a different root cause. 6 | type Error string 7 | 8 | // Error returns a string representation of an error. 9 | func (err Error) Error() string { return string(err) } 10 | 11 | // Unwrap always returns nil means that an error doesn't have other root cause. 12 | func (err Error) Unwrap() error { return nil } 13 | 14 | func unwrap(err error) error { 15 | for err != nil { 16 | layer, is := err.(wrapper) 17 | if is { 18 | err = layer.Unwrap() 19 | continue 20 | } 21 | cause, is := err.(causer) 22 | if is { 23 | err = cause.Cause() 24 | continue 25 | } 26 | break 27 | } 28 | return err 29 | } 30 | 31 | // compatible with github.com/pkg/errors 32 | type causer interface { 33 | Cause() error 34 | } 35 | 36 | // compatible with built-in errors since 1.13 37 | type wrapper interface { 38 | Unwrap() error 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 OctoLab, https://www.octolab.org/ 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 | -------------------------------------------------------------------------------- /sandbox/strategy.go: -------------------------------------------------------------------------------- 1 | package sandbox 2 | 3 | import "net" 4 | 5 | const ( 6 | Skip = true 7 | Stop = false 8 | ) 9 | 10 | // A Breaker carries a cancellation signal to interrupt an action execution. 11 | // 12 | // It is a subset of the built-in context and github.com/kamilsk/breaker interfaces. 13 | type Breaker = interface { 14 | // Done returns a channel that's closed when a cancellation signal occurred. 15 | Done() <-chan struct{} 16 | // If Done is not yet closed, Err returns nil. 17 | // If Done is closed, Err returns a non-nil error. 18 | // After Err returns a non-nil error, successive calls to Err return the same error. 19 | Err() error 20 | } 21 | 22 | // ErrorHandler defines a function that CheckError calls 23 | // to determine whether it should make the next attempt or not. 24 | // Returning true allows for the next attempt to be made. 25 | // Returning false halts the retrying process and returns the last error 26 | // returned by the called Action. 27 | type ErrorHandler = func(error) bool 28 | 29 | // CheckError creates a Strategy that checks an error and returns 30 | // if an error is retriable or not. Otherwise, it returns the defaults. 31 | func CheckError(handlers ...func(error) bool) func(Breaker, uint, error) bool { 32 | // equal to go.octolab.org/errors.Retriable 33 | type retriable interface { 34 | error 35 | Retriable() bool // Is the error retriable? 36 | } 37 | 38 | return func(_ Breaker, _ uint, err error) bool { 39 | if err == nil { 40 | return true 41 | } 42 | if err, is := err.(retriable); is { 43 | return err.Retriable() 44 | } 45 | for _, handle := range handlers { 46 | if !handle(err) { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | } 53 | 54 | // NetworkError creates an error Handler that checks an error and returns true 55 | // if an error is the temporary network error. 56 | // The Handler returns the defaults if an error is not a network error. 57 | func NetworkError(defaults bool) func(error) bool { 58 | return func(err error) bool { 59 | if err, is := err.(net.Error); is { 60 | return err.Temporary() || err.Timeout() 61 | } 62 | return defaults 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /backoff/backoff.go: -------------------------------------------------------------------------------- 1 | // Package backoff provides stateless methods of calculating durations based on 2 | // a number of attempts made. 3 | package backoff 4 | 5 | import ( 6 | "math" 7 | "time" 8 | ) 9 | 10 | // Algorithm defines a function that calculates a time.Duration based on 11 | // the given retry attempt number. 12 | type Algorithm = func(attempt uint) time.Duration 13 | 14 | // Constant creates an Algorithm that returns the initial duration 15 | // by the all time. 16 | func Constant(duration time.Duration) Algorithm { 17 | return func(uint) time.Duration { 18 | return duration 19 | } 20 | } 21 | 22 | // Incremental creates an Algorithm that increments the initial duration 23 | // by the given increment for each attempt. 24 | func Incremental(initial, increment time.Duration) Algorithm { 25 | return func(attempt uint) time.Duration { 26 | return initial + (increment * time.Duration(attempt)) 27 | } 28 | } 29 | 30 | // Linear creates an Algorithm that linearly multiplies the factor 31 | // duration by the attempt number for each attempt. 32 | func Linear(factor time.Duration) Algorithm { 33 | return Incremental(0, factor) 34 | } 35 | 36 | // Exponential creates an Algorithm that multiplies the factor duration by 37 | // an exponentially increasing factor for each attempt, where the factor is 38 | // calculated as the given base raised to the attempt number. 39 | func Exponential(factor time.Duration, base float64) Algorithm { 40 | return func(attempt uint) time.Duration { 41 | return factor * time.Duration(math.Pow(base, float64(attempt))) 42 | } 43 | } 44 | 45 | // BinaryExponential creates an Algorithm that multiplies the factor 46 | // duration by an exponentially increasing factor for each attempt, where the 47 | // factor is calculated as 2 raised to the attempt number (2^attempt). 48 | func BinaryExponential(factor time.Duration) Algorithm { 49 | return Exponential(factor, 2) 50 | } 51 | 52 | // Fibonacci creates an Algorithm that multiplies the factor duration by 53 | // an increasing factor for each attempt, where the factor is the Nth number in 54 | // the Fibonacci sequence. 55 | func Fibonacci(factor time.Duration) Algorithm { 56 | return func(attempt uint) time.Duration { 57 | n := attempt 58 | if n != 0 { 59 | var a, b uint = 0, 1 60 | for i := uint(1); i < attempt; i++ { 61 | a, b = b, a+b 62 | } 63 | n = b 64 | } 65 | return factor * time.Duration(n) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /jitter/jitter.go: -------------------------------------------------------------------------------- 1 | // Package jitter provides methods of transforming durations. 2 | package jitter 3 | 4 | import ( 5 | "math" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | // Transformation defines a function that calculates a time.Duration based on 11 | // the given duration. 12 | type Transformation = func(duration time.Duration) time.Duration 13 | 14 | // Full creates a Transformation that transforms a duration into a result 15 | // duration in [0, n) randomly, where n is the given duration. 16 | // 17 | // The given generator is what is used to determine the random transformation. 18 | // 19 | // Inspired by https://www.awsarchitectureblog.com/2015/03/backoff.html 20 | func Full(generator *rand.Rand) Transformation { 21 | return func(duration time.Duration) time.Duration { 22 | return time.Duration(generator.Int63n(int64(duration))) 23 | } 24 | } 25 | 26 | // Equal creates a Transformation that transforms a duration into a result 27 | // duration in [n/2, n) randomly, where n is the given duration. 28 | // 29 | // The given generator is what is used to determine the random transformation. 30 | // 31 | // Inspired by https://www.awsarchitectureblog.com/2015/03/backoff.html 32 | func Equal(generator *rand.Rand) Transformation { 33 | return func(duration time.Duration) time.Duration { 34 | return (duration / 2) + time.Duration(generator.Int63n(int64(duration))/2) 35 | } 36 | } 37 | 38 | // Deviation creates a Transformation that transforms a duration into a result 39 | // duration that deviates from the input randomly by a given factor. 40 | // 41 | // The given generator is what is used to determine the random transformation. 42 | // 43 | // Inspired by https://developers.google.com/api-client-library/java/google-http-java-client/backoff 44 | func Deviation(generator *rand.Rand, factor float64) Transformation { 45 | return func(duration time.Duration) time.Duration { 46 | min := int64(math.Floor(float64(duration) * (1 - factor))) 47 | max := int64(math.Ceil(float64(duration) * (1 + factor))) 48 | return time.Duration(generator.Int63n(max-min) + min) 49 | } 50 | } 51 | 52 | // NormalDistribution creates a Transformation that transforms a duration into a 53 | // result duration based on a normal distribution of the input and the given 54 | // standard deviation. 55 | // 56 | // The given generator is what is used to determine the random transformation. 57 | func NormalDistribution(generator *rand.Rand, standardDeviation float64) Transformation { 58 | return func(duration time.Duration) time.Duration { 59 | return time.Duration(generator.NormFloat64()*standardDeviation + float64(duration)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // Action defines a callable function that package retry can handle. 9 | type Action = func(context.Context) error 10 | 11 | // A Breaker carries a cancellation signal to interrupt an action execution. 12 | // 13 | // It is a subset of the built-in context and github.com/kamilsk/breaker interfaces. 14 | type Breaker = interface { 15 | // Done returns a channel that's closed when a cancellation signal occurred. 16 | Done() <-chan struct{} 17 | // If Done is not yet closed, Err returns nil. 18 | // If Done is closed, Err returns a non-nil error. 19 | // After Err returns a non-nil error, successive calls to Err return the same error. 20 | Err() error 21 | } 22 | 23 | // How is an alias for batch of Strategies. 24 | // 25 | // how := retry.How{ 26 | // strategy.Limit(3), 27 | // } 28 | // 29 | type How = []func(Breaker, uint, error) bool 30 | 31 | // Do takes the action and performs it, repetitively, until successful. 32 | // 33 | // Optionally, strategies may be passed that assess whether or not an attempt 34 | // should be made. 35 | func Do( 36 | breaker Breaker, 37 | action func(context.Context) error, 38 | strategies ...func(Breaker, uint, error) bool, 39 | ) error { 40 | var ( 41 | ctx = convert(breaker) 42 | err error = internal 43 | core error 44 | ) 45 | 46 | for attempt, should := uint(0), true; should; attempt++ { 47 | core = unwrap(err) 48 | for i, repeat := 0, len(strategies); should && i < repeat; i++ { 49 | should = should && strategies[i](breaker, attempt, core) 50 | } 51 | 52 | select { 53 | case <-breaker.Done(): 54 | return breaker.Err() 55 | default: 56 | if should { 57 | err = action(ctx) 58 | } 59 | } 60 | 61 | should = should && err != nil 62 | } 63 | 64 | return err 65 | } 66 | 67 | // Go takes the action and performs it, repetitively, until successful. 68 | // It differs from the Do method in that it performs the action in a goroutine. 69 | // 70 | // Optionally, strategies may be passed that assess whether or not an attempt 71 | // should be made. 72 | func Go( 73 | breaker Breaker, 74 | action func(context.Context) error, 75 | strategies ...func(Breaker, uint, error) bool, 76 | ) error { 77 | done := make(chan error, 1) 78 | 79 | go func() { 80 | defer func() { 81 | if r := recover(); r != nil { 82 | err, ok := r.(error) 83 | if !ok { 84 | err = fmt.Errorf("retry: unexpected panic: %#v", r) 85 | } 86 | done <- err 87 | } 88 | close(done) 89 | }() 90 | done <- Do(breaker, action, strategies...) 91 | }() 92 | 93 | select { 94 | case <-breaker.Done(): 95 | return breaker.Err() 96 | case err := <-done: 97 | return err 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /strategy/strategy.go: -------------------------------------------------------------------------------- 1 | // Package strategy provides a way to define how retry is performed. 2 | package strategy 3 | 4 | import "time" 5 | 6 | // A Breaker carries a cancellation signal to interrupt an action execution. 7 | // 8 | // It is a subset of the built-in context and github.com/kamilsk/breaker interfaces. 9 | type Breaker = interface { 10 | // Done returns a channel that's closed when a cancellation signal occurred. 11 | Done() <-chan struct{} 12 | // If Done is not yet closed, Err returns nil. 13 | // If Done is closed, Err returns a non-nil error. 14 | // After Err returns a non-nil error, successive calls to Err return the same error. 15 | Err() error 16 | } 17 | 18 | // Strategy defines a function that Retry calls before every successive attempt 19 | // to determine whether it should make the next attempt or not. Returning true 20 | // allows for the next attempt to be made. Returning false halts the retrying 21 | // process and returns the last error returned by the called Action. 22 | // 23 | // The strategy will be passed an "attempt" number on each successive retry 24 | // iteration, starting with a 0 value before the first attempt is actually 25 | // made. This allows for a pre-action delay, etc. 26 | type Strategy = func(breaker Breaker, attempt uint, err error) bool 27 | 28 | // Limit creates a Strategy that limits the number of attempts 29 | // that Retry will make. 30 | func Limit(value uint) Strategy { 31 | return func(_ Breaker, attempt uint, _ error) bool { 32 | return attempt < value 33 | } 34 | } 35 | 36 | // Delay creates a Strategy that waits the given duration 37 | // before the first attempt is made. 38 | func Delay(duration time.Duration) Strategy { 39 | return func(breaker Breaker, attempt uint, _ error) bool { 40 | keep := true 41 | if attempt == 0 { 42 | timer := time.NewTimer(duration) 43 | select { 44 | case <-timer.C: 45 | case <-breaker.Done(): 46 | keep = false 47 | } 48 | stop(timer) 49 | } 50 | return keep 51 | } 52 | } 53 | 54 | // Wait creates a Strategy that waits the given durations for each attempt after 55 | // the first. If the number of attempts is greater than the number of durations 56 | // provided, then the strategy uses the last duration provided. 57 | func Wait(durations ...time.Duration) Strategy { 58 | return func(breaker Breaker, attempt uint, _ error) bool { 59 | keep := true 60 | if attempt > 0 && len(durations) > 0 { 61 | durationIndex := int(attempt - 1) 62 | if len(durations) <= durationIndex { 63 | durationIndex = len(durations) - 1 64 | } 65 | timer := time.NewTimer(durations[durationIndex]) 66 | select { 67 | case <-timer.C: 68 | case <-breaker.Done(): 69 | keep = false 70 | } 71 | stop(timer) 72 | } 73 | return keep 74 | } 75 | } 76 | 77 | // Backoff creates a Strategy that waits before each attempt, with a duration as 78 | // defined by the given backoff.Algorithm. 79 | func Backoff(algorithm func(attempt uint) time.Duration) Strategy { 80 | return BackoffWithJitter(algorithm, func(duration time.Duration) time.Duration { 81 | return duration 82 | }) 83 | } 84 | 85 | // BackoffWithJitter creates a Strategy that waits before each attempt, with a 86 | // duration as defined by the given backoff.Algorithm and jitter.Transformation. 87 | func BackoffWithJitter( 88 | algorithm func(attempt uint) time.Duration, 89 | transformation func(duration time.Duration) time.Duration, 90 | ) Strategy { 91 | return func(breaker Breaker, attempt uint, _ error) bool { 92 | keep := true 93 | if attempt > 0 { 94 | timer := time.NewTimer(transformation(algorithm(attempt))) 95 | select { 96 | case <-timer.C: 97 | case <-breaker.Done(): 98 | keep = false 99 | } 100 | stop(timer) 101 | } 102 | return keep 103 | } 104 | } 105 | 106 | func stop(timer *time.Timer) { 107 | if !timer.Stop() { 108 | select { 109 | case <-timer.C: 110 | default: 111 | } 112 | } 113 | } 114 | --------------------------------------------------------------------------------