// Exercise 06 — Retry with Exponential Backoff + Jitter // // ASSIGNMENT // ---------- // Implement Retry(ctx, attempts, base, fn): // // 1. Calls fn up to `attempts` times; returns nil on first success. // 2. Retries ONLY errors matching ErrRetryable (errors.Is — fn may wrap it). // Non-retryable errors are returned immediately. // 3. Between attempt i and i+1, sleeps a random duration in // [0, base * 2^i) — exponential backoff with FULL JITTER. // 4. The sleep must be interruptible by ctx: if ctx is cancelled mid-sleep, // return promptly with an error that matches ctx.Err() (errors.Is). // 5. If all attempts fail, return an error that wraps the last error from fn. // 6. Never sleep after the final attempt. // // Run the tests: go test -race ./06-retry-backoff/ package main import ( "context" "errors" "fmt" "time" ) // ErrRetryable marks transient failures. fn returns errors wrapping this // to request a retry. var ErrRetryable = errors.New("retryable") // Retry runs fn with exponential backoff + full jitter. // TODO: implement me. func Retry(ctx context.Context, attempts int, base time.Duration, fn func(context.Context) error) error { // TODO: // - loop attempts times, call fn // - errors.Is(err, ErrRetryable) to classify // - backoff := base << i ; sleep := rand in [0, backoff) // - select { time.After(sleep) / ctx.Done() } // - wrap the final error with fmt.Errorf("... %w", err) return nil } func main() { i := 0 err := Retry(context.Background(), 5, 50*time.Millisecond, func(ctx context.Context) error { i++ if i < 3 { return fmt.Errorf("attempt %d flaked: %w", i, ErrRetryable) } return nil }) fmt.Println("attempts:", i, "err:", err) }