-module(automata@retry).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/retry.gleam").
-export([no_retry/0, fixed/2, with_jitter/2, start/2, current_attempt/1, cumulative_delay/1, policy/1, exponential/3, capped_exponential/4, decide/2, should_retry/2, next_delay/2]).
-export_type([policy/0, context/0, decision/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-opaque policy() :: no_retry |
{fixed,
automata@retry@ast:duration(),
integer(),
automata@retry@ast:jitter()} |
{exponential,
automata@retry@ast:duration(),
integer(),
integer(),
automata@retry@ast:jitter()} |
{capped_exponential,
automata@retry@ast:duration(),
integer(),
automata@retry@ast:duration(),
integer(),
automata@retry@ast:jitter()}.
-opaque context() :: {context,
policy(),
integer(),
automata@retry@ast:duration(),
automata@retry@internal@prng:prng_state()}.
-type decision() :: {retry, automata@retry@ast:duration(), context()} |
{give_up, automata@retry@ast:give_up_reason()}.
-file("src/automata/retry.gleam", 66).
?DOC(
" Build a policy that never retries. Any failure (transient or\n"
" permanent) ends the sequence on the first call to `decide`.\n"
).
-spec no_retry() -> policy().
no_retry() ->
no_retry.
-file("src/automata/retry.gleam", 72).
?DOC(
" Build a fixed-delay policy: every retry waits exactly `delay`,\n"
" up to `max_attempts` total tries.\n"
).
-spec fixed(automata@retry@ast:duration(), integer()) -> {ok, policy()} |
{error, automata@retry@ast:retry_error()}.
fixed(Delay, Max_attempts) ->
case Max_attempts =< 0 of
true ->
{error, {max_attempts_must_be_positive, Max_attempts}};
false ->
{ok, {fixed, Delay, Max_attempts, no_jitter}}
end.
-file("src/automata/retry.gleam", 136).
?DOC(
" Decorate an existing policy with a jitter strategy.\n"
"\n"
" `no_retry` is unaffected (jitter has nothing to spread).\n"
).
-spec with_jitter(policy(), automata@retry@ast:jitter()) -> policy().
with_jitter(Policy, Jitter) ->
case Policy of
no_retry ->
no_retry;
{fixed, D, M, _} ->
{fixed, D, M, Jitter};
{exponential, I, Mu, M@1, _} ->
{exponential, I, Mu, M@1, Jitter};
{capped_exponential, I@1, Mu@1, C, M@2, _} ->
{capped_exponential, I@1, Mu@1, C, M@2, Jitter}
end.
-file("src/automata/retry.gleam", 165).
?DOC(
" Begin a new retry sequence.\n"
"\n"
" `seed` deterministically drives any jitter the policy applies. Use\n"
" the same seed twice and you get the same delay sequence, on the\n"
" BEAM and on the JavaScript target alike.\n"
).
-spec start(policy(), integer()) -> context().
start(Policy, Seed) ->
{context,
Policy,
0,
automata@retry@ast:unsafe_milliseconds(0),
automata@retry@internal@prng:new(Seed)}.
-file("src/automata/retry.gleam", 175).
?DOC(" Number of attempts already completed in this sequence.\n").
-spec current_attempt(context()) -> integer().
current_attempt(Ctx) ->
erlang:element(3, Ctx).
-file("src/automata/retry.gleam", 181).
?DOC(
" Total of the delays the policy has handed out so far. Useful for\n"
" applying an upper-bound deadline at the call site.\n"
).
-spec cumulative_delay(context()) -> automata@retry@ast:duration().
cumulative_delay(Ctx) ->
erlang:element(4, Ctx).
-file("src/automata/retry.gleam", 186).
?DOC(" Recover the policy a context was started from.\n").
-spec policy(context()) -> policy().
policy(Ctx) ->
erlang:element(2, Ctx).
-file("src/automata/retry.gleam", 244).
-spec validate_exponential_params(
automata@retry@ast:duration(),
integer(),
integer()
) -> {ok, nil} | {error, automata@retry@ast:retry_error()}.
validate_exponential_params(Initial, Multiplier, Max_attempts) ->
case automata@retry@ast:duration_milliseconds(Initial) =< 0 of
true ->
{error,
{initial_delay_must_be_positive,
automata@retry@ast:duration_milliseconds(Initial)}};
false ->
case Multiplier < 2 of
true ->
{error, {multiplier_must_be_at_least_two, Multiplier}};
false ->
case Max_attempts =< 0 of
true ->
{error,
{max_attempts_must_be_positive, Max_attempts}};
false ->
{ok, nil}
end
end
end.
-file("src/automata/retry.gleam", 86).
?DOC(
" Build an unbounded exponential backoff: the next delay is\n"
" `initial * multiplier ^ (attempt - 1)`, where `attempt` is the\n"
" upcoming try number (1-based).\n"
).
-spec exponential(automata@retry@ast:duration(), integer(), integer()) -> {ok,
policy()} |
{error, automata@retry@ast:retry_error()}.
exponential(Initial, Multiplier, Max_attempts) ->
case validate_exponential_params(Initial, Multiplier, Max_attempts) of
{error, Error} ->
{error, Error};
{ok, _} ->
{ok, {exponential, Initial, Multiplier, Max_attempts, no_jitter}}
end.
-file("src/automata/retry.gleam", 106).
?DOC(
" Build an exponential backoff that saturates at `cap`. Equivalent\n"
" to `exponential` with an extra ceiling applied to every computed\n"
" delay.\n"
).
-spec capped_exponential(
automata@retry@ast:duration(),
integer(),
automata@retry@ast:duration(),
integer()
) -> {ok, policy()} | {error, automata@retry@ast:retry_error()}.
capped_exponential(Initial, Multiplier, Cap, Max_attempts) ->
case validate_exponential_params(Initial, Multiplier, Max_attempts) of
{error, Error} ->
{error, Error};
{ok, _} ->
case automata@retry@ast:duration_milliseconds(Cap) < automata@retry@ast:duration_milliseconds(
Initial
) of
true ->
{error,
{cap_must_not_be_less_than_initial,
automata@retry@ast:duration_milliseconds(Cap),
automata@retry@ast:duration_milliseconds(Initial)}};
false ->
{ok,
{capped_exponential,
Initial,
Multiplier,
Cap,
Max_attempts,
no_jitter}}
end
end.
-file("src/automata/retry.gleam", 384).
-spec apply_jitter(
integer(),
automata@retry@ast:jitter(),
automata@retry@internal@prng:prng_state()
) -> {integer(), automata@retry@internal@prng:prng_state()}.
apply_jitter(Base_ms, Jitter, State) ->
case Jitter of
no_jitter ->
{_, Next} = automata@retry@internal@prng:next_u32(State),
{Base_ms, Next};
full_jitter ->
automata@retry@internal@prng:bounded(State, Base_ms);
equal_jitter ->
Half = Base_ms div 2,
{Extra, Next@1} = automata@retry@internal@prng:bounded(State, Half),
{Half + Extra, Next@1}
end.
-file("src/automata/retry.gleam", 403).
-spec finish_decision(
context(),
integer(),
integer(),
automata@retry@internal@prng:prng_state()
) -> decision().
finish_decision(Ctx, Final_ms, Next_attempt, Next_prng) ->
Prev_cum = automata@retry@ast:duration_milliseconds(erlang:element(4, Ctx)),
case Final_ms > (9007199254740991 - Prev_cum) of
true ->
{give_up, {delay_overflow, Next_attempt}};
false ->
New_cum = automata@retry@ast:unsafe_milliseconds(
Prev_cum + Final_ms
),
{retry,
automata@retry@ast:unsafe_milliseconds(Final_ms),
{context,
erlang:element(2, Ctx),
Next_attempt,
New_cum,
Next_prng}}
end.
-file("src/automata/retry.gleam", 268).
-spec decide_fixed(
context(),
automata@retry@ast:duration(),
integer(),
automata@retry@ast:jitter(),
integer()
) -> decision().
decide_fixed(Ctx, Delay, Max_attempts, Jitter, Next_attempt) ->
case Next_attempt >= Max_attempts of
true ->
{give_up, {max_attempts_reached, Next_attempt, Max_attempts}};
false ->
Base_ms = automata@retry@ast:duration_milliseconds(Delay),
{Final_ms, Next_prng} = apply_jitter(
Base_ms,
Jitter,
erlang:element(5, Ctx)
),
finish_decision(Ctx, Final_ms, Next_attempt, Next_prng)
end.
-file("src/automata/retry.gleam", 362).
-spec advance_or_overflow(
integer(),
integer(),
integer(),
gleam@option:option(integer())
) -> {ok, integer()} | {error, nil}.
advance_or_overflow(Current, Multiplier, Remaining_steps, Cap_ms) ->
case Current > (case Multiplier of
0 -> 0;
Gleam@denominator -> 9007199254740991 div Gleam@denominator
end) of
true ->
case Cap_ms of
{some, C} ->
{ok, C};
none ->
{error, nil}
end;
false ->
exponential_step(
Current * Multiplier,
Multiplier,
Remaining_steps - 1,
Cap_ms
)
end.
-file("src/automata/retry.gleam", 332).
-spec exponential_step(
integer(),
integer(),
integer(),
gleam@option:option(integer())
) -> {ok, integer()} | {error, nil}.
exponential_step(Current, Multiplier, Remaining_steps, Cap_ms) ->
case Remaining_steps =< 0 of
true ->
case Cap_ms of
{some, C} ->
case Current > C of
true ->
{ok, C};
false ->
{ok, Current}
end;
none ->
{ok, Current}
end;
false ->
case Cap_ms of
{some, C@1} ->
case Current >= C@1 of
true ->
{ok, C@1};
false ->
advance_or_overflow(
Current,
Multiplier,
Remaining_steps,
Cap_ms
)
end;
none ->
advance_or_overflow(
Current,
Multiplier,
Remaining_steps,
Cap_ms
)
end
end.
-file("src/automata/retry.gleam", 323).
-spec compute_exponential_base(
integer(),
integer(),
integer(),
gleam@option:option(integer())
) -> {ok, integer()} | {error, nil}.
compute_exponential_base(Initial_ms, Multiplier, Next_attempt, Cap_ms) ->
exponential_step(Initial_ms, Multiplier, Next_attempt - 1, Cap_ms).
-file("src/automata/retry.gleam", 289).
-spec decide_exponential(
context(),
automata@retry@ast:duration(),
integer(),
gleam@option:option(automata@retry@ast:duration()),
integer(),
automata@retry@ast:jitter(),
integer()
) -> decision().
decide_exponential(
Ctx,
Initial,
Multiplier,
Cap,
Max_attempts,
Jitter,
Next_attempt
) ->
case Next_attempt >= Max_attempts of
true ->
{give_up, {max_attempts_reached, Next_attempt, Max_attempts}};
false ->
Initial_ms = automata@retry@ast:duration_milliseconds(Initial),
Cap_ms = case Cap of
{some, C} ->
{some, automata@retry@ast:duration_milliseconds(C)};
none ->
none
end,
case compute_exponential_base(
Initial_ms,
Multiplier,
Next_attempt,
Cap_ms
) of
{error, nil} ->
{give_up, {delay_overflow, Next_attempt}};
{ok, Base_ms} ->
{Final_ms, Next_prng} = apply_jitter(
Base_ms,
Jitter,
erlang:element(5, Ctx)
),
finish_decision(Ctx, Final_ms, Next_attempt, Next_prng)
end
end.
-file("src/automata/retry.gleam", 195).
?DOC(
" Decide what to do after a failed attempt.\n"
"\n"
" `Permanent` short-circuits regardless of the policy. `Transient`\n"
" consults the policy: it may yield a retry delay, the context to\n"
" use next, or a structured reason to stop.\n"
).
-spec decide(context(), automata@retry@ast:failure_kind()) -> decision().
decide(Ctx, Failure) ->
Next_attempt = erlang:element(3, Ctx) + 1,
case Failure of
permanent ->
{give_up, {permanent_failure_signaled, Next_attempt}};
transient ->
case erlang:element(2, Ctx) of
no_retry ->
{give_up, policy_disallows_retry};
{fixed, D, M, J} ->
decide_fixed(Ctx, D, M, J, Next_attempt);
{exponential, I, Mu, M@1, J@1} ->
decide_exponential(Ctx, I, Mu, none, M@1, J@1, Next_attempt);
{capped_exponential, I@1, Mu@1, C, M@2, J@2} ->
decide_exponential(
Ctx,
I@1,
Mu@1,
{some, C},
M@2,
J@2,
Next_attempt
)
end
end.
-file("src/automata/retry.gleam", 222).
?DOC(
" Convenience predicate: `True` when `decide` would return `Retry`.\n"
"\n"
" Does not advance the context — call `decide` if you intend to act\n"
" on the answer.\n"
).
-spec should_retry(context(), automata@retry@ast:failure_kind()) -> boolean().
should_retry(Ctx, Failure) ->
case decide(Ctx, Failure) of
{retry, _, _} ->
true;
{give_up, _} ->
false
end.
-file("src/automata/retry.gleam", 234).
?DOC(
" Convenience accessor: the delay component of `decide`, or the\n"
" give-up reason as `Error`.\n"
"\n"
" Does not advance the context — call `decide` if you intend to act\n"
" on the answer.\n"
).
-spec next_delay(context(), automata@retry@ast:failure_kind()) -> {ok,
automata@retry@ast:duration()} |
{error, automata@retry@ast:give_up_reason()}.
next_delay(Ctx, Failure) ->
case decide(Ctx, Failure) of
{retry, D, _} ->
{ok, D};
{give_up, R} ->
{error, R}
end.