diff --git a/internal/retry/retry.go b/internal/retry/retry.go new file mode 100644 index 0000000..c7948e8 --- /dev/null +++ b/internal/retry/retry.go @@ -0,0 +1,45 @@ +package retry + +import ( + "context" + "fmt" + "log/slog" + "time" +) + +type Retrier struct { + maxRetries int + backoff []time.Duration +} + +func NewRetrier(maxRetries int, backoff []time.Duration) *Retrier { + return &Retrier{maxRetries: maxRetries, backoff: backoff} +} + +// DefaultRetrier returns a retrier with 3 attempts, exponential backoff: 1s, 5s, 30s. +func DefaultRetrier() *Retrier { + return NewRetrier(3, []time.Duration{1 * time.Second, 5 * time.Second, 30 * time.Second}) +} + +func (r *Retrier) Do(ctx context.Context, fn func() error) error { + var lastErr error + for attempt := 0; attempt <= r.maxRetries; attempt++ { + if attempt > 0 { + delay := r.backoff[attempt-1] + slog.Info("retrying", "attempt", attempt, "delay", delay) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(delay): + } + } + + err := fn() + if err == nil { + return nil + } + lastErr = err + slog.Warn("attempt failed", "attempt", attempt, "error", err) + } + return fmt.Errorf("all %d attempts failed, last error: %w", r.maxRetries+1, lastErr) +}