Packages
phoenix_kit
1.7.212
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/integrations/probe.ex
defmodule PhoenixKit.Integrations.Probe do
@moduledoc """
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/1` runs in the **calling** process and, past the TCP
connect, waits on a hard-coded `?TIMEOUT` of 1_200_000 ms — the `timeout`
option bounds only `connect`. 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/1` links, and a LiveView
does not trap exits, so a raise or an abnormal exit inside the check killed
the operator's page outright — before `Task.yield/2` could hand back
`{:exit, reason}`, which is why that clause never ran.
* **The caller must not lose the check.** With a bare `spawn_monitor/1` the
deadline lives in the caller's `receive/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.
"""
use Gettext, backend: PhoenixKitWeb.Gettext
require Logger
@typedoc """
Talks to the network; answers `:ok`, `{:ok, note}` when it succeeded but has
something the operator needs to know, or `{:error, message}`.
"""
@type check :: (-> :ok | {:ok, String.t()} | {:error, String.t()})
@default_deadline 15_000
@doc """
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.
"""
@spec run(check(), timeout()) :: :ok | {:ok, String.t()} | {:error, String.t()}
def run(check, deadline \\ deadline()) when is_function(check, 0) do
parent = self()
ref = make_ref()
# Gettext keeps the locale in the process dictionary, which a spawned process
# does not inherit. Without this the operator reads half the failures in their
# own language and half in English.
locale = Gettext.get_locale(PhoenixKitWeb.Gettext)
# The check waits for `:go` so it cannot finish — or die — before the monitor
# is in place. `spawn_link/1` establishes the link atomically; `Process.link/1`
# from inside the child would leave a window in which the caller could die
# unwatched, which is the whole hazard.
pid =
spawn_link(fn ->
receive do
{:go, ^ref} ->
try do
Gettext.put_locale(PhoenixKitWeb.Gettext, locale)
send(parent, {ref, check.()})
after
# Runs on success and on any exception or exit, so our own failure
# never reaches the caller through the link. Only an untrappable
# :kill skips it.
Process.unlink(parent)
end
end
end)
monitor = Process.monitor(pid)
send(pid, {:go, ref})
receive do
{^ref, result} ->
Process.demonitor(monitor, [:flush])
result
{:DOWN, ^monitor, :process, ^pid, reason} ->
Logger.warning("Connection check crashed: #{inspect(reason)}")
{:error, gettext("Could not reach the service")}
after
deadline ->
# Unlink first: :kill is untrappable, so the check cannot run its `after`
# and unlink itself, and the link would deliver :killed to us.
Process.unlink(pid)
Process.exit(pid, :kill)
Process.demonitor(monitor, [:flush])
flush(ref)
{:error, gettext("The service did not respond in time")}
end
end
# A result that lands in the instant the deadline fires must not be left behind
# in the caller's mailbox: the caller is a LiveView, and it would log the stray
# reply as an unexpected message.
defp flush(ref) do
receive do
{^ref, _result} -> :ok
after
0 -> :ok
end
end
defp deadline do
Application.get_env(:phoenix_kit, :integration_check_deadline, @default_deadline)
end
end