go-retry

repository·main·Indexed 20 days ago

https://github.com/sethvargo/go-retry

A highly extensible Go library for implementing retry logic and backoff strategies. It provides built-in algorithms such as Constant, Exponential, and Fibonacci, along with middleware modifiers for adding jitter, capping individual durations, limiting total elapsed time, and setting maximum retry counts.

Tokens
3.6K
Snippets
17
Records
19
Agent score
71%

What's inside go-retry

  1. Configure backoff behavior with Modifiers (Middleware)

    main

    Since base backoff algorithms are infinite, you must use middleware (modifiers) to add limits, caps, or randomness. Modifiers wrap an existing backoff.

    Jitter (Randomness)

    Reduces 'thundering herd' issues by adding randomness.

    • WithJitter(duration, b): Returns the next value +/- the specified duration.
    • WithJitterPercent(percent, b): Returns the next value +/- a percentage of the result.
    • WithFullJitter(b): Returns a random value in the range [0, next value).

    Limits and Caps

    • WithMaxRetries(count, b): Stops after a specific number of retries. Note: total attempts will be count + 1.
    • WithCappedDuration(duration, b): Ensures no single calculated sleep duration exceeds the specified value.
    • WithMaxDuration(duration, b): Sets a best-effort limit on the total elapsed time for all retry attempts combined.
    b := retry.NewFibonacci(1 * time.Second)
    
    // Add jitter (+/- 500ms)
    b = retry.WithJitter(500*time.Millisecond, b)
    
    // Stop after 4 retries
    b = retry.WithMaxRetries(4, b)
    
    // Ensure no single sleep exceeds 2s
    b = retry.WithCappedDuration(2 * time.Second, b)
    
    // Ensure total execution doesn't exceed 5s
    b = retry.WithMaxDuration(5 * time.Second, b)
  2. Use go-retry to implement retry logic

    main

    To perform a retry operation, use one of the provided backoff functions (like retry.Fibonacci, retry.Exponential, or retry.Constant) and pass a context and a function that returns an error. To signal that an error should trigger a retry, wrap it with retry.RetryableError(err). If the function returns a non-retryable error, the retry loop terminates immediately.

    package main
    
    import (
      "context"
      "database/sql"
      "log"
      "time"
    
      "github.com/sethvargo/go-retry"
    )
    
    func main() {
      db, err := sql.Open("mysql", "...")
      if err != nil {
        log.Fatal(err)
      }
    
      ctx := context.Background()
      if err := retry.Fibonacci(ctx, 1*time.Second, func(ctx context.Context) error {
        if err := db.PingContext(ctx); err != nil {
          // This marks the error as retryable
          return retry.RetryableError(err)
        }
        return nil
      }); err != nil {
        log.Fatal(err)
      }
    }
  3. Important notes on modifier ordering

    main

    The order in which you wrap your backoff with modifiers matters significantly:

    1. CappedDuration vs WithMaxDuration: Always add WithCappedDuration before WithMaxDuration. If added after, the total duration limit might trigger prematurely.
    2. Jitter placement: You can add Jitter before or after CappedDuration depending on whether you want the jitter to be subject to the cap or applied to the capped value.
  4. Built-in Backoff Algorithms

    main

    The library provides several base backoff algorithms. These algorithms, by default, never terminate and have no limits. They are used as the foundation for middleware composition.

    • Constant: Returns the same duration every time.
      • Usage: retry.NewConstant(duration)
    • Exponential: Doubles the duration with each step (e.g., 1s, 2s, 4s, 8s).
      • Usage: retry.NewExponential(duration)
    • Fibonacci: Uses the Fibonacci sequence (e.g., 1s, 1s, 2s, 3s, 5s). Ideal for network-type issues.
      • Usage: retry.NewFibonacci(duration)
    // Examples of base backoff creation
    b := retry.NewConstant(1 * time.Second)
    b := retry.NewExponential(1 * time.Second)
    b := retry.NewFibonacci(1 * time.Second)
  5. Mark an error as retryable with RetryableError

    main

    To instruct the retry mechanism to attempt the operation again, you must wrap the error using RetryableError(err). If an error is returned that is not wrapped with RetryableError, the retry loop will terminate immediately and return that error. This allows you to distinguish between transient errors that should be retried and permanent errors that should fail fast.

    err := retry.RetryableError(fmt.Errorf("temporary failure"))
  6. Create an exponential backoff with NewExponential()

    main

    Use NewExponential(base time.Duration) to create a Backoff instance that implements an exponential growth strategy.

    • Behavior: The delay doubles on each failure (1, 2, 4, 8, 16, 32, 64...).
    • Concurrency: The returned Backoff is safe for concurrent use.
    • Overflow: Once the duration overflows, it returns math.MaxInt64 and false (indicating no further valid increments).
    • Panic: The function will panic if the provided base is less than or equal to zero.
    backoff := retry.NewExponential(100 * time.Millisecond)
    // Use backoff with retry.Do()...
  7. Limit retries with WithMaxRetries

    main

    Use WithMaxRetries(max uint64, next Backoff) to wrap a backoff strategy so that it stops after a specific number of attempts. Once the max number of attempts is reached, Next() will return 0, true.

    // Example: Stop after 5 attempts
    // backoff := retry.WithMaxRetries(5, retry.Exponential(time.Second, 2.0))
  8. Execute a function with retries and return a value using DoValue

    main

    Use DoValue when the function being retried returns both a value and an error. It follows the same retry logic as Do: it will continue retrying as long as the error returned by the function is wrapped with RetryableError and the Backoff strategy allows more attempts. If the function succeeds, it returns the value and a nil error. If the backoff is exhausted or the context is canceled, it returns the zero value of the type and the error.

    type result struct {
        Data string
    }
    
    val, err := retry.DoValue(ctx, backoff, func(ctx context.Context) (result, error) {
        res, err := fetchData(ctx)
        if err != nil {
            return result{}, retry.RetryableError(err)
        }
        return res, nil
    })
  9. Use exponential backoff with Exponential()

    main

    The Exponential function is a convenience wrapper that executes a RetryFunc using an exponential backoff strategy. It starts with the provided base duration and doubles the delay on each subsequent failure (e.g., 1, 2, 4, 8...).

    It uses Do(ctx, NewExponential(base), f) internally. If the calculated delay overflows the 64-bit integer limit, it returns math.MaxInt64.

    err := retry.Exponential(ctx, 100*time.Millisecond, func(ctx context.Context) error {
        // Your retryable logic here
        return nil
    })
  10. Apply jitter to a backoff strategy

    main

    Jitter helps prevent 'thundering herd' problems by adding randomness to retry intervals. The library provides three ways to apply jitter:

    1. WithJitter(j time.Duration, next Backoff): Adds a fixed range of +/- j to the returned duration. The result is clamped to a minimum of 0.
    2. WithJitterPercent(j uint64, next Backoff): Adds a percentage-based jitter of +/- j%. For example, if j is 5 and the backoff returns 20s, the result will be between 19s and 21s.
    3. WithFullJitter(next Backoff): Returns a random value in the range [0, next_duration). This is highly effective for spreading out retrying clients across the entire interval.
    // Example of applying jitter
    // backoff := retry.WithFullJitter(retry.Constant(time.Second))
    // backoff := retry.WithJitter(5*time.Second, retry.Constant(20*time.Second))
  11. Create a constant backoff strategy with NewConstant()

    main

    The NewConstant function returns a Backoff implementation that always returns the same duration t for every retry attempt. This is useful when you want to pass a specific backoff strategy into the Do function or other retry orchestrators.

    Note: This function will panic if the provided duration t is less than or equal to zero.

    backoff := retry.NewConstant(500 * time.Millisecond)
    // Use the backoff with retry.Do(ctx, backoff, retryFunc)
  12. Use Fibonacci backoff with Fibonacci()

    main

    The Fibonacci function is a convenience wrapper that executes a RetryFunc using a Fibonacci backoff strategy. It takes a context.Context, a base duration (the starting value for the sequence), and the function to retry. The wait time follows the Fibonacci sequence (e.g., 1, 1, 2, 3, 5, 8, 13...) based on the provided base.

    err := retry.Fibonacci(ctx, 100*time.Millisecond, func(ctx context.Context) error {
        // Your retryable logic here
        return nil
    })