Dowser. Client. Retry
(Dowser.Client v0.3.0)
View Source
Retry policy for transient HTTP failures.
Lives outside Dowser.Client.HTTP deliberately: retry is orchestration
around the transport, not a transport concern.
resolve/2 turns opts[:retry] into a full config (stored on
%Dowser.Client.Request{}); run/2 drives an arbitrary zero-arity fun
through that policy, sleeping between attempts; transient?/1 classifies
the transport's raw {:error, reason} payload.
What may be retried
A retry is only safe when the request cannot already have been applied. Three cases, and only the third needs a judgement call:
- Never reached the server —
:econnrefused,:nxdomain,{:failed_connect, _},:enetunreach,:ehostunreach. Nothing ran. Always retried. - The server rejected it — a
:retryable_statusesresponse,429and503by default. The server said it did not do the work. Always retried. - Ambiguous — a timeout, a connection dropped mid-flight, or an
:ambiguous_statusesresponse (502/504, where a proxy answers for a server that may well have applied the request). The request may have taken effect, with no answer to say so. Retried only when the request is idempotent.
That last case is what makes a retried POST duplicate work: a bulk index
or an auto-id create that timed out may have been applied in full, and
sending it again writes it twice. So :idempotent defaults to the method —
GET, HEAD, PUT, DELETE, OPTIONS and TRACE are idempotent, POST
and PATCH are not — and a caller who knows better says so per request:
# a search is a POST, but it writes nothing
Dowser.Client.post("/posts/_search", query, retry: [idempotent: true])Options
opts[:retry] accepts:
false— disables retries (max_attempts: 1).- a keyword list — merged over the defaults, so a partial override (e.g.
retry: [max_attempts: 5]) only changes the given keys::max_attempts— total attempts, including the first (default3).:max_elapsed_ms— give up once this much wall-clock time has gone into the request, however many attempts are left (defaultnil, no budget). A burst of429s outlasts three attempts; a budget bounds what the caller waits for.:base_delay_ms/:max_delay_ms— exponential full-jitter backoff bounds (default200/2_000).:retryable_statuses— statuses the server rejected the request with (default[429, 503]).:ambiguous_statuses— statuses that may or may not mean the request was applied (default[502, 504]), retried only when the request is idempotent.:idempotent— whether re-sending this request is harmless (default: derived from the HTTP method).:respect_retry_after— obey aRetry-Afterresponse header, given in seconds or as an HTTP date, in place of the computed backoff (defaulttrue).:max_retry_after_ms— cap on aRetry-Afterthe server asks for (default60_000). Past the cap the request gives up rather than sleeping as asked.
Summary
Functions
Whether a failure leaves it unknown whether the request was applied — false
for one that demonstrably never reached the server.
Resolves opts[:retry] into a full retry config, with :idempotent
defaulted from method unless the caller set it.
Runs fun up to config[:max_attempts] times, retrying as long as the
result may safely be retried (see the module documentation) and both the
attempts and the time budget last, sleeping in between.
Whether an adapter's raw {:error, reason} payload looks transient — worth
retrying at all, idempotence aside.
Types
@type config() :: keyword()
Functions
Whether a failure leaves it unknown whether the request was applied — false
for one that demonstrably never reached the server.
A non-idempotent request is not retried when this is true: that is what
keeps a timed-out write from being applied twice.
@spec resolve(keyword(), atom() | nil) :: {:ok, config()} | {:error, Dowser.Client.Error.t()}
Resolves opts[:retry] into a full retry config, with :idempotent
defaulted from method unless the caller set it.
Returns {:ok, config} or {:error, %Dowser.Client.Error{}} when
opts[:retry] is neither false nor a keyword list.
@spec run(config(), (-> term())) :: {term(), pos_integer()}
Runs fun up to config[:max_attempts] times, retrying as long as the
result may safely be retried (see the module documentation) and both the
attempts and the time budget last, sleeping in between.
Returns {result, attempts} — fun's final return value and the number
of times it was actually called.
Whether an adapter's raw {:error, reason} payload looks transient — worth
retrying at all, idempotence aside.