# `Fivetrex.Retry`
[🔗](https://github.com/lostbean/fivetrex/blob/v0.2.3/lib/fivetrex/retry.ex#L1)

Retry utilities with exponential backoff for handling transient failures.

This module provides retry logic for Fivetran API calls that may fail due to
rate limiting, temporary server errors, or network issues. It implements
exponential backoff with optional jitter to prevent thundering herd problems.

## Quick Start

    # Retry with defaults (3 attempts, exponential backoff)
    {:ok, groups} = Fivetrex.Retry.with_backoff(fn ->
      Fivetrex.Groups.list(client)
    end)

    # Custom retry configuration
    {:ok, connector} = Fivetrex.Retry.with_backoff(
      fn -> Fivetrex.Connectors.get(client, connector_id) end,
      max_attempts: 5,
      base_delay_ms: 500,
      max_delay_ms: 30_000
    )

## How It Works

1. Executes the provided function
2. If successful, returns the result immediately
3. If it fails with a retryable error, waits with exponential backoff
4. Repeats until success or max attempts reached

## Retryable Errors

By default, these error types are retried:
  * `:rate_limited` - Respects `retry_after` header when available
  * `:server_error` - 5xx errors are typically transient

Non-retryable errors (returned immediately):
  * `:unauthorized` - Invalid credentials won't become valid
  * `:not_found` - Resource doesn't exist
  * `:unknown` - Unexpected errors need investigation

## Exponential Backoff

Delays increase exponentially: `base_delay * 2^attempt`

With default settings (base_delay: 1000ms):
  * Attempt 1 fails → wait ~1 second
  * Attempt 2 fails → wait ~2 seconds
  * Attempt 3 fails → wait ~4 seconds
  * (capped at max_delay)

## Jitter

Optional random jitter prevents synchronized retries when multiple clients
hit rate limits simultaneously:

    Fivetrex.Retry.with_backoff(func, jitter: true)

## Examples

### Basic Usage

    case Fivetrex.Retry.with_backoff(fn -> Fivetrex.Groups.list(client) end) do
      {:ok, %{items: groups}} ->
        process_groups(groups)

      {:error, error} ->
        # All retries exhausted
        Logger.error("Failed after retries: #{error.message}")
    end

### With Rate Limit Handling

    # Respects Fivetran's retry-after header automatically
    {:ok, _} = Fivetrex.Retry.with_backoff(fn ->
      Fivetrex.Connectors.sync(client, connector_id)
    end)

### Custom Retry Predicate

    # Only retry on specific errors
    Fivetrex.Retry.with_backoff(
      fn -> Fivetrex.Connectors.get(client, id) end,
      retry_if: fn
        %Fivetrex.Error{type: :rate_limited} -> true
        _ -> false
      end
    )

### Fire and Forget with Logging

    Fivetrex.Retry.with_backoff(
      fn -> Fivetrex.Connectors.sync(client, connector_id) end,
      on_retry: fn error, attempt, delay ->
        Logger.warn("Retry #{attempt}: #{error.message}, waiting #{delay}ms")
      end
    )

# `retry_opts`

```elixir
@type retry_opts() :: [
  max_attempts: pos_integer(),
  base_delay_ms: pos_integer(),
  max_delay_ms: pos_integer(),
  jitter: boolean(),
  retry_if: (Fivetrex.Error.t() -&gt; boolean()),
  on_retry: (Fivetrex.Error.t(), pos_integer(), pos_integer() -&gt; any())
]
```

Options for configuring retry behavior.

  * `:max_attempts` - Maximum number of attempts (default: 3)
  * `:base_delay_ms` - Initial delay in milliseconds (default: 1000)
  * `:max_delay_ms` - Maximum delay cap in milliseconds (default: 30000)
  * `:jitter` - Add random jitter to delays (default: false)
  * `:retry_if` - Custom function to determine if error is retryable
  * `:on_retry` - Callback function called before each retry

# `calculate_delay`

```elixir
@spec calculate_delay(
  Fivetrex.Error.t(),
  pos_integer(),
  pos_integer(),
  pos_integer(),
  boolean()
) ::
  pos_integer()
```

Calculates the delay before the next retry attempt.

For rate-limited errors with a `retry_after` value, uses that directly.
Otherwise, uses exponential backoff: `base_delay * 2^(attempt-1)`

## Parameters

  * `error` - The error that triggered the retry
  * `attempt` - The current attempt number (1-based)
  * `base_delay_ms` - Base delay in milliseconds
  * `max_delay_ms` - Maximum delay cap
  * `jitter` - Whether to add random jitter

## Examples

    iex> error = %Fivetrex.Error{type: :server_error, retry_after: nil}
    iex> Fivetrex.Retry.calculate_delay(error, 1, 1000, 30000, false)
    1000

    iex> error = %Fivetrex.Error{type: :server_error, retry_after: nil}
    iex> Fivetrex.Retry.calculate_delay(error, 3, 1000, 30000, false)
    4000

    iex> error = %Fivetrex.Error{type: :rate_limited, retry_after: 60}
    iex> Fivetrex.Retry.calculate_delay(error, 1, 1000, 30000, false)
    60000

# `default_retry_predicate`

```elixir
@spec default_retry_predicate(Fivetrex.Error.t()) :: boolean()
```

The default retry predicate - determines which errors are retryable.

Returns `true` for:
  * `:rate_limited` - API rate limits are transient
  * `:server_error` - 5xx errors are typically transient

Returns `false` for:
  * `:unauthorized` - Invalid credentials
  * `:not_found` - Resource doesn't exist
  * `:unknown` - Unexpected errors

## Examples

    iex> Fivetrex.Retry.default_retry_predicate(%Fivetrex.Error{type: :rate_limited})
    true

    iex> Fivetrex.Retry.default_retry_predicate(%Fivetrex.Error{type: :not_found})
    false

# `with_backoff`

```elixir
@spec with_backoff((-&gt; {:ok, any()} | {:error, Fivetrex.Error.t()}), retry_opts()) ::
  {:ok, any()} | {:error, Fivetrex.Error.t()}
```

Executes a function with automatic retry and exponential backoff.

## Parameters

  * `func` - A zero-arity function that returns `{:ok, result}` or `{:error, %Fivetrex.Error{}}`
  * `opts` - Optional keyword list (see module docs for options)

## Returns

  * `{:ok, result}` - The successful result from `func`
  * `{:error, %Fivetrex.Error{}}` - The last error after all retries exhausted

## Examples

    # Simple usage
    {:ok, groups} = Fivetrex.Retry.with_backoff(fn ->
      Fivetrex.Groups.list(client)
    end)

    # With options
    {:ok, connector} = Fivetrex.Retry.with_backoff(
      fn -> Fivetrex.Connectors.get(client, id) end,
      max_attempts: 5,
      jitter: true
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
