Current section

Files

Jump to
automata src automata@retry.erl
Raw

src/automata@retry.erl

-module(automata@retry).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/retry.gleam").
-export([duration_milliseconds/1, duration_seconds/1, duration_to_string/1, unsafe_milliseconds/1, no_retry/0, fixed/2, with_jitter/2, start/2, current_attempt/1, cumulative_delay/1, policy/1, exponential/3, capped_exponential/4, from_seconds/1, from_minutes/1, from_milliseconds/1, capped_exponential_ms/4, decide/2, should_retry/2, next_delay/2]).
-export_type([failure_kind/0, give_up_reason/0, retry_error/0, duration/0, jitter/0, 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.
-type failure_kind() :: transient | permanent.
-type give_up_reason() :: policy_disallows_retry |
{max_attempts_reached, integer(), integer()} |
{permanent_failure_signaled, integer()} |
{delay_overflow, integer()}.
-type retry_error() :: {negative_duration, binary(), integer()} |
{duration_overflow, binary(), integer()} |
{max_attempts_must_be_positive, integer()} |
{multiplier_must_be_at_least_two, integer()} |
{initial_delay_must_be_positive, integer()} |
{cap_must_not_be_less_than_initial, integer(), integer()}.
-opaque duration() :: {duration, integer()}.
-type jitter() :: no_jitter | full_jitter | equal_jitter.
-opaque policy() :: no_retry |
{fixed, duration(), integer(), jitter()} |
{exponential, duration(), integer(), integer(), jitter()} |
{capped_exponential, duration(), integer(), duration(), integer(), jitter()}.
-opaque context() :: {context,
policy(),
integer(),
duration(),
automata@retry@internal@prng:prng_state()}.
-type decision() :: {retry, duration(), context()} | {give_up, give_up_reason()}.
-file("src/automata/retry.gleam", 105).
?DOC(" Recover the raw millisecond value from a duration.\n").
-spec duration_milliseconds(duration()) -> integer().
duration_milliseconds(Duration) ->
erlang:element(2, Duration).
-file("src/automata/retry.gleam", 111).
?DOC(
" Recover the duration in whole seconds, truncating any sub-second\n"
" remainder.\n"
).
-spec duration_seconds(duration()) -> integer().
duration_seconds(Duration) ->
erlang:element(2, Duration) div 1000.
-file("src/automata/retry.gleam", 116).
?DOC(" Render a duration as `\"<n>ms\"`.\n").
-spec duration_to_string(duration()) -> binary().
duration_to_string(Duration) ->
<<(erlang:integer_to_binary(erlang:element(2, Duration)))/binary,
"ms"/utf8>>.
-file("src/automata/retry.gleam", 125).
?DOC(false).
-spec unsafe_milliseconds(integer()) -> duration().
unsafe_milliseconds(Value) ->
{duration, Value}.
-file("src/automata/retry.gleam", 144).
-spec validate_unit(binary(), integer(), integer()) -> {ok, nil} |
{error, retry_error()}.
validate_unit(Unit, Value, Upper_bound) ->
case Value < 0 of
true ->
{error, {negative_duration, Unit, Value}};
false ->
case Value > Upper_bound of
true ->
{error, {duration_overflow, Unit, Value}};
false ->
{ok, nil}
end
end.
-file("src/automata/retry.gleam", 214).
?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", 220).
?DOC(
" Build a fixed-delay policy: every retry waits exactly `delay`,\n"
" up to `max_attempts` total tries.\n"
).
-spec fixed(duration(), integer()) -> {ok, policy()} | {error, 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", 319).
?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(), 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", 348).
?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,
unsafe_milliseconds(0),
automata@retry@internal@prng:new(Seed)}.
-file("src/automata/retry.gleam", 358).
?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", 364).
?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()) -> duration().
cumulative_delay(Ctx) ->
erlang:element(4, Ctx).
-file("src/automata/retry.gleam", 369).
?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", 427).
-spec validate_exponential_params(duration(), integer(), integer()) -> {ok, nil} |
{error, retry_error()}.
validate_exponential_params(Initial, Multiplier, Max_attempts) ->
case duration_milliseconds(Initial) =< 0 of
true ->
{error,
{initial_delay_must_be_positive, 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", 234).
?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(duration(), integer(), integer()) -> {ok, policy()} |
{error, 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", 254).
?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(duration(), integer(), duration(), integer()) -> {ok,
policy()} |
{error, retry_error()}.
capped_exponential(Initial, Multiplier, Cap, Max_attempts) ->
case validate_exponential_params(Initial, Multiplier, Max_attempts) of
{error, Error} ->
{error, Error};
{ok, _} ->
case duration_milliseconds(Cap) < duration_milliseconds(Initial) of
true ->
{error,
{cap_must_not_be_less_than_initial,
duration_milliseconds(Cap),
duration_milliseconds(Initial)}};
false ->
{ok,
{capped_exponential,
Initial,
Multiplier,
Cap,
Max_attempts,
no_jitter}}
end
end.
-file("src/automata/retry.gleam", 567).
-spec apply_jitter(
integer(),
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", 89).
?DOC(" Build a duration from seconds.\n").
-spec from_seconds(integer()) -> {ok, duration()} | {error, retry_error()}.
from_seconds(Seconds) ->
case validate_unit(<<"seconds"/utf8>>, Seconds, 9007199254740991 div 1000) of
{error, Error} ->
{error, Error};
{ok, _} ->
{ok, {duration, Seconds * 1000}}
end.
-file("src/automata/retry.gleam", 97).
?DOC(" Build a duration from minutes.\n").
-spec from_minutes(integer()) -> {ok, duration()} | {error, retry_error()}.
from_minutes(Minutes) ->
case validate_unit(<<"minutes"/utf8>>, Minutes, 9007199254740991 div 60000) of
{error, Error} ->
{error, Error};
{ok, _} ->
{ok, {duration, Minutes * 60000}}
end.
-file("src/automata/retry.gleam", 129).
-spec guard_duration(binary(), integer(), integer()) -> {ok, duration()} |
{error, retry_error()}.
guard_duration(Unit, Raw, Millis) ->
case Raw < 0 of
true ->
{error, {negative_duration, Unit, Raw}};
false ->
case Millis > 9007199254740991 of
true ->
{error, {duration_overflow, Unit, Raw}};
false ->
{ok, {duration, Millis}}
end
end.
-file("src/automata/retry.gleam", 82).
?DOC(" Build a duration from milliseconds.\n").
-spec from_milliseconds(integer()) -> {ok, duration()} | {error, retry_error()}.
from_milliseconds(Milliseconds) ->
guard_duration(<<"milliseconds"/utf8>>, Milliseconds, Milliseconds).
-file("src/automata/retry.gleam", 300).
?DOC(
" Build a capped exponential policy directly from raw millisecond\n"
" integers, skipping the explicit `from_milliseconds` calls that\n"
" `capped_exponential/4` would otherwise require. Useful when both\n"
" the initial delay and the cap are compile-time literals — the\n"
" common case at a call site.\n"
"\n"
" Validation runs in the same order as `capped_exponential/4`: the\n"
" `initial_ms` duration is constructed first, then `cap_ms`, then the\n"
" underlying `capped_exponential` validates `multiplier`, `max_attempts`,\n"
" and the `cap >= initial` invariant. A bad value at any step short-\n"
" circuits with the corresponding `RetryError`.\n"
"\n"
" Callers that already hold runtime-derived `Duration` values (for\n"
" example, from a config parser that ran `from_minutes`) should\n"
" keep using `capped_exponential/4` and pass the values through\n"
" unchanged.\n"
).
-spec capped_exponential_ms(integer(), integer(), integer(), integer()) -> {ok,
policy()} |
{error, retry_error()}.
capped_exponential_ms(Initial_ms, Multiplier, Cap_ms, Max_attempts) ->
gleam@result:'try'(
from_milliseconds(Initial_ms),
fun(Initial) ->
gleam@result:'try'(
from_milliseconds(Cap_ms),
fun(Cap) ->
capped_exponential(Initial, Multiplier, Cap, Max_attempts)
end
)
end
).
-file("src/automata/retry.gleam", 586).
-spec finish_decision(
context(),
integer(),
integer(),
automata@retry@internal@prng:prng_state()
) -> decision().
finish_decision(Ctx, Final_ms, Next_attempt, Next_prng) ->
Prev_cum = duration_milliseconds(erlang:element(4, Ctx)),
case Final_ms > (9007199254740991 - Prev_cum) of
true ->
{give_up, {delay_overflow, Next_attempt}};
false ->
New_cum = unsafe_milliseconds(Prev_cum + Final_ms),
{retry,
unsafe_milliseconds(Final_ms),
{context,
erlang:element(2, Ctx),
Next_attempt,
New_cum,
Next_prng}}
end.
-file("src/automata/retry.gleam", 451).
-spec decide_fixed(context(), duration(), integer(), 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 = 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", 545).
-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", 515).
-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", 506).
-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", 472).
-spec decide_exponential(
context(),
duration(),
integer(),
gleam@option:option(duration()),
integer(),
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 = duration_milliseconds(Initial),
Cap_ms = case Cap of
{some, C} ->
{some, 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", 378).
?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(), 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", 405).
?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(), failure_kind()) -> boolean().
should_retry(Ctx, Failure) ->
case decide(Ctx, Failure) of
{retry, _, _} ->
true;
{give_up, _} ->
false
end.
-file("src/automata/retry.gleam", 417).
?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(), failure_kind()) -> {ok, duration()} |
{error, give_up_reason()}.
next_delay(Ctx, Failure) ->
case decide(Ctx, Failure) of
{retry, D, _} ->
{ok, D};
{give_up, R} ->
{error, R}
end.