package retry import ( "context" "errors" "time" ) // Config controls retry attempts and exponential backoff. type Config struct { MaxAttempts int InitialDelay time.Duration MaxDelay time.Duration } // Retry calls operation until it succeeds, the attempt limit is reached, // or the context is canceled. func Retry(ctx context.Context, cfg Config, operation func(context.Context) error) error { if operation == nil { return errors.New("retry: operation is nil") } if cfg.MaxAttempts < 1 { return errors.New("retry: MaxAttempts must be at least 1") } if cfg.InitialDelay < 0 || cfg.MaxDelay < 0 { return errors.New("retry: delays cannot be negative") } if cfg.MaxDelay > 0 && cfg.InitialDelay > cfg.MaxDelay { return errors.New("retry: InitialDelay exceeds MaxDelay") } delay := cfg.InitialDelay var lastErr error for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ { if err := ctx.Err(); err != nil { return err } lastErr = operation(ctx) if lastErr == nil { return nil } if attempt == cfg.MaxAttempts { break } timer := time.NewTimer(delay) select { case <-timer.C: case <-ctx.Done(): if !timer.Stop() { <-timer.C } return ctx.Err() } // Double the delay without overflowing or exceeding MaxDelay. if delay > 0 { if cfg.MaxDelay > 0 && delay >= cfg.MaxDelay { delay = cfg.MaxDelay } else if delay > time.Duration(1<<63-1)/2 { delay = time.Duration(1<<63 - 1) } else { delay *= 2 if cfg.MaxDelay > 0 && delay > cfg.MaxDelay { delay = cfg.MaxDelay } } } } return lastErr }