A small, dependency-free retry-with-backoff helper.
Moov.Client uses this to automatically retry requests that fail with a
transient error: 429 (rate limited), 5xx (Moov degraded), or a
transport-level network error. Non-transient failures (400, 401,
404, 409, 422, decode errors, ...) are never retried, since retrying
a request that was rejected for being invalid will just fail again.
Delay between attempts is computed as exponential backoff with full
jitter, capped at :max_delay_ms - except when Moov's response body or a
standard Retry-After header tells us exactly how long to wait
(Moov.Error.retry_after_ms/0), in which case that takes precedence.
Summary
Functions
Computes the delay (in milliseconds) before the next attempt.
Whether the given Moov.Error represents a transient failure worth
retrying.
Calls fun and, if it returns {:error, error} for a retryable error,
calls it again (up to :max_retries additional times) with an
exponentially increasing delay between attempts.
Types
@type result() :: {:ok, term()} | {:error, Moov.Error.t()}
Functions
@spec delay_ms(Moov.Error.t(), pos_integer(), pos_integer(), pos_integer()) :: non_neg_integer()
Computes the delay (in milliseconds) before the next attempt.
Prefers an explicit retry_after_ms from the error (Moov told us exactly
how long to wait) and otherwise falls back to exponential backoff with
full jitter: a random value between 0 and
min(base_delay_ms * 2^(attempt - 1), max_delay_ms).
@spec retryable?(Moov.Error.t()) :: boolean()
Whether the given Moov.Error represents a transient failure worth
retrying.
Calls fun and, if it returns {:error, error} for a retryable error,
calls it again (up to :max_retries additional times) with an
exponentially increasing delay between attempts.
Options
:max_retries- maximum number of additional attempts after the first one fails (default:3):base_delay_ms- base delay before the first retry (default:250):max_delay_ms- upper bound for the computed delay (default:8_000):sleep_fun- function called with the computed delay in milliseconds instead ofProcess.sleep/1; override this in tests so retry tests don't actually take seconds to run:on_retry- a 3-arity function called with(attempt_number, delay_ms, error)before each retry, useful for logging or telemetry
Examples
Moov.Retry.run(fn -> do_request() end, max_retries: 3)