Current section
Files
Jump to
Current section
Files
lib/exrpc.ex
defmodule Exrpc do
@moduledoc """
Lean Elixir RPC library based on RESP (REdis Serialization Protocol).
To set up, start `Exrpc.Server` on the server side and `Exrpc.Client` on the client side.
## Server-side
```elixir
# module with function to be called remotely
defmodule ServerSideModule do
def hello(name), do: "Hello " <> name
end
# start link or add child spec to your supervisor
Exrpc.Server.start_link(name: HelloRpc, port: 7379, mfa_list: [&ServerSideModule.hello/1])
```
## Client-side
```elixir
# start link or add child spec to your supervisor
Exrpc.Client.start_link(name: HelloRpc, host: "localhost", port: 7379)
# make a remote function call:
Exrpc.call(HelloRpc, ServerSideModule, :hello, ["world"])
```
"""
require Logger
alias Exrpc.Client
alias Exrpc.MFALookup
alias Exrpc.Request
alias Exrpc.Response
@type on_call :: {:badrpc, atom} | {:badrpc, atom, binary} | any()
@initial_backoff_ms 100
@max_backoff_ms 5000
@jitter_factor 0.2
@doc """
List available remote functions.
## Example
iex> Exrpc.mfa_list(:my_client)
[{ServerSideModule, :hello, 1}]
"""
@spec mfa_list(Client.t()) :: list(mfa())
def mfa_list(client) do
Client.InfoTable.get(client, :mfa_list, [])
end
@doc """
Calls a remote function.
## Example
iex> Exrpc.call(:my_client, ServerSideModule, :hello, ["world"])
"Hello world"
"""
@spec call(Client.t(), module(), atom(), list(), timeout()) :: on_call()
def call(client, mod, fun, arg, timeout \\ 5000)
when is_atom(mod) and is_atom(fun) and is_list(arg) do
task =
client
|> Client.task_via()
|> Task.Supervisor.async(__MODULE__, :loop_call, [client, mod, fun, arg])
try do
Task.await(task, timeout)
catch
:exit, {:timeout, _} ->
Task.shutdown(task, :brutal_kill)
{:badrpc, :timeout}
end
end
def loop_call(client, mod, fun, arg, retries \\ 0) do
delay_ms = calculate_delay_ms(retries)
:timer.sleep(delay_ms)
with {_, _} = mfa_lookup <- Client.InfoTable.get(client, :mfa_lookup, :mfa_lookup_fail),
id when is_integer(id) <- MFALookup.mfa_to_id(mfa_lookup, {mod, fun, length(arg)}),
command <- Request.encode({:apply, id, arg}),
{:ok, response} <- Redix.command(Client.via(client, retries), command),
{:goodrpc, result} <- Response.decode(response) do
result
else
:mfa_lookup_fail ->
loop_call(client, mod, fun, arg, retries + 1)
{:error, %Redix.ConnectionError{}} ->
loop_call(client, mod, fun, arg, retries + 1)
nil ->
{:badrpc, :invalid_request}
{:badrpc, reason} ->
{:badrpc, reason}
{:badrpc, reason, message} ->
{:badrpc, reason, message}
end
end
defp calculate_delay_ms(0), do: 0
defp calculate_delay_ms(retries) do
backoff = retries * @initial_backoff_ms
capped_backoff = min(@max_backoff_ms, backoff)
jitter = :rand.uniform() * (capped_backoff * @jitter_factor)
trunc(capped_backoff + jitter)
end
end