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([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.