Unexpected :slave.call/4 or :peer.call/4 behaviour

Hi.
I have a simple GenServer module like:

defmodule TestGenServer do
  @moduledoc false

  use GenServer

  def start_link(options), do: GenServer.start_link(__MODULE__, options, name: __MODULE__)

  @impl true
  def init(_options) do
    IO.inspect("INIT")
    {:ok, %{}, 0}
  end

  def fetch(), do: GenServer.call(__MODULE__, :fetch)

  @impl true
  def handle_call(:fetch, _from, state) do
    reply = :ok
    {:reply, reply, state}
  end

  @impl true
  def handle_info(:timeout, state) do
    IO.inspect("Timeout")
    {:noreply, state, 100}
  end
end

I’m trying to start it on other or slave node like.

Node.start(:'test@127.0.0.1')
pa = :code.get_path |> Enum.reduce([], &([~c"-pa", &1 | &2]))
{:ok, pid, node} = :peer.start_link(%{name: :test1, host: ~c"127.0.0.1", args: pa})
{:ok, p} = :rpc.call(node, TestGenServer, :start_link, [:ok])
:rpc.call(node, TestGenServer, :fetch, [])

And last call fails with

{:badrpc, {:EXIT, {:noproc, {GenServer, :call, [TelemetryMetricsMnesia, :fetch, 5000]}}}}

If I use :rpc.cast/4 to start GenServer everything works fine.

Is it expected behaviour for :npc.call/4 to terminate process after it returns?
I tried on Erlang 26.1.2 with Elixir 1.15.7 and Erlang 26.0.2 with Elixir 1.15.4. Both works in the same manner.

RPC afaik works by having a process on the other node execute your code. That process starts and links your genserver. Then the process executing your code stops, so the genserver stops as well, as it was linked to that process.

Cast might work by accident if the process executing the first cast has not been stopped / and brought down the genserver before the call is executed.

You either want to start the genserver without linking it to the caller process or even better start it as a child of some supervisor running on the other node.

Supervisor works in the same manner.

Are you trying to start a supervisor or trying to use one already running? The first option will have the exact same constraints I just described for the genserver. It doesn’t matter what process you start, if you use rpc you don’t want to link the started process to the rpc code executing process.

Not linking processes is however not a great thing to do, as they won’t be properly shut down when the system shuts down. So really the genserver (or other processes) should be linked to a supervisor in one of the supervision trees on that other node instead of being linked to the rpc code executing process.

I’m trying to write UnitTest for distributed code. If there is another way I’m curious about it.

I described two options on how to resolve the problems you’re having. :peer is certainly the way to test things requiring distribution.

:peer.call/4 works in the same manner.

It doesn’t matter how many layers you put over :rpc. The issue isn’t the method of transfering the code to be called to the other node. The issue is in the code you let it execute.

There is also :erlang.spawn_link
Or implement start/1 for your GenServer and use :erpc.call.
Or put your GenServer under a Supervisor and start your application on the peer node.