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 the operator needs to know, {: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".
Functions
Runs check in an isolated process, bounded by deadline milliseconds.
Types
Talks to the network; answers :ok, {:ok, note} when it succeeded but has
something the operator needs to know, {: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".
Functions
@spec run(check(), timeout()) :: :ok | {:ok, String.t()} | {: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.