Packages

A complete, production-grade Elixir client for the Mercury Banking API (accounts, transactions, recipients, invoices, payments, treasury, webhooks, and more), with typed errors, retry with backoff, and lazy auto-paginating streams.

Current section

Files

Jump to
mercury_client lib mercury retry.ex
Raw

lib/mercury/retry.ex

defmodule Mercury.Retry do
@moduledoc """
Controls retry behaviour for transient failures (rate limiting and 5xx
server errors). Retries use exponential backoff with jitter, and honour
the `Retry-After` header when Mercury returns one on a 429.
Pass a custom policy via `Mercury.Client.new/2`'s `:retry` option:
Mercury.Client.new(api_key, retry: %Mercury.Retry{max_retries: 5})
Set `max_retries: 0` to disable retries entirely.
"""
@type t :: %__MODULE__{
max_retries: non_neg_integer(),
initial_delay: non_neg_integer(),
max_delay: non_neg_integer(),
jitter_factor: float()
}
defstruct max_retries: 3,
initial_delay: 500,
max_delay: 30_000,
jitter_factor: 0.25
@doc "The default retry policy: 3 retries, 500ms base delay, 30s cap, ±25% jitter."
@spec default() :: t()
def default, do: %__MODULE__{}
@doc false
@spec run(config :: t(), fun :: (-> {:ok, any()} | {:error, Exception.t()})) ::
{:ok, any()} | {:error, Exception.t()}
def run(%__MODULE__{} = config, fun) when is_function(fun, 0) do
attempt(config, fun, 0)
end
defp attempt(config, fun, attempt_number) do
case fun.() do
{:ok, _result} = ok ->
ok
{:error, error} = error_tuple ->
if attempt_number < config.max_retries and retryable?(error) do
sleep(config, error, attempt_number + 1)
attempt(config, fun, attempt_number + 1)
else
error_tuple
end
end
end
@doc false
@spec retryable?(error :: Exception.t()) :: boolean()
def retryable?(%Mercury.RateLimitError{}), do: true
def retryable?(%Mercury.ServerError{status: status}) when is_integer(status), do: status >= 500
def retryable?(_other), do: false
defp sleep(_config, %Mercury.RateLimitError{retry_after: retry_after}, _attempt)
when is_integer(retry_after) and retry_after > 0 do
Process.sleep(retry_after * 1_000)
end
defp sleep(config, _error, attempt_number) do
base = config.initial_delay * :math.pow(2, attempt_number - 1)
jitter = base * config.jitter_factor * (:rand.uniform() * 2 - 1)
delay =
(base + jitter)
|> min(config.max_delay)
|> max(0)
|> round()
Process.sleep(delay)
end
end