package retry import ( "context" "errors" "time" ) var ErrInvalidAttempts = errors.New("retry: attempts must be greater than zero") // Do calls fn until it succeeds, the context is canceled, or attempts are exhausted. // Delays start at initialDelay, double after each failure, and are capped at maxDelay. func Do( ctx context.Context, attempts int, initialDelay time.Duration, maxDelay time.Duration, fn func(context.Context) error, ) error { if attempts <= 0 { return ErrInvalidAttempts } if fn == nil { return errors.New("retry: nil function") } 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 := 0; attempt < attempts; attempt++ { if err := ctx.Err(); err != nil { return err } if err := fn(ctx); err == nil { return nil } else { lastErr = err } if attempt == attempts-1 { break } timer := time.NewTimer(delay) select { case <-timer.C: case <-ctx.Done(): if !timer.Stop() { <-timer.C } return ctx.Err() } // Double without overflowing, respecting maxDelay when configured. if maxDelay > 0 && delay >= maxDelay-delay { delay = maxDelay } else if delay > 0 && delay > time.Duration(1<<63-1)/2 { delay = time.Duration(1<<63 - 1) } else { delay *= 2 if maxDelay > 0 && delay > maxDelay { delay = maxDelay } } } return lastErr }