blob: f2914da40a9ad2cffeb33ce7e6d271f4f9f96b68 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package retry
import (
"context"
"errors"
"time"
)
type fatalError struct {
error
}
func (e fatalError) Unwrap() error {
return e.error
}
// Fatal tags an error as fatal. If the callback passed to Retry() returns a
// fatal error, Retry() will quit retrying and exit early (returning the error
// passed to Fatal) even there are remaining attempts.
func Fatal(err error) error {
if err == nil {
// As a convenience, return a nil error if the input is nil. This way
// users can unconditionally return Fatal(err) if err is a fatal error
// or nil, and it will just work.
return nil
}
return fatalError{error: err}
}
// Retry the operation using the provided back-off policy until it succeeds, or
// the context is cancelled. Any intermediate errors (but not the final error)
// returned by the function will be sent on the given channel, if the channel is
// non-nil.
//
// If f returns an error wrapped with Fatal(), Retry() will stop retrying the
// operation and exit, returning the wrapped error.
func Retry(ctx context.Context, b Backoff, f func() error, c chan<- error) error {
b.Reset()
var timer *time.Timer
for {
err := f()
if err == nil {
return nil
}
var fatalErr fatalError
if errors.As(err, &fatalErr) {
// Return the original error.
return fatalErr.error
}
next := b.Next()
if next == Stop {
return err
}
if timer == nil {
timer = time.NewTimer(next)
defer timer.Stop()
} else {
// Reset() is only safe to call after the timer has fired and its
// channel has been drained. At this point in the loop we know that
// the timer's channel was drained on the last iteration (otherwise
// we would have exited the loop), so it's safe to call Reset().
timer.Reset(next)
}
// If the context has already been canceled, exit immediately rather
// than entering the select statement to ensure determinism when the
// backoff is zero. If multiple channels in a select statement are ready
// when entering the statement, Go will randomly choose one, which is
// not the desired behavior here - we always want to stop once the
// context gets canceled.
if ctx.Err() != nil {
return err
}
select {
case <-ctx.Done():
return err
case <-timer.C:
if c != nil {
c <- err
}
}
}
}