forked from toolshed/abra
140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
package backoff
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// DefaultMaxElapsedTime sets a default limit for the total retry duration.
|
|
const DefaultMaxElapsedTime = 15 * time.Minute
|
|
|
|
// Operation is a function that attempts an operation and may be retried.
|
|
type Operation[T any] func() (T, error)
|
|
|
|
// Notify is a function called on operation error with the error and backoff duration.
|
|
type Notify func(error, time.Duration)
|
|
|
|
// retryOptions holds configuration settings for the retry mechanism.
|
|
type retryOptions struct {
|
|
BackOff BackOff // Strategy for calculating backoff periods.
|
|
Timer timer // Timer to manage retry delays.
|
|
Notify Notify // Optional function to notify on each retry error.
|
|
MaxTries uint // Maximum number of retry attempts.
|
|
MaxElapsedTime time.Duration // Maximum total time for all retries.
|
|
}
|
|
|
|
type RetryOption func(*retryOptions)
|
|
|
|
// WithBackOff configures a custom backoff strategy.
|
|
func WithBackOff(b BackOff) RetryOption {
|
|
return func(args *retryOptions) {
|
|
args.BackOff = b
|
|
}
|
|
}
|
|
|
|
// withTimer sets a custom timer for managing delays between retries.
|
|
func withTimer(t timer) RetryOption {
|
|
return func(args *retryOptions) {
|
|
args.Timer = t
|
|
}
|
|
}
|
|
|
|
// WithNotify sets a notification function to handle retry errors.
|
|
func WithNotify(n Notify) RetryOption {
|
|
return func(args *retryOptions) {
|
|
args.Notify = n
|
|
}
|
|
}
|
|
|
|
// WithMaxTries limits the number of all attempts.
|
|
func WithMaxTries(n uint) RetryOption {
|
|
return func(args *retryOptions) {
|
|
args.MaxTries = n
|
|
}
|
|
}
|
|
|
|
// WithMaxElapsedTime limits the total duration for retry attempts.
|
|
func WithMaxElapsedTime(d time.Duration) RetryOption {
|
|
return func(args *retryOptions) {
|
|
args.MaxElapsedTime = d
|
|
}
|
|
}
|
|
|
|
// Retry attempts the operation until success, a permanent error, or backoff completion.
|
|
// It ensures the operation is executed at least once.
|
|
//
|
|
// Returns the operation result or error if retries are exhausted or context is cancelled.
|
|
func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOption) (T, error) {
|
|
// Initialize default retry options.
|
|
args := &retryOptions{
|
|
BackOff: NewExponentialBackOff(),
|
|
Timer: &defaultTimer{},
|
|
MaxElapsedTime: DefaultMaxElapsedTime,
|
|
}
|
|
|
|
// Apply user-provided options to the default settings.
|
|
for _, opt := range opts {
|
|
opt(args)
|
|
}
|
|
|
|
defer args.Timer.Stop()
|
|
|
|
startedAt := time.Now()
|
|
args.BackOff.Reset()
|
|
for numTries := uint(1); ; numTries++ {
|
|
// Execute the operation.
|
|
res, err := operation()
|
|
if err == nil {
|
|
return res, nil
|
|
}
|
|
|
|
// Stop retrying if maximum tries exceeded.
|
|
if args.MaxTries > 0 && numTries >= args.MaxTries {
|
|
return res, err
|
|
}
|
|
|
|
// Handle permanent errors without retrying.
|
|
var permanent *PermanentError
|
|
if errors.As(err, &permanent) {
|
|
return res, permanent.Unwrap()
|
|
}
|
|
|
|
// Stop retrying if context is cancelled.
|
|
if cerr := context.Cause(ctx); cerr != nil {
|
|
return res, cerr
|
|
}
|
|
|
|
// Calculate next backoff duration.
|
|
next := args.BackOff.NextBackOff()
|
|
if next == Stop {
|
|
return res, err
|
|
}
|
|
|
|
// Reset backoff if RetryAfterError is encountered.
|
|
var retryAfter *RetryAfterError
|
|
if errors.As(err, &retryAfter) {
|
|
next = retryAfter.Duration
|
|
args.BackOff.Reset()
|
|
}
|
|
|
|
// Stop retrying if maximum elapsed time exceeded.
|
|
if args.MaxElapsedTime > 0 && time.Since(startedAt)+next > args.MaxElapsedTime {
|
|
return res, err
|
|
}
|
|
|
|
// Notify on error if a notifier function is provided.
|
|
if args.Notify != nil {
|
|
args.Notify(err, next)
|
|
}
|
|
|
|
// Wait for the next backoff period or context cancellation.
|
|
args.Timer.Start(next)
|
|
select {
|
|
case <-args.Timer.C():
|
|
case <-ctx.Done():
|
|
return res, context.Cause(ctx)
|
|
}
|
|
}
|
|
}
|