EasyRpc - A simple way to call rpc in Elixir cluster

Intro

EasyRpc for easy to wrap a remote function (rpc) to local module for convenience. Dev just to need add config for target nodes (or use {Module, :function, args} for get nodes in runtime) and add functions want to wrap then use EasyRpc with option for EasyRpc can read the config. Then call rpc in app like a local function.

This library help developer easy to wrap a remote procedure call (rpc, library uses Erlang :erpc module).

EasyRpc supports some basic features for wrapping rpc: retry, timeout, error_handling. Each function can has seperated options or use global options (config for all function in a module).

Source code is available on Github and package on Hex

Example

Add Configs

This way you can add config to config.exs/dev/prod.exs or runtime.exs(for runtime).

config :simple_example, :remote_defrpc,
  nodes: [:"remote@127.0.0.1"],  # or {ClusterHelper, :get_nodes, [:remote_api]},
  select_mode: :round_robin

Declare functions

defmodule Remote
  use EasyRpc.DefRpc,
    otp_app: :simple_example,
    config_name: :remote_defrpc,
    # Remote module name
    module: RemoteNode.Interface,
    timeout: 1000

  defrpc :get_data
  defrpc :put_data, args: 1
  defrpc :clear, args: 2, as: :clear_data, private: true, retry: 1
  defrpc :put_data, args: [:name], new_name: :put_with_retry, retry: 3, timeout: 3000
end

Now call rpc like a local function

Remote.get_data()

Thanks for reading!

3 Likes

Why I need to define remote functions as part of compile-time config instead of defining them, well, in module? With similar approach as I could do with defdelegate for example.

Simple & easy for newbies is thing for this libray and lib also help reduce boilerplate code & can use with our other module is ClusterHelper (like a global registry for cluster). Remember with with dynamic Elixir cluster much harder than static cluster.

Thanks for you comment!

Yeah, but I find it easier to reason if instead of writing:

# config/config.ex
config :app_name, :wrapper_name,
  nodes: [:"test1@test.local"], # or using function like nodes: {Module, Fun, Args}
  error_handling: true,
  select_mode: :random,
  module: TargetApp.Interface.Api,
  functions: [
    # {function_name, arity, options}
    {:get_data, 1},
    {:put_data, 1, error_handling: false},
    {:clear, 2, new_name: :clear_data, retry: 3},
    {:clear_all, 0, new_name: :clear_all, private: true}, # wrap to private function.
  ]

# Code
defmodule DataHelper do
  use EasyRpc.RpcWrapper,
    otp_app: :app_name,
    config_name: :account_wrapper

  def process_remote() do
    # call rpc like a local function.
    case get_data("key") do
      {:ok, data} ->
        # do something with data

      {:error, reason} ->
        # handle error
    end
  end
end

# Or call from other module like
{:ok, result} = DataHelper.get_data("my_key")

You would do:

# config/runtime.exs
# Bonus - it supports config to be more runtime thingy
config :app_name, DataHelper,
  nodes: [:"test1@test.local"], # or using function like nodes: {Module, Fun, Args}
  select_mode: :random

# Code
defmodule DataHelper do
  use EasyRpc.RpcWrapper,
    otp_app: :app_name,
    module: TargetApp.Interface.Api,
    error_handling: true

  defrpc get_data(key)
  defrpc put_data(key, data), error_handling: false
  defrpc clear(key, opts), as: :clear_data, retries: 3
  defprcp clear_all(), 

  def process_remote() do
    # call rpc like a local function.
    case get_data("key") do
      {:ok, data} ->
        # do something with data

      {:error, reason} ->
        # handle error
    end
  end
end

# Or call from other module like
{:ok, result} = DataHelper.get_data("my_key")
4 Likes

Totally agree with your version being more readable. I am in general not a fan of using the tuple format in code if it can be avoided, as it’s very hard to track down.

In this case it also provides a way to write documentation for these generated functions, which is not possible with original approach.

1 Like

Yes, I think this way is better than config way. I will add support this in the next version. Thank you for your suggestion!

1 Like

I have added new way to wrapping rpc. More friendly than last one but still not good enough. I’ finding a way to improve defrpc macro. I hope I can back and improve this soon!

Add Configs

config :simple_example, :remote_defrpc,
  nodes: [:"remote@127.0.0.1"],  # or {ClusterHelper, :get_nodes, [:remote_api]},
  select_mode: :round_robin

Declare functions

defmodule Remote
  use EasyRpc.DefRpc,
    otp_app: :simple_example,
    config_name: :remote_defrpc,
    # Remote module name
    module: RemoteNode.Interface,
    timeout: 1000

  defrpc :get_data
  defrpc :put_data, args: 1
  defrpc :clear, args: 2, as: :clear_data, private: true, retry: 1
  defrpc :put_data, args: [:name], new_name: :put_with_retry, retry: 3, timeout: 3000
end