retry-go

repository·main·Indexed 25 days ago

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

A simple Go library for retrying operations that might fail, such as HTTP requests. It provides a flexible mechanism to configure retry attempts, delay strategies (including exponential backoff and jitter), and custom retry predicates. Version 5.0.0 introduces a method-based API using Retrier and RetrierWithData for improved performance and support for Go 1.20 multiple error wrapping.

Tokens
3.2K
Snippets
8
Records
19
Agent score
84%

What's inside retry-go

  1. Handle and inspect retry errors

    main

    When a retry operation fails, it returns an Error type, which is a slice of all errors encountered during the attempts. This type implements the Go 1.20 Unwrap() []error interface.

    To inspect errors:

    • Recommended: Use standard library errors.Is(err, target) or errors.As(err, &target) to check all wrapped errors.
    • Get the last error: If you specifically need the final error that caused the failure, cast the error to retry.Error and call .LastError().
    • Unrecoverable errors: Wrap an error with retry.Unrecoverable(err) to stop retries immediately.
    // Use errors.Is to check for specific errors
    err := retry.New(retry.Attempts(3)).Do(func() error {
    	return os.ErrNotExist
    })
    if errors.Is(err, os.ErrNotExist) {
    	// Handle not exist error
    }
    
    // Use errors.As to extract error details
    var pathErr *fs.PathError
    if errors.As(err, &pathErr) {
    	fmt.Println("Failed at path:", pathErr.Path)
    }
    
    // Get the last error directly (migration path from v4)
    if retryErr, ok := err.(retry.Error); ok {
    	lastErr := retryErr.LastError()
    }
  2. Migrate from v4 to v5

    main

    Version 5.0.0 introduced a complete API redesign. Key changes include:

    • Pattern Change: Shifted from package-level functions to method-based operations.
      • Old: retry.Do(func, opts...)
      • New: retry.New(opts...).Do(func)
    • Type Renaming: Config is now Retrier, and NewConfig() is now New().
    • Error Handling: Unwrap() now returns []error (supporting Go 1.20 multiple error wrapping). Use errors.Is or errors.As to inspect wrapped errors.
    • Custom Delay Functions: The DelayTypeFunc signature changed from func(n uint, err error, config *Config) to func(n uint, err error, r *Retrier).
  3. Configure retry behavior with Options

    main

    Use retry.New to create a Retrier (for functions returning only error) or retry.NewWithData[T] (for functions returning (T, error)). You can pass several Option functions to customize the retry logic.

    Common options include:

    • Attempts(uint): Set the number of retries. 0 retries until success. Default is 10.
    • UntilSucceeded(): Alias for Attempts(0).
    • Delay(time.Duration): Set the base delay between retries. Default is 100ms.
    • MaxDelay(time.Duration): Set the maximum delay allowed.
    • MaxJitter(time.Duration): Set the maximum random jitter for RandomDelay.
    • Context(context.Context): Set the context for the retry operation.
    • OnRetry(OnRetryFunc): A callback executed on every retry attempt.
    • RetryIf(RetryIfFunc): A predicate to decide if a retry should be attempted based on the error.
    • LastErrorOnly(bool): If true, returns only the direct last error instead of the wrapped error list. Default is false.
    • WithTimer(Timer): Provide a custom timer implementation (useful for testing).
    // Example: Retry with 3 attempts and a custom callback
    retry.New(
    	retry.Attempts(3),
    	retry.OnRetry(func(n uint, err error) {
    		log.Printf("#%d: %s\n", n, err)
    	}),
    ).Do(func() error {
    	return errors.New("some error")
    })
  4. Perform retries with retry.New().Do()

    main

    Use retry.New() to create a retrier with specific options, then call .Do() to execute a function that returns an error. This is the recommended pattern for v5.0.0+ and allows for reusing the retrier instance to minimize allocations in high-frequency operations.

    url := "http://example.com"
    var body []byte
    
    err := retry.New(
    	retry.Attempts(5),
    	retry.Delay(100*time.Millisecond),
    ).Do(
    	func() error {
    		resp, err := http.Get(url)
    		if err != nil {
    			return err
    		}
    		defer resp.Body.Close()
    		body, err = ioutil.ReadAll(resp.Body)
    		if err != nil {
    			return err
    		}
    		return nil
    	},
    )
    
    if err != nil {
    	// handle error
    }
    
    fmt.Println(string(body))
  5. Perform retries with data using retry.DoWithData()

    main

    Use retry.DoWithData() when your retryable function needs to return both a result and an error. This function takes a Retrier (created via retry.New()) and a function with the signature func() (T, error).

    url := "http://example.com"
    
    body, err := retry.DoWithData(retry.New(),
    	func() ([]byte, error) {
    		resp, err := http.Get(url)
    		if err != nil {
    			return nil, err
    		}
    		defer resp.Body.Close()
    		body, err := ioutil.ReadAll(resp.Body)
    		if err != nil {
    			return nil, err
    		}
    
    		return body, nil
    	},
    )
    
    if err != nil {
    	// handle error
    }
    
    fmt.Println(string(body))
  6. Reuse a retrier for high-frequency operations

    main

    To minimize allocations in performance-critical loops, create a single Retrier instance using retry.New() and reuse it for multiple calls to .Do().

    // Create retrier once, reuse many times
    retrier := retry.New(
    	retry.Attempts(5),
    	retry.Delay(100*time.Millisecond),
    )
    
    // Minimal allocations in happy path
    for {
    	err := retrier.Do(
    	func() error {
    		return doWork()
    	},
    )
    	if err != nil {
    		// handle error
    	}
    }
  7. Configure delay strategies with DelayType

    main

    You can customize the delay between retries using the DelayType(DelayTypeFunc) option. A DelayTypeFunc calculates the next delay based on the attempt number, the error, and a DelayContext.

    Available delay functions:

    • FixedDelay: Keeps the delay constant across all iterations.
    • BackOffDelay: Increases delay between consecutive retries.
    • FullJitterBackoffDelay: Calculates delay using exponential backoff with full jitter. The delay is a random value between 0 and min(cap, base * 2^attempt), where base is config.Delay and cap is config.MaxDelay.
    • RandomDelay: Picks a random delay up to config.MaxJitter.
    • CombineDelay(...DelayTypeFunc): Combines multiple delay strategies into one.
  8. Execute retries with data using RetrierWithData

    main
    If your retryable function returns both a value and an error, use retry.NewWithData[T] to create a RetrierWithData[T]. The .Do() method will then return the successfully retrieved data or the final error.
  9. Stop retries using Unrecoverable errors

    main
    You can force a retry loop to stop immediately by wrapping the error in retry.Unrecoverable(err). This is more concise than using RetryIf to check for specific error types.
  10. Handle multiple errors with the `retry.Error` type

    main

    When all retry attempts fail, the library returns a retry.Error type, which is a slice of all errors encountered ([]error).

    Because it implements the Go 1.20 Unwrap() []error interface, you should use errors.Is or errors.As to inspect the errors. These functions will traverse the entire list of errors.

    If you specifically need the very last error encountered, use the .LastError() method.

  11. Execute functions that return data using `RetrierWithData.Do`

    main

    If your function returns both a value and an error, use retry.DoWithData. This returns the successful value and any error encountered during the retry process.

    Note: retry.DoWithData is used to create a RetrierWithData[T] instance which provides the generic .Do() method.

    body, err := retry.DoWithData(retry.New(),
    	func() ([]byte, error) {
    		// your logic that returns (data, error)
    		return []byte("data"), nil
    	},
    )
    
    if err != nil {
    	// handle error
    }
    fmt.Println(string(body))
  12. Configure Context and Error Wrapping

    main

    Manage lifecycle and error reporting:

    • Context(ctx context.Context): Sets the context for the retry operation. If the context is cancelled, retries stop.
    • WrapContextErrorWithLastError(bool): If true, when retrying indefinitely (Attempts(0)) and the context is cancelled, the returned error will wrap the last error encountered from the retried function instead of just returning the context error.
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    retry.New(
        retry.Context(ctx),
        retry.Attempts(0),
        retry.WrapContextErrorWithLastError(true),
    ).Do(func() error {
        return errors.New("last error")
    })