package retry import ( "context" "errors" "fmt" "time" ) // ErrInvalidAttempts indicates that Retry was called without any attempts. var ErrInvalidAttempts = errors.New("retry: attempts must be greater than zero") // Retry calls operation until it succeeds, the context is canceled, or all // attempts are exhausted. The delay doubles after each failed attempt. func Retry( ctx context.Context, attempts int, initialDelay time.Duration, maxDelay time.Duration, operation func() error, ) error { if attempts <= 0 { return ErrInvalidAttempts } if operation == nil { return errors.New("retry: operation is nil") } if initialDelay < 0 || maxDelay < 0 { return errors.New("retry: delays must not be negative") } if maxDelay > 0 && initialDelay > maxDelay { initialDelay = maxDelay } delay := initialDelay var lastErr error for attempt := 1; attempt <= attempts; attempt++ { if err := ctx.Err(); err != nil { return err } if err := operation(); err == nil { return nil } else { lastErr = err } if attempt == attempts { break } timer := time.NewTimer(delay) select { case <-timer.C: case <-ctx.Done(): if !timer.Stop() { <-timer.C } return ctx.Err() } delay = nextDelay(delay, maxDelay) } return fmt.Errorf("retry: failed after %d attempts: %w", attempts, lastErr) } func nextDelay(current, maximum time.Duration) time.Duration { if maximum > 0 && current >= maximum { return maximum } if current > time.Duration(1<<63-1)/2 { if maximum > 0 { return maximum } return time.Duration(1<<63 - 1) } next := current * 2 if maximum > 0 && next > maximum { return maximum } return next }