Skip to main content

src/aws@internal@actor_lifecycle.erl

-module(aws@internal@actor_lifecycle).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/actor_lifecycle.gleam").
-export([safe_call/3, shutdown_via_stop/2, shutdown_via_stop_sync/3]).

-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(
    " Generic teardown helpers for actors that follow the SDK's\n"
    " \"polite stop message\" convention. Used by `credentials_cache` and\n"
    " `retry/rate_limiter` — both opaque types wrap a `Subject` whose\n"
    " message variant set includes a `Stop` constructor that triggers\n"
    " `actor.stop()` next dispatch.\n"
    "\n"
    " Lives in a separate module so the same fire-and-forget + monitor-\n"
    " based synchronous teardown lands in exactly one place. Adding a\n"
    " third long-lived actor later (a request-rate limiter, an event-\n"
    " stream demuxer) reuses these directly.\n"
).

-file("src/aws/internal/actor_lifecycle.gleam", 28).
?DOC(
    " A non-crashing replacement for `actor.call` / `process.call`.\n"
    "\n"
    " `actor.call` *panics* the caller when the callee has exited or fails to\n"
    " reply within the timeout. That is fine for a supervised callee, but the\n"
    " SDK's long-lived actors (credentials cache, retry rate-limiter) are\n"
    " unsupervised — a single crashing provider would otherwise take down the\n"
    " consumer's process on every subsequent call. This variant monitors the\n"
    " callee and selects over both the reply and the `DOWN` signal, returning\n"
    " `Error(Nil)` on a dead owner, an immediate `DOWN`, or a timeout, and\n"
    " `Ok(reply)` on a normal reply. Callers map `Error(Nil)` onto their own\n"
    " error shape so the consumer sees a recoverable error, never a panic.\n"
    "\n"
    " Mirrors `gleam_erlang`'s internal `perform_call`, minus the two\n"
    " `let assert`/`panic` lines that make the stock `call` crash.\n"
).
-spec safe_call(
    gleam@erlang@process:subject(LBN),
    integer(),
    fun((gleam@erlang@process:subject(LBP)) -> LBN)
) -> {ok, LBP} | {error, nil}.
safe_call(Subject, Timeout_ms, Make_request) ->
    case gleam@erlang@process:subject_owner(Subject) of
        {error, _} ->
            {error, nil};

        {ok, Callee} ->
            Reply_subject = gleam@erlang@process:new_subject(),
            Monitor = gleam@erlang@process:monitor(Callee),
            gleam@erlang@process:send(Subject, Make_request(Reply_subject)),
            Outcome = begin
                _pipe = gleam_erlang_ffi:new_selector(),
                _pipe@1 = gleam@erlang@process:select_map(
                    _pipe,
                    Reply_subject,
                    fun(Field@0) -> {ok, Field@0} end
                ),
                _pipe@2 = gleam@erlang@process:select_specific_monitor(
                    _pipe@1,
                    Monitor,
                    fun(_) -> {error, nil} end
                ),
                gleam_erlang_ffi:select(_pipe@2, Timeout_ms)
            end,
            gleam@erlang@process:demonitor_process(Monitor),
            case Outcome of
                {ok, Reply_result} ->
                    Reply_result;

                {error, nil} ->
                    {error, nil}
            end
    end.

-file("src/aws/internal/actor_lifecycle.gleam", 57).
?DOC(
    " Fire-and-forget teardown: send the supplied `Stop` message and\n"
    " return immediately. The actor exits on its next dispatch. Safe to\n"
    " call against a dead actor — Erlang silently drops sends to a\n"
    " terminated Pid.\n"
).
-spec shutdown_via_stop(gleam@erlang@process:subject(LBT), LBT) -> nil.
shutdown_via_stop(Subject, Stop_message) ->
    gleam@erlang@process:send(Subject, Stop_message).

-file("src/aws/internal/actor_lifecycle.gleam", 68).
?DOC(
    " Synchronous teardown: monitor the owning Pid, send `Stop`, then\n"
    " wait for the `DOWN` signal up to `timeout_ms`. Returns `Ok(Nil)`\n"
    " on clean exit (already-dead actor short-circuits here too — the\n"
    " `subject_owner` lookup fails fast). Returns `Error(Nil)` only on\n"
    " real timeout — i.e. the actor is alive but didn't exit within the\n"
    " window. The monitor is demonitored on the timeout path so the\n"
    " caller's mailbox doesn't accumulate stray `DOWN` messages.\n"
).
-spec shutdown_via_stop_sync(gleam@erlang@process:subject(LBV), LBV, integer()) -> {ok,
        nil} |
    {error, nil}.
shutdown_via_stop_sync(Subject, Stop_message, Timeout_ms) ->
    case gleam@erlang@process:subject_owner(Subject) of
        {error, _} ->
            {ok, nil};

        {ok, Pid} ->
            Monitor = gleam@erlang@process:monitor(Pid),
            gleam@erlang@process:send(Subject, Stop_message),
            Selector = begin
                _pipe = gleam_erlang_ffi:new_selector(),
                gleam@erlang@process:select_specific_monitor(
                    _pipe,
                    Monitor,
                    fun(_) -> nil end
                )
            end,
            case gleam_erlang_ffi:select(Selector, Timeout_ms) of
                {ok, nil} ->
                    {ok, nil};

                {error, nil} ->
                    gleam@erlang@process:demonitor_process(Monitor),
                    {error, nil}
            end
    end.