Skip to main content

src/aws@waiter.erl

-module(aws@waiter).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/waiter.gleam").
-export([wait/4]).
-export_type([step/1, waiter_error/1]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Polling helper for Smithy `@waitable` operations.\n"
    "\n"
    " The codegen-emitted `wait_until_<name>` functions are thin\n"
    " wrappers over `wait`. Each wrapper:\n"
    "\n"
    "   1. Invokes the underlying typed operation in a `step` closure.\n"
    "   2. Matches the result against the waiter's acceptors (a list\n"
    "      of `state` + `matcher` rules lifted from the Smithy trait).\n"
    "      Returns `Settled` when an acceptor with `state: success`\n"
    "      matches, `FailedNow(err)` when `state: failure` matches,\n"
    "      `Continue` otherwise.\n"
    "   3. Calls `wait(step, max_attempts, min_delay, max_delay)` to\n"
    "      drive the loop with exponential backoff between attempts.\n"
    "\n"
    " Backoff doubles the previous delay (starting at `min_delay_ms`)\n"
    " up to `max_delay_ms` — Smithy's standard waiter cadence. The\n"
    " helper sleeps between attempts via `process.sleep`; tests pass\n"
    " zero delays so they don't block.\n"
    "\n"
    " `step` carries the 1-indexed attempt number. The codegen\n"
    " generally ignores it but specialised callers can use it for\n"
    " dynamic adjustments (e.g. emit a log line per attempt).\n"
).

-type step(OVJ) :: settled | continue | {failed_now, OVJ}.

-type waiter_error(OVK) :: {failed, OVK} | {max_attempts_exceeded, integer()}.

-file("src/aws/waiter.gleam", 103).
?DOC(
    " Smithy's waiter backoff: double the previous delay, capped at\n"
    " `max_delay_ms`. Starts at `min_delay_ms` (the first sleep) and\n"
    " climbs from there.\n"
).
-spec next_delay(integer(), integer()) -> integer().
next_delay(Current_ms, Max_delay_ms) ->
    Doubled = Current_ms * 2,
    case Doubled > Max_delay_ms of
        true ->
            Max_delay_ms;

        false ->
            Doubled
    end.

-file("src/aws/waiter.gleam", 111).
-spec sleep(integer()) -> nil.
sleep(Ms) ->
    case Ms =< 0 of
        true ->
            nil;

        false ->
            gleam_erlang_ffi:sleep(Ms)
    end.

-file("src/aws/waiter.gleam", 70).
-spec loop(
    fun((integer()) -> step(OVQ)),
    integer(),
    integer(),
    integer(),
    integer()
) -> {ok, nil} | {error, waiter_error(OVQ)}.
loop(Step, Attempt, Max_attempts, Current_delay_ms, Max_delay_ms) ->
    case Step(Attempt) of
        settled ->
            {ok, nil};

        {failed_now, E} ->
            {error, {failed, E}};

        continue ->
            case Attempt >= Max_attempts of
                true ->
                    {error, {max_attempts_exceeded, Max_attempts}};

                false ->
                    sleep(Current_delay_ms),
                    loop(
                        Step,
                        Attempt + 1,
                        Max_attempts,
                        next_delay(Current_delay_ms, Max_delay_ms),
                        Max_delay_ms
                    )
            end
    end.

-file("src/aws/waiter.gleam", 58).
?DOC(
    " Drive a waiter step closure to completion, sleeping between\n"
    " attempts with exponential backoff capped at `max_delay_ms`.\n"
    " Returns `Ok(Nil)` on settle, `Error(Failed(_))` on a\n"
    " `state: failure` acceptor match, `Error(MaxAttemptsExceeded(_))`\n"
    " when the attempt budget is exhausted. `max_attempts == 0` is a\n"
    " defensive guard that returns immediately without invoking\n"
    " `step`.\n"
).
-spec wait(fun((integer()) -> step(OVL)), integer(), integer(), integer()) -> {ok,
        nil} |
    {error, waiter_error(OVL)}.
wait(Step, Max_attempts, Min_delay_ms, Max_delay_ms) ->
    case Max_attempts > 0 of
        false ->
            {error, {max_attempts_exceeded, 0}};

        true ->
            loop(Step, 1, Max_attempts, Min_delay_ms, Max_delay_ms)
    end.