Runs a connection check in an isolated process under a hard deadline.
Connection checks talk to the network, and the libraries behind them bound neither their runtime nor their crashes:
:gen_smtp_client.open/1runs in the calling process and, past the TCP connect, waits on a hard-coded?TIMEOUTof 1_200_000 ms — thetimeoutoption bounds onlyconnect. A tarpit relay parks the caller for twenty minutes.- ExAws retries transport errors with backoff, which adds up to minutes.
Every call site is a LiveView callback, so the check must be watched in both directions, and getting only one of them right is worse than getting neither:
- The check must not kill the caller.
Task.async/1links, and a LiveView does not trap exits, so a raise or an abnormal exit inside the check killed the operator's page outright — beforeTask.yield/2could hand back{:exit, reason}, which is why that clause never ran. - The caller must not lose the check. With a bare
spawn_monitor/1the deadline lives in the caller'sreceive/after, so when the LiveView goes away mid-check — the operator hit refresh — nothing is left to fire it. The check stays parked in gen_smtp for twenty minutes holding its socket, and, being unlinked, it is now unreachable rather than merely slow. That is the first hazard relocated, not removed.
So: link, monitor, and unlink before dying — which is exactly what
LiveView's own start_async does (phoenix_live_view/async.ex: Task.start_link/1,
then a monitor on top, then the work wrapped in try/after Process.unlink/1).
The link reaps the check when the
caller dies; the monitor delivers the result and the crash reason; unlinking
before dying keeps the check's own failure from travelling back up the link. At
the deadline the caller unlinks before killing, because :kill is untrappable
— the check cannot unlink itself, and the link would carry :killed straight
back.
The one signal a link necessarily carries is an untrappable :kill of the check
process by a third party. Nothing holds its pid, so nothing can; Task and
LiveView accept the same exposure.
Summary
Types
Talks to the network; answers :ok, {:ok, note} when it succeeded but has
something to say as well (see note/0), {:error, message} when it was
actively rejected, or {:inconclusive, message} when the check could not
reach a yes/no verdict at all (a response neither a pass nor a rejection) —
distinct from :error so a caller can tell "definitely wrong" apart from
"could not tell".
What a check has to say beyond pass or fail, split by how long it stays true.
Functions
Runs check in an isolated process, bounded by deadline milliseconds.
Types
@type check() :: (-> :ok | {:ok, String.t() | note()} | {:error, String.t()} | {:inconclusive, String.t()})
Talks to the network; answers :ok, {:ok, note} when it succeeded but has
something to say as well (see note/0), {:error, message} when it was
actively rejected, or {:inconclusive, message} when the check could not
reach a yes/no verdict at all (a response neither a pass nor a rejection) —
distinct from :error so a caller can tell "definitely wrong" apart from
"could not tell".
What a check has to say beyond pass or fail, split by how long it stays true.
:fact— a standing property of this connection: the account it belongs to, the bot it is, a permission it lacks. It stays true until the credentials or the account change, so it is stored on the connection and shown with it.:reading— a figure at the moment of the check: a balance, remaining credits, a send quota. It is out of date as soon as anything runs, so it is reported once, to whoever asked for the check, and never stored. Somewhere that wants a current figure asks for a check of its own (PhoenixKit.Integrations.reading/2).
A bare string is read as a :fact, which is what validators returned before
the two were told apart.
Functions
@spec run(check(), timeout()) :: :ok | {:ok, String.t() | note()} | {:error, String.t()} | {:inconclusive, String.t()}
Runs check in an isolated process, bounded by deadline milliseconds.
Returns what check returned. If it crashes, exits, or overruns the deadline,
returns {:error, message} — either way the caller is left standing, and the
check does not outlive it.