Fivetrex.Retry (Fivetrex v0.2.1)

View Source

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
)

Summary

Types

Options for configuring retry behavior.

Functions

Calculates the delay before the next retry attempt.

The default retry predicate - determines which errors are retryable.

Executes a function with automatic retry and exponential backoff.

Types

retry_opts()

@type retry_opts() :: [
  max_attempts: pos_integer(),
  base_delay_ms: pos_integer(),
  max_delay_ms: pos_integer(),
  jitter: boolean(),
  retry_if: (Fivetrex.Error.t() -> boolean()),
  on_retry: (Fivetrex.Error.t(), pos_integer(), pos_integer() -> 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

Functions

calculate_delay(error, attempt, base_delay_ms, max_delay_ms, jitter)

@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(error)

@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(func, opts \\ [])

@spec with_backoff((-> {: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
)