Skip to content

ohhi-vn/easy_rpc

Repository files navigation

Docs Hex.pm

EasyRpc

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: Collaboration between human & AI.

Installation

Add easy_rpc to your dependencies in mix.exs:

def deps do
  [
    {:easy_rpc, "~> 0.9.2"}
  ]
end

Usage (Spark DSL)

EasyRpc now uses Spark DSL for a more powerful and extensible DSL experience.

1. Define your RPC module

defmodule MyApp.RemoteApi do
  use EasyRpc

  config do
    nodes [:"api@node1", :"api@node2"]
    select_mode :round_robin
    sticky_node true
    module RemoteNode.Api
    timeout 5_000
    retry 0
    error_handling false
  end

  rpc_functions do
    rpc_function :get_user, 1
    rpc_function :create_user, 2, retry: 3, timeout: 10_000
    rpc_function :delete_user, 1, new_name: :remove_user, private: true
  end
end

2. Use the generated functions

# With error handling disabled (raises on error)
user = MyApp.RemoteApi.get_user(123)

# With error handling enabled (returns {:ok, result} or {:error, reason})
case MyApp.RemoteApi.get_user(123) do
  {:ok, user} -> process_user(user)
  {:error, %EasyRpc.Error{} = error} -> Logger.error(EasyRpc.Error.format(error))
end

# Private functions (marked with private: true) are not exposed in the public API
# They can only be called from within the module itself

DSL Options

config section:

Option Description
:nodes Static list of node names (e.g., [:"node1@host"])
:nodes_provider Dynamic node discovery via MFA {Mod, Fun, Args}
:select_mode :random, :round_robin, or :hash (default: :random)
:sticky_node Pin to first selected node (default: false)
:module Remote module to call (required)
:timeout Global timeout in ms (default: 5000, use :infinity for no timeout)
:retry Global retry count (default: 0)
:sleep_before_retry Ms to wait before retry (default: 0)
:error_handling Return {:ok, result} tuples (default: false)
:enable_logging Enable detailed logging (default: true)

rpc_function options:

Option Description
:new_name Override the generated function name
:private Generate as defp (default: false)
:retry Override global retry count
:timeout Override global timeout (ms)
:sleep_before_retry Override global sleep before retry
:error_handling Override global error-handling flag

Node Selection Strategies

Configure via the select_mode: option in your config section:

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

Sticky Nodes

config do
  nodes: [:"node1@host", :"node2@host"]
  select_mode: :random
  sticky_node: true
  module: RemoteNode.Api
end

sticky_node: true # process pins to first selected node


### Dynamic Node Discovery

```elixir
config :my_app, :api,
  nodes: {ClusterHelper, :get_nodes, [:backend]},
  select_mode: :round_robin

Error Handling

Without error handling (default — raises on error)

user = MyApi.get_user(123)

With error handling (returns tagged tuples)

case MyApi.get_user(123) do
  {:ok, user}                    -> process(user)
  {:error, %EasyRpc.Error{} = e} -> Logger.error(EasyRpc.Error.format(e))
end

Enable globally in config or per function:

config do
  error_handling true
end

# or per rpc_function:
rpc_functions do
  rpc_function :get_user, 1, error_handling: true
end

Retry Logic

# Global retry
config do
  retry 3
end

# Per-function
rpc_functions do
  rpc_function :critical_op, 1, retry: 5
end

When retry > 0, error_handling is automatically enabled — retried calls always return {:ok, result} | {:error, %EasyRpc.Error{}}.


Sleep Before Retry

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 do
  retry 3
  sleep_before_retry 500
end

# Per-function override
rpc_functions do
  rpc_function :critical_op, 1, retry: 5, sleep_before_retry: 200
end

The 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_retry requires a non-negative integer. The default is 0 (no sleep). Setting it without also setting retry has no effect.


Timeout Configuration

# Global
config do
  timeout 5_000
end

# Per-function
rpc_functions do
  rpc_function :long_op, 1, timeout: 30_000
  rpc_function :health_check, 0, timeout: 500
  rpc_function :no_limit, 0, timeout: :infinity
end

Examples

See the lib_examples repository for complete, runnable examples.


AI Agents & MCP Support

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:all

Start the MCP server:

mix tidewave

Configure your agent to connect to http://localhost:4113/tidewave/mcp (change port in mix.exs if needed). See Tidewave docs for details.

About

Help dev can work easily with rpc in Elixir cluster

Topics

Resources

License

Contributing

Stars

5 stars

Watchers

2 watching

Forks

Packages

 
 
 

Contributors

Languages