Current section

Files

Jump to
safe_rpc lib safe_rpc client.ex
Raw

lib/safe_rpc/client.ex

defmodule SafeRPC.Client do
@moduledoc "SafeRPC client process and one-shot client helpers."
use GenServer
alias SafeRPC.Protocol
alias SafeRPC.Transport.Unix
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name))
def call(client, op), do: call(client, op, %{}, [])
def call(client, op, payload), do: call(client, op, payload, [])
def call(client, op, payload, opts) when is_pid(client) or is_atom(client) do
timeout = Keyword.get(opts, :timeout, 5_000)
GenServer.call(client, {:request, :call, op, payload, opts}, timeout + 1_000)
end
def call(socket, op, payload, opts) when is_binary(socket) do
request(socket, :call, op, payload, opts)
end
def cast(client, op), do: cast(client, op, %{}, [])
def cast(client, op, payload), do: cast(client, op, payload, [])
def cast(client, op, payload, opts) when is_pid(client) or is_atom(client) do
timeout = Keyword.get(opts, :timeout, 5_000)
GenServer.call(client, {:request, :cast, op, payload, opts}, timeout + 1_000)
end
def cast(socket, op, payload, opts) when is_binary(socket) do
request(socket, :cast, op, payload, opts)
end
def send_request(client, op, payload \\ %{}, opts \\ [])
when is_pid(client) or is_atom(client) do
GenServer.call(client, {:send_request, :call, op, payload, opts})
end
def cancel(%SafeRPC.Task{client: client, id: id}) do
GenServer.call(client, {:cancel, id})
end
@impl true
def init(opts) do
transport = Keyword.get(opts, :transport, Unix)
with {:ok, socket} <- transport.connect(opts) do
state = %{
transport: transport,
socket: socket,
opts: opts,
cap: Keyword.get(opts, :cap),
pending: %{}
}
owner = self()
receiver = spawn_link(fn -> recv_loop(owner, transport, socket) end)
{:ok, Map.put(state, :receiver, receiver)}
end
end
@impl true
def handle_call({:request, kind, op, payload, opts}, from, state) do
timeout = Keyword.get(opts, :timeout, 5_000)
{id, state} = put_pending(from, op, timeout, state)
case send_frame(state, kind, id, op, payload, opts) do
{:ok, state} ->
{:noreply, state}
{:error, reason, state} ->
{pending, state} = pop_pending(id, state)
reply(pending.from, {:error, reason})
{:noreply, state}
end
end
def handle_call({:cancel, id}, _from, state) do
case state.transport.send(state.socket, Protocol.encode_cancel(id), 5_000) do
:ok -> {:reply, :ok, state}
error -> {:reply, error, state}
end
end
def handle_call({:send_request, kind, op, payload, opts}, {caller, _tag}, state) do
timeout = Keyword.get(opts, :timeout, 5_000)
{id, state} = put_pending({:message, caller, :pending_id}, op, timeout, state)
state = put_in(state.pending[id].from, {:message, caller, id})
case send_frame(state, kind, id, op, payload, opts) do
{:ok, state} ->
{:reply, {:ok, %SafeRPC.Task{client: self(), id: id, op: op}}, state}
{:error, reason, state} ->
{_pending, state} = pop_pending(id, state)
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_info({:safe_rpc_reply, payload}, state) do
{:noreply, handle_reply(payload, state)}
end
def handle_info({:safe_rpc_closed, :closed}, state) do
state = fail_pending(:closed, state)
{:stop, :normal, state}
end
def handle_info({:safe_rpc_closed, reason}, state) do
state = fail_pending(reason, state)
{:stop, reason, state}
end
def handle_info({:request_timeout, id}, state) do
case pop_pending(id, state) do
{nil, state} ->
{:noreply, state}
{pending, state} ->
reply(pending.from, {:error, :timeout})
{:noreply, state}
end
end
@impl true
def terminate(_reason, state) do
state.transport.close(state.socket)
:ok
end
defp recv_loop(owner, transport, socket) do
case transport.recv(socket, :infinity) do
{:ok, payload} ->
send(owner, {:safe_rpc_reply, payload})
recv_loop(owner, transport, socket)
{:error, reason} ->
send(owner, {:safe_rpc_closed, reason})
end
end
defp put_pending(from, op, timeout, state) do
id = make_ref()
timer = Process.send_after(self(), {:request_timeout, id}, timeout)
pending = %{from: from, op: op, timer: timer}
{id, %{state | pending: Map.put(state.pending, id, pending)}}
end
defp pop_pending(id, state) do
{pending, pending_map} = Map.pop(state.pending, id)
if pending do
Process.cancel_timer(pending.timer)
end
{pending, %{state | pending: pending_map}}
end
defp send_frame(state, kind, id, op, payload, opts) do
cap = Keyword.get(opts, :cap, state.cap)
meta = Keyword.get(opts, :meta, %{})
encoded = encode(kind, id, cap, op, payload, meta)
case state.transport.send(state.socket, encoded, Keyword.get(opts, :timeout, 5_000)) do
:ok -> {:ok, state}
{:error, reason} -> {:error, reason, state}
end
end
defp handle_reply(payload, state) do
with {:ok, %{id: id, result: result}} <- Protocol.decode_reply(payload),
{pending, state} when not is_nil(pending) <- pop_pending(id, state) do
reply(pending.from, result)
state
else
_error -> state
end
end
defp reply({:message, caller, id}, result), do: send(caller, {SafeRPC.Task, id, result})
defp reply({:message, caller}, result), do: send(caller, {SafeRPC.Task, result})
defp reply(from, result), do: GenServer.reply(from, result)
defp fail_pending(reason, state) do
Enum.each(state.pending, fn {_id, pending} -> reply(pending.from, {:error, reason}) end)
%{state | pending: %{}}
end
defp request(socket, kind, op, payload, opts) do
transport = Keyword.get(opts, :transport, Unix)
timeout = Keyword.get(opts, :timeout, 5_000)
cap = Keyword.get(opts, :cap)
meta = Keyword.get(opts, :meta, %{})
with {:ok, port} <- transport.connect(Keyword.put(opts, :socket, socket)),
result <- send_blocking_request(transport, port, kind, op, payload, cap, meta, timeout),
:ok <- transport.close(port) do
result
end
end
defp send_blocking_request(transport, socket, kind, op, payload, cap, meta, timeout) do
id = make_ref()
encoded = encode(kind, id, cap, op, payload, meta)
with :ok <- transport.send(socket, encoded, timeout),
{:ok, response} <- transport.recv(socket, timeout),
{:ok, result} <- Protocol.decode_reply(response, id) do
result
end
end
defp encode(:call, id, cap, op, payload, meta),
do: Protocol.encode_call(id, cap, op, payload, meta)
defp encode(:cast, id, cap, op, payload, meta),
do: Protocol.encode_cast(id, cap, op, payload, meta)
end