2

I was testing inter-node function call (rpc) like this:

defmodule RpcTest do
  def run do 
    Task.Supervisor.async( {DBServer.DistSupervisor, :'dbserver@hostname'},  fn -> "test" end)
    |> Task.await
    |> IO.inspect
  end
end

Then I run dbserver node, call Task.Supervisor.start_link(name: DBServer.DistSupervisor) in the dbserver to receive rcp, and execute the code above in another node dbclient

It could run the rpc correctly as below.

dbserver

$ iex --sname dbserver --cookie a -S mix
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Compiling 1 file (.ex)
Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(dbserver@hostname)1> Task.Supervisor.start_link(name: DBServer.DistSupervisor)
{:ok, #PID<0.101.0>}
iex(dbserver@hostname)2> 

dbclient

$ elixir --sname dbclient --cookie a -S mix run -e RpcTest.run
"test"

However, after I change the code from "test" to "test test", the dbclient node does not work.

Here is the error message

dbserver

iex(dbserver@hostname)2> 
12:54:57.430 [error] Task #PID<0.110.0> started from {:"dbclient@hostname", #PID<13423.52.0>} terminating
** (BadFunctionError) expected a function, got: #Function<0.113878361/0 in RpcTest>
    :erlang.apply/2
    (elixir) lib/task/supervised.ex:94: Task.Supervised.do_apply/2
    (elixir) lib/task/supervised.ex:45: Task.Supervised.reply/5
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Function: #Function<0.113878361/0 in RpcTest>
    Args: []

dbclient

$ elixir --sname dbclient --cookie a -S mix run -e RpcTest.run
Compiling 1 file (.ex)
** (EXIT from #PID<0.52.0>) an exception was raised:
    ** (BadFunctionError) expected a function, got: #Function<0.113878361/0 in RpcTest.run/0>
        :erlang.apply/2
        (elixir) lib/task/supervised.ex:94: Task.Supervised.do_apply/2
        (elixir) lib/task/supervised.ex:45: Task.Supervised.reply/5
        (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Moreover, it does work after dbserver restarts manually.

Interestingly, it does work with iex whatever the code is.

$ iex --sname dbclient --cookie a -S mix
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Compiling 1 file (.ex)
Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(dbclient@hostname)1> Task.Supervisor.async( {DBServer.DistSupervisor, :'dbserver@hostname'},  fn -> "test" end) |>  
...(dbclient@hostname)1> Task.await |>                                      
...(dbclient@hostname)1> IO.inspect                                          
"test"                                       
"test"
iex(dbclient@hostname)2> Task.Supervisor.async( {DBServer.DistSupervisor, :'dbserver@hostname'},  fn -> "test test" end) |> 
...(dbclient@hostname)2> Task.await |>                                      
...(dbclient@hostname)2> IO.inspect  
"test test"                                       
"test test"

My question is,

  • is it the wrong way to call the function in a node from another node?
  • when using inter-node function call, do I have to re-execute the callee node every time I changed the client code?
  • why are the behaviors different between iex and elixir script?

The code and the whole project are uploaded here: https://github.com/ayamamori/rpc_test/blob/master/lib/rpc_test.ex

Thanks in advance!

gecko655
  • 313
  • 2
  • 9

1 Answers1

1

There is a subtle difference between functions declared in IEx and the ones declared in a module, even if they are anonymous.

Let's say we have the following module:

defmodule FunInfo do
  def info do
    :erlang.fun_info(fn -> "test" end)
  end
end

Now in IEx:

iex(1)> FunInfo.info
[pid: #PID<0.91.0>, module: FunInfo, new_index: 0,
 new_uniq: <<98, 250, 216, 183, 54, 136, 109, 221, 200, 243, 9, 84, 249, 185, 187, 173>>,
 index: 0, uniq: 51893957, name: :"-info/0-fun-0-", arity: 0, env: [], 
 type: :local]

And the same function, but declared in IEx:

iex(2)> :erlang.fun_info(fn -> "test" end)
[pid: #PID<0.91.0>, module: :erl_eval, new_index: 20,
 new_uniq: <<103, 57, 49, 11, 11, 201, 159, 65, 226, 96, 121, 97, 18, 82, 151, 208>>,
 index: 20, uniq: 54118792, name: :"-expr/5-fun-3-", arity: 0,
 env: [{[], :none, :none,
   [{:clause, 12, [], [],
     [{:bin, 0,
       [{:bin_element, 0, {:string, 0, 'test'}, :default, :default}]}]}]}],
 type: :local]

We can highlight a few things about the information of the functions. For the one defined in the FunInfo module we have:

  • module: FunInfo
  • :new_uniq: and :uniq identificators.
  • :env does not contain the AST of the function.

While for the one define in IEx we have:

  • module: :erl_eval
  • :new_uniq: and :uniq identificators.
  • :env has the AST of the anonymous function.

Anonymous functions declared in a module

The :new_uniq and :uniq values change every time you change the anonymous function source code. If two nodes have different versions of the same module, this anonymous functions will have different :new_uniq and :uniq identificator. When you try to execute the anonymous function the server doesn't know, it'll fail.

Anonymous functions declared in IEx

The :new_uniq and :uniq values does not change for every function you declare. They live inside :erl_eval module which is already compiled and you are not changing its source code. Also, every function declared in IEx has its AST in :env so when you ask a remote node to execute that function, you are also sending the body of the function.

iex(1)> :erlang.fun_info(fn -> "test" end)
[pid: #PID<0.91.0>, module: :erl_eval, new_index: 20,
 new_uniq: <<103, 57, 49, 11, 11, 201, 159, 65, 226, 96, 121, 97, 18, 82, 151, 208>>,
 index: 20, uniq: 54118792, name: :"-expr/5-fun-3-", arity: 0,
 env: [{[], :none, :none,
       [{:clause, 26, [], [],
         [{:bin, 0,
           [{:bin_element, 0, {:string, 0, 'test'}, :default, :default}]}]}]}],
 type: :local]

If I create another anonymous function the :new_uniq and :uniq values do not change, but the :env value changes with the correct AST.

iex(2)> :erlang.fun_info(fn -> "test test" end)
[pid: #PID<0.91.0>, module: :erl_eval, new_index: 20,
new_uniq: <<103, 57, 49, 11, 11, 201, 159, 65, 226, 96, 121, 97, 18, 82, 151, 208>>,
 index: 20, uniq: 54118792, name: :"-expr/5-fun-3-", arity: 0,
 env: [{[], :none, :none,
       [{:clause, 27, [], [],
         [{:bin, 0,
           [{:bin_element, 0, {:string, 0, 'test test'}, :default, :default}]}]}]}],
 type: :local]

I hope this answers your question.

Alex de Sousa
  • 1,531
  • 12
  • 14
  • That's what I wanted to know! Thanks! – gecko655 Jul 05 '16 at 03:21
  • So, is there no means to pass the body of the function from the modules? If not, do I have to re-execute the server node every time I changed the client code? – gecko655 Jul 05 '16 at 03:26
  • 1
    I think this is a limitation with `Agent` and `Task`. But I know you can accomplish something like this with `GenServer` behaviour. The following Stack Overflow question explains how to do it in Erlang, but it is pretty much the same in Elixir: http://stackoverflow.com/questions/1840717/achieving-code-swapping-in-erlangs-gen-server – Alex de Sousa Jul 05 '16 at 13:49
  • OK. So I cannot pass the body of anonymous functions and I have to reload the code in the callee node. It's weird and troublesome for me. – gecko655 Jul 06 '16 at 02:44
  • You can generate a quoted function, send it to the remote `TaskSupervisor` and then use `Code.eval_quoted/3` to run the function in the remote `Task`. I think that would accomplish what you are looking for. It's just a thought. I think it would work, but I haven't tested it. – Alex de Sousa Jul 06 '16 at 09:55