nmagent/internal/retry.go (87 lines of code) (raw):

package internal import ( "context" "errors" "math" "time" pkgerrors "github.com/pkg/errors" ) const ( noDelay = 0 * time.Nanosecond ) const ( ErrMaxAttempts = Error("maximum attempts reached") ) // TemporaryError is an error that can indicate whether it may be resolved with // another attempt. type TemporaryError interface { error Temporary() bool } // Retrier is a construct for attempting some operation multiple times with a // configurable backoff strategy. type Retrier struct { Cooldown CooldownFactory } // Do repeatedly invokes the provided run function while the context remains // active. It waits in between invocations of the provided functions by // delegating to the provided Cooldown function. func (r Retrier) Do(ctx context.Context, run func() error) error { cooldown := r.Cooldown() for { if err := ctx.Err(); err != nil { // nolint:wrapcheck // no meaningful information can be added to this error return err } err := run() if err != nil { // check to see if it's temporary. var tempErr TemporaryError if ok := errors.As(err, &tempErr); ok && tempErr.Temporary() { delay, err := cooldown() // nolint:govet // the shadow is intentional if err != nil { return pkgerrors.Wrap(err, "sleeping during retry") } time.Sleep(delay) continue } // since it's not temporary, it can't be retried, so... return err } return nil } } // CooldownFunc is a function that will block when called. It is intended for // use with retry logic. type CooldownFunc func() (time.Duration, error) // CooldownFactory is a function that returns CooldownFuncs. It helps // CooldownFuncs dispose of any accumulated state so that they function // correctly upon successive uses. type CooldownFactory func() CooldownFunc // Max provides a fixed limit for the number of times a subordinate cooldown // function can be invoked. func Max(limit int, factory CooldownFactory) CooldownFactory { return func() CooldownFunc { cooldown := factory() count := 0 return func() (time.Duration, error) { if count >= limit { return noDelay, ErrMaxAttempts } delay, err := cooldown() if err != nil { return noDelay, err } count++ return delay, nil } } } // AsFastAsPossible is a Cooldown strategy that does not block, allowing retry // logic to proceed as fast as possible. This is particularly useful in tests. func AsFastAsPossible() CooldownFactory { return func() CooldownFunc { return func() (time.Duration, error) { return noDelay, nil } } } // Exponential provides an exponential increase the the base interval provided. func Exponential(interval time.Duration, base int) CooldownFactory { return func() CooldownFunc { count := 0 return func() (time.Duration, error) { increment := math.Pow(float64(base), float64(count)) delay := interval.Nanoseconds() * int64(increment) count++ return time.Duration(delay), nil } } } // Fixed produced the same delay value upon each invocation. func Fixed(delay time.Duration) CooldownFactory { return func() CooldownFunc { return func() (time.Duration, error) { return delay, nil } } }