ExternalService.RetryOptions (ExternalService v3.0.0-rc.1)

Copy Markdown View Source

Options that control retry logic for calls to external services.

Retry options can be given either as this struct or as a plain keyword list (which is validated and converted with new/1). The available options are:

  • :backoff - The backoff strategy used to grow the delay between retries. The default value is :exponential.

  • :base (non_neg_integer/0) - The initial delay between retries, in milliseconds (0 for no delay). The default value is 10.

  • :factor (pos_integer/0) - Growth factor applied on each retry. Only used for :linear backoff. The default value is 1.

  • :cap (pos_integer/0) - Caps the delay between retries to at most this many milliseconds.

  • :expiry - Time budget for the retrying, in milliseconds. Delays that fit are used as-is; the delay that would overshoot the budget is trimmed instead, so the last attempt starts exactly at the deadline rather than past it. Defaults to no time budget; :infinity states that explicitly (see the note on unbounded retries below).

  • :max_attempts - Maximum number of attempts, counting the initial attempt — so the default of 5 is one try plus four retries. Use :infinity to retry without a count bound (see the note on unbounded retries below). The default value is 5.

  • :jitter - Random jitter applied to delays. true applies +/- 10%; a float (e.g. 0.25) applies that proportion. Helps avoid retrying in lockstep (thundering herd). The default value is false.

  • :retry_on (function of arity 1) - A predicate run on the return value of the call. When it returns a truthy value the call is retried, exactly as if the function had returned :retry (the result itself is used as the retry reason, and the circuit breaker melts). Lets you drive retries from a function that was not written to return :retry/{:retry, reason}. Defaults to no predicate. An explicit :retry/{:retry, reason} return always takes precedence over the predicate. A predicate that fails — raising, throwing, or exiting rather than answering — is treated as no match, leaving the result untouched, and logs a warning.

  • :retry_exceptions - Which raised exceptions should trigger a retry, as either a list of exception modules or a predicate run on the exception itself. A predicate can decide per instance rather than per type — useful when the same exception type is sometimes transient and sometimes not. Defaults to [], meaning raised exceptions are not retried; use :retry/{:retry, reason} return values, or the :retry_on predicate, to drive retries instead. An exception that is not matched also does not melt the circuit breaker, and once retries are spent the original exception is re-raised with its original stacktrace. A predicate that fails — raising, throwing, or exiting rather than answering — is treated as no match, so the exception it was asked to classify reaches the caller unchanged, and a warning is logged. The default value is [].

Bounds

:max_attempts defaults to 5, so retrying always stops on its own. With the default :base of 10 and exponential backoff that is a bound of four retries across roughly 150ms of waiting — deliberately a safety net rather than a tuned policy. If your dependency needs a longer retry window, raise :base (100 is the usual choice for an HTTP service) rather than the attempt count.

Note that this bound and the circuit breaker's :tolerate interact: every failing attempt melts the breaker, so five attempts melt five of the ten a default breaker tolerates, and two fully-failing calls open it.

:expiry adds a time budget alongside the count; whichever is reached first stops the retrying. See Bounding retries.

Unbounded retries

Retrying without a count bound is available, but it has to be asked for:

ExternalService.start(:my_service, retry: [max_attempts: :infinity])

Be deliberate about it. A call that keeps returning :retry then keeps retrying forever, and the circuit breaker is not a reliable backstop — exponential backoff widens the gap between attempts until failures no longer accumulate fast enough to open it. Pair it with an :expiry, or reserve it for work that genuinely has nowhere else to go.

Summary

Functions

Layers a keyword list of per-call overrides onto a base struct.

Builds a validated RetryOptions struct from a keyword list (or returns an existing struct unchanged).

Types

t()

@type t() :: %ExternalService.RetryOptions{
  backoff: :exponential | :linear,
  base: non_neg_integer(),
  cap: pos_integer() | nil,
  expiry: pos_integer() | :infinity | nil,
  factor: pos_integer(),
  jitter: boolean() | float(),
  max_attempts: pos_integer() | :infinity | nil,
  retry_exceptions: [module()] | (Exception.t() -> as_boolean(term())),
  retry_on: (term() -> as_boolean(term())) | nil
}

Functions

merge(base, retry_options)

@spec merge(t(), t() | keyword()) :: t()

Layers a keyword list of per-call overrides onto a base struct.

Only the keys actually present in opts are overridden; every other field is taken from base. This is how per-call retry options tweak — rather than reset — a service's configured defaults. A %RetryOptions{} given in place of the keyword list replaces base wholesale, since a struct is already a complete set of options.

Raises NimbleOptions.ValidationError if opts is invalid.

new(retry_options)

@spec new(t() | keyword()) :: t()

Builds a validated RetryOptions struct from a keyword list (or returns an existing struct unchanged).

Raises NimbleOptions.ValidationError if the options are invalid.