A library that makes it easy to wrap a remote procedure call (RPC) as a local function.
EasyRpc uses Erlang's :erpc module under the hood and adds retry, timeout, and error-handling support on top.
Each function can carry its own options, or inherit global options declared at the module level. EasyRpc works seamlessly with ClusterHelper for dynamic Elixir clusters.
Note: Collab between human & AI.
Add easy_rpc to your dependencies in mix.exs:
def deps do
[
{:easy_rpc, "~> 0.7.0"}
]
endDefRpc |
RpcWrapper |
|
|---|---|---|
| Functions declared | In module via defrpc macro |
In config file |
| Node config loaded | At runtime per call | At compile time |
| Cluster topology changes | ✅ Picked up automatically | Requires recompile |
| Best for | Explicit control, dynamic clusters | All-in-config, stable topology |
# config/config.exs (or runtime.exs for runtime topology)
config :my_app, :remote_defrpc,
nodes: [:"remote@127.0.0.1"],
# or: nodes: {ClusterHelper, :get_nodes, [:remote_api]},
select_mode: :round_robin,
sticky_node: true
:round_robinand:sticky_nodeare tracked per process.
defmodule MyApp.Remote do
use EasyRpc.DefRpc,
otp_app: :my_app,
config_name: :remote_defrpc,
module: RemoteNode.Interface,
timeout: 1_000
defrpc :get_data
defrpc :put_data, args: 1
defrpc :clear, args: 2, as: :clear_data, private: true
defrpc :put_data, args: [:name], as: :put_with_retry, retry: 3, sleep_before_retry: 200, timeout: 1_000
enddefrpc options:
| Option | Description |
|---|---|
:args |
Arity as integer, [] (zero), or list of named atoms |
:as / :new_name |
Override the generated function name |
:private |
Generate as defp (default: false) |
:retry |
Override global retry count |
:sleep_before_retry |
Milliseconds to wait before each retry (default: 0) |
:timeout |
Override global timeout (ms or :infinity) |
:error_handling |
Override global error-handling flag |
All function and node information is declared in config. Functions are generated at compile time.
# config/config.exs
config :my_app, :data_wrapper,
nodes: [:"node1@host", :"node2@host"],
# or: nodes: {ClusterHelper, :get_nodes, [:data]},
error_handling: true,
select_mode: :random,
module: TargetApp.Interface.Api,
timeout: 5_000,
retry: 3,
sleep_before_retry: 500, # wait 500 ms between retries
functions: [
# {function_name, arity}
# {function_name, arity, options}
{:get_data, 1},
{:put_data, 1, [error_handling: false]},
{:clear, 2, [new_name: :clear_data, retry: 3, sleep_before_retry: 100]},
{:clear_all, 0, [new_name: :reset, private: true]}
]defmodule MyApp.DataHelper do
use EasyRpc.RpcWrapper,
otp_app: :my_app,
config_name: :data_wrapper
def process_remote() do
case get_data("key") do
{:ok, data} -> data
{:error, reason} -> {:error, reason}
end
end
end
# Or call directly:
{:ok, result} = MyApp.DataHelper.get_data("my_key")Configure via select_mode: in your config:
| Strategy | Description |
|---|---|
:random |
Randomly picks a node on each call (default) |
:round_robin |
Circular distribution, tracked per process |
:hash |
Consistent hashing on args — same args always hit the same node |
config :my_app, :api,
nodes: [:node1@host, :node2@host],
select_mode: :random,
sticky_node: true # process pins to first selected nodeconfig :my_app, :api,
nodes: {ClusterHelper, :get_nodes, [:backend]},
select_mode: :round_robinuser = MyApi.get_user(123)case MyApi.get_user(123) do
{:ok, user} -> process(user)
{:error, %EasyRpc.Error{} = e} -> Logger.error(EasyRpc.Error.format(e))
endEnable globally in config or per function:
config :my_app, :api, error_handling: true
# or per defrpc:
defrpc :get_user, args: 1, error_handling: true# Global retry
config :my_app, :api, retry: 3
# Per-function
defrpc :critical_op, args: 1, retry: 5When
retry > 0,error_handlingis automatically enabled — retried calls always return{:ok, result} | {:error, %EasyRpc.Error{}}.
By default EasyRpc retries immediately after a failure. Use sleep_before_retry
to add a fixed delay (in milliseconds) between attempts. This is useful for
giving a remote node time to recover, or for reducing thundering-herd pressure
on a flapping service.
# Global — all retries in this config wait 500 ms
config :my_app, :api,
retry: 3,
sleep_before_retry: 500
# Per-function override
defrpc :critical_op, args: 1, retry: 5, sleep_before_retry: 200The sleep happens between attempts — there is no delay before the first call, and no delay after the final failure.
attempt 1 → fails → sleep 500 ms
attempt 2 → fails → sleep 500 ms
attempt 3 → fails → sleep 500 ms
attempt 4 → fails → return {:error, ...}
sleep_before_retryrequires a non-negative integer. The default is0(no sleep). Setting it without also settingretryhas no effect.
# Global
config :my_app, :api, timeout: 5_000
# Per-function
defrpc :long_op, args: 1, timeout: 30_000
defrpc :health_check, timeout: 500
defrpc :no_limit, timeout: :infinitySee the lib_examples repository for complete, runnable examples.
Sync usage rules from deps into your repo for AI agent support:
mix usage_rules.sync AGENTS.md --all \
--link-to-folder deps \
--inline usage_rules:allStart the MCP server:
mix tidewaveConfigure your agent to connect to http://localhost:4113/tidewave/mcp (change port in mix.exs if needed).
See Tidewave docs for details.