Current section
Files
Jump to
Current section
Files
lib/api_brasil/core/retry.ex
defmodule ApiBrasil.Core.Retry do
@moduledoc """
Política de retry da SDK.
Por padrão a SDK tenta novamente apenas em **HTTP 429** (rate limit) e em
**falhas de conexão** — nunca em timeouts nem em erros de negócio, para
não duplicar cobranças/envios.
ApiBrasil.new(
retry: %ApiBrasil.Core.Retry{
retries: 3,
min_delay: 500,
retry_on_statuses: [429, 503]
}
)
# ou desativando o retry
ApiBrasil.new(retry: ApiBrasil.Core.Retry.none())
Todos os tempos são em milissegundos.
"""
@type t :: %__MODULE__{
retries: non_neg_integer(),
min_delay: non_neg_integer(),
max_delay: non_neg_integer(),
retry_on_statuses: [pos_integer()]
}
@doc """
Política de retry.
- `:retries` — novas tentativas além da original. Padrão: `2`.
- `:min_delay` — atraso base do backoff exponencial, em ms. Padrão: `300`.
- `:max_delay` — teto do atraso entre tentativas, em ms. Padrão: `5_000`.
- `:retry_on_statuses` — status HTTP que disparam retry. Padrão: `[429]`.
"""
defstruct retries: 2,
min_delay: 300,
max_delay: 5_000,
retry_on_statuses: [429]
@doc "Cria a política a partir de uma keyword list ou de um mapa."
@spec new(t() | keyword() | map()) :: t()
def new(%__MODULE__{} = retry), do: retry
def new(fields) when is_list(fields) or is_map(fields), do: struct!(__MODULE__, fields)
@doc "Política que desativa o retry."
@spec none() :: t()
def none, do: %__MODULE__{retries: 0}
@doc "Número total de tentativas (a original mais os retries)."
@spec max_attempts(t()) :: pos_integer()
def max_attempts(%__MODULE__{retries: retries}), do: retries + 1
@doc "Informa se um status HTTP dispara retry nesta política."
@spec retries_status?(t(), pos_integer()) :: boolean()
def retries_status?(%__MODULE__{retry_on_statuses: statuses}, status),
do: status in statuses
@doc """
Calcula o backoff exponencial com jitter: `min_delay * 2^tentativa`,
limitado a `max_delay`. A tentativa é contada a partir de `0`.
"""
@spec backoff_delay(t(), non_neg_integer()) :: non_neg_integer()
def backoff_delay(%__MODULE__{} = retry, attempt) when attempt >= 0 do
exponential = retry.min_delay * :math.pow(2, attempt)
jittered = exponential * (0.5 + :rand.uniform() * 0.5)
jittered
|> round()
|> min(retry.max_delay)
|> max(0)
end
@doc "Aguarda o atraso informado. Zero não bloqueia."
@spec sleep(non_neg_integer()) :: :ok
def sleep(0), do: :ok
def sleep(delay) when is_integer(delay) and delay > 0, do: Process.sleep(delay)
end