Edited at

Erlang VMのnode間通信で無名関数がうまく渡せなかった話

More than 3 years have passed since last update.


背景

Erlangの仮想マシン(VM)にはnodeとprocessの概念があり、

各nodeにErlang VMが立ち上がり、Erlang VM上で複数のprocessが動作します。

各nodeは複数サーバーに分散して配置していてもよく、Supervisorという、node間通信を行なうモジュールを利用して、nodo間の同期を取ったり処理を分散させています。

例えば、以下のようにサーバーnodeを立ち上げます。

$ iex --sname server --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: Server.DistSupervisor)
{:ok, #PID<0.101.0>}

ここでは、"server"という名前のnodeを立て、"Server.DistSupervisor"という識別子でnode間通信を受信します。

また、起動時にはcookieという文字列を指定し、同じcookieを持つnodeのみがnode間通信できるようになります。

クライアントnodeからは以下の様に通信します。

$ 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( {Server.DistSupervisor, :'server@hostname'}, fn -> "test" end) |>
...(dbclient@hostname)1> Task.await |>
...(dbclient@hostname)1> IO.inspect
"test"

ここでは、サーバーnodeに

1. fn -> "test" endという関数を渡す

2. サーバーノードでの関数の実行が終わるのを待って結果を受け取る

3. 結果を出力

ということをしており、正しく"test"という文字列が返ってきています。

次に、対話環境ではなくスクリプトから実行した時のことを考えます。

defmodule RpcTest do

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

$ elixir --sname client --cookie a -S mix run -e RpcTest.run

"test"

スクリプト上でも正しく通信することができました。


発生した問題

以上のnode間通信を試した後、node間で渡したい関数の中身を変更することを考えます。

ここでは、

fn -> "test" end

から

fn -> "test test" end

に変更することを考えます。

そうすると、対話環境上では正しく動作しました。

iex(dbclient@hostname)2> Task.Supervisor.async( {Server.DistSupervisor, :'server@hostname'},  fn -> "test test" end) |> 

...(dbclient@hostname)2> Task.await |>
...(dbclient@hostname)2> IO.inspect
"test test"

しかし、スクリプト上では正しく動作せず、BadFunctionErrorが投げられてしまいます。

defmodule RpcTest do

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

$ elixir --sname client --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

ただ単に、サーバーに無名関数fn -> "test test" endを渡しているだけなのに、対話環境上での動作とスクリプト上での動作が異なってしまいました。


原因

対話環境上で定義した無名関数と、スクリプト上で定義した無名関数では持っている情報が異なるのだそうです。

対話環境上での無名関数は以下のように表現されています。

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]

詳しく説明はしませんが、env:以下の要素に{:string, 0, 'test'}と入っていることなどから、

無名関数はこの中に定義されていることがわかります。

スクリプト上での無名関数もどのように表現されるか見てみます。

defmodule FunInfo do

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

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]

先ほどと異なり、FunInfoというモジュール名が入っており、関数の実体と思われる部分が消えています。

モジュール内で定義した無名関数はモジュール名とuniq: 51893957new_uniq: <<98, 250, 216, 183, 54, 136, 109, 221, 200, 243, 9, 84, 249, 185, 187, 173>>の部分で一意に表現されるため、関数の実体は無名関数の中で表現されていません。

node間通信においても、関数の実体ではなくこれらを渡して、他のノード内で関数を実行しているそうです。

スクリプト上で変更を行なったのちサーバーnodeを再起動せずにクライアントnodeからnode間通信で関数を渡すと、無名関数の実体ではなく、サーバーnode上ではまだ定義されていない識別子を持った関数が渡ってしまいます。

その結果、そのような関数は存在しないということでエラーが投げられていたようです。


解決策

サーバーnodeを再起動するか、変更のあったソースファイルをサーバーnode内で再読み込みすることで解決します。

対話環境上での無名関数の表現と、スクリプト上での無名関数の表現が異なる点は、現時点ではまだ謎のままです。

(elixir-langのgithub issueにでも質問しに言ったら答えてくれるかな…)

対話環境上での動作とスクリプト上での動作が異なるのが大変気持ち悪いのですが、きっと何かの制約でこうなってしまったんでしょう。

また、関数の実体を持つ無名関数をソースファイル内で定義してnode間通信で渡すのがごく自然な実装だと思いますが、

現在はそのような方法は用意されていないようです。

悲しい。


出典

この記事の内容はすべて http://stackoverflow.com/questions/38137106/elixirs-inter-node-function-call-will-fail-after-changing-code を基にしています。