Current section
Files
Jump to
Current section
Files
lib/pi/target/runtime/worker.ex
defmodule Pi.Target.Runtime.Worker do
@moduledoc false
alias Pi.Target.Runtime.Evaluator
alias Pi.Target.Runtime.Transport
@protocol 1
@capabilities [
:stateful_eval,
:structured_diagnostics,
:sidecar_restore,
:timeout_cancellation
]
def run do
with {:ok, socket} <- connect(),
:ok <- Transport.send_term(socket, {:hello, handshake()}),
:ok <- loop(socket, %{}) do
:ok
else
{:error, reason} ->
IO.puts(:stderr, "pi target worker stopped: #{inspect(reason)}")
System.halt(70)
end
end
defp loop(socket, evaluators) do
case Transport.recv_term(socket, :infinity) do
{:ok, {:request, id, :ping, _payload}} ->
with :ok <- Transport.send_term(socket, {:response, id, {:ok, handshake()}}) do
loop(socket, evaluators)
end
{:ok, {:request, id, :eval, payload}} when is_map(payload) ->
{response, evaluators} = evaluate(evaluators, payload)
with :ok <- Transport.send_term(socket, {:response, id, response}) do
loop(socket, evaluators)
end
{:ok, {:request, id, :reset, %{session_id: session_id}}} ->
{response, evaluators} = reset(evaluators, session_id)
with :ok <- Transport.send_term(socket, {:response, id, response}) do
loop(socket, evaluators)
end
{:ok, {:request, id, :reload, %{beams: beams}}} when is_list(beams) ->
response = {:ok, reload_beams(beams)}
with :ok <- Transport.send_term(socket, {:response, id, response}) do
loop(socket, evaluators)
end
{:ok, {:request, id, :shutdown, _payload}} ->
Transport.send_term(socket, {:response, id, {:ok, :shutdown}})
{:ok, request} ->
Transport.send_term(socket, {:protocol_error, inspect(request)})
loop(socket, evaluators)
{:error, reason} ->
{:error, reason}
end
end
defp evaluate(evaluators, payload) do
session_id = Map.get(payload, :session_id, "default")
timeout = Map.get(payload, :timeout, 30_000)
{pid, evaluators} = evaluator(evaluators, session_id, payload)
tag = make_ref()
owner = self()
{task, monitor} =
spawn_monitor(fn -> send(owner, {tag, Evaluator.evaluate(pid, payload)}) end)
receive do
{^tag, result} ->
Process.demonitor(monitor, [:flush])
{{:ok, Map.put(result, :runtime, runtime_meta())}, evaluators}
{:DOWN, ^monitor, :process, ^task, reason} ->
{{:error, %{kind: :worker_eval_exit, message: Exception.format_exit(reason)}},
Map.delete(evaluators, session_id)}
after
timeout ->
Process.exit(task, :kill)
Process.exit(pid, :kill)
Process.demonitor(monitor, [:flush])
{{:error, %{kind: :timeout, message: "Evaluation timed out after #{timeout}ms"}},
Map.delete(evaluators, session_id)}
end
end
defp evaluator(evaluators, session_id, payload) do
case Map.get(evaluators, session_id) do
pid when is_pid(pid) ->
if Process.alive?(pid) do
{pid, evaluators}
else
start_evaluator(evaluators, session_id, payload)
end
_other ->
start_evaluator(evaluators, session_id, payload)
end
end
defp start_evaluator(evaluators, session_id, payload) do
{:ok, pid} = Evaluator.start(payload)
{pid, Map.put(evaluators, session_id, pid)}
end
defp reload_beams(beams) do
Enum.map(beams, fn path ->
path = Path.rootname(path)
case :code.load_abs(String.to_charlist(path)) do
{:module, module} -> %{path: path <> ".beam", module: module, status: :loaded}
{:error, reason} -> %{path: path <> ".beam", status: :error, reason: reason}
end
end)
end
defp reset(evaluators, session_id) do
case Map.pop(evaluators, session_id) do
{pid, evaluators} when is_pid(pid) ->
result = Evaluator.reset(pid)
{{:ok, result}, evaluators}
{nil, evaluators} ->
{{:ok, :not_found}, evaluators}
end
end
defp connect do
port = System.fetch_env!("PI_ELIXIR_TARGET_PORT") |> String.to_integer()
token = System.fetch_env!("PI_ELIXIR_TARGET_TOKEN")
case :gen_tcp.connect({127, 0, 0, 1}, port, [:binary, packet: 4, active: false], 10_000) do
{:ok, socket} ->
with :ok <- Transport.send_term(socket, {:authenticate, token}), do: {:ok, socket}
error ->
error
end
end
defp handshake do
%{
protocol: @protocol,
bootstrap_hash: System.get_env("PI_ELIXIR_TARGET_BOOTSTRAP_HASH"),
capabilities: @capabilities,
project_root: File.cwd!(),
profile: System.get_env("PI_ELIXIR_TARGET_PROFILE", "project"),
app: Mix.Project.config()[:app],
elixir: System.version(),
otp: System.otp_release(),
os_pid: System.pid()
}
end
defp runtime_meta do
%{
profile: System.get_env("PI_ELIXIR_TARGET_PROFILE", "project"),
node: Node.self(),
os_pid: System.pid()
}
end
end