fukuoka.ex代表のpiacereです
今回もご覧いただいて、ありがとうございます
Elixirのメッセージパッシングには、関数を指定して送信する機能があることを、こないだ知りました
これを使って、メッセージパッシング経由で、別ノードでの関数を呼び出してみる検証を行います
なお、今回の記事は、ノード間をいったり来たりしながら、コードを書いたり、モジュールを動かしたりするので、書いている通りに実行しないと、すぐに迷子になるので、読み飛ばしながら実行するクセのある方は、注意しながら試してください
(思った以上に、書いてある通りにやらない方が多いこと、最近、分かってきました…)
特に、コードに書いてある「ノードxx側」は、キチンと確認しながら、進めていきましょう
2つのノードを接続する
シェルを2枚、起動し、各シェルで各々Elixir PJを作成します
mix new node1
cd node1
mix new node2
cd node2
次に、各々のiexをノード名付きで起動します
iex --name elixir_node1@127.0.0.1 -S mix
iex --name elixir_node2@127.0.0.1 -S mix
立ち上げた時点では、この2つのノードは、接続していないため、以下コマンドで確認しても、ノードのリストは空です
iex> Node.list
[]
これを、以下コマンドで接続します
iex> Node.connect( :"elixir_node2@127.0.0.1" )
[]
すると、ノードのリストに出てくるようになります
iex> Node.list
[:"elixir_node2@127.0.0.1"]
反対に、ノード2側からも、ノード1に接続していることが分かります
iex> Node.list
[:"elixir_node1@127.0.0.1"]
ノード間で通信する
それでは、ノード1を送信側、ノード2を受信側として、ノード間の通信を行ってみましょう
まずノード2のiexのプロセスで、受信用の待受をします
iex> pid = self()
#PID<0.110.0>
iex> :global.register_name( :endpoint_node1_test, pid )
iex> receive do _ -> IO.puts( "received" ) end
最初は、自身(≒iex)のプロセスIDを取得しており、その次で、自プロセスIDを「:endpoint_node1_test」という名前で、ノード間の名前検索を可能とする登録を行っています
最後のreceiveにて、待受状態に入り、メッセージが来たら、「received」と表示します
さて、受信側の準備が出来たので、今度はノード1から、「:endpoint_node1_test」に向けて、メッセージ送信します
iex> to_pid = :global.whereis_name( :endpoint_node1_test )
#PID<14973.110.0>
iex> send( to_pid, "send from Node1" )
すると、ノード2側で、「received」と表示されます
iex> receive do _ -> IO.puts( "received" ) end
received
:ok
こんな感じで、Elixirでは、カンタンにメッセージパッシングが実現できます、素晴らしい
なお待受側は、1度、処理を行うと、その後は、普通に終了するため、再度メッセージ送信しても、ノード2は、もう反応しなくなります(上記の:okで終了しています)
iex> send( :global.whereis_name( :endpoint_node1_test ), "send from Node1(2nd time)" )
ただし、メッセージが送られたことそのものは、プロセス(≒ここではiexを起動しているプロセス)で保持されているため、ノード2の待受を再度行うと、即座に待受処理が走ります
iex> receive do _ -> IO.puts( "received(2nd time)" ) end
received(2nd time)
:ok
「sendで投げ、プロセスにキューイングされ、receiveでキューから取り出す」という機構が、キチンと機能していることが分かります
さて、受信はしているものの、送信側から送られたメッセージは無視しているため、メッセージも受け取り、受信側で表示してみましょう
iex> receive do message -> IO.puts( "received(3rd time): #{ message }" ) end
再度、送信します
iex> send( :global.whereis_name( :endpoint_node1_test ), "send from Node1(2nd time)" )
受け取ったメッセージが表示されるようになりました
received(3rd time): send from Node1(3rd time)
:ok
サーバプロセス化(受信し続ける)
上記の処理を、モジュール化しながら、待受側を1度の受信で終わらない処理に変え、iexとは別のサーバプロセスで起動できるように変えます
以下の各ノードのモジュールファイルを作成してください
defmodule Node1 do
def send( message ) do
to_pid = :global.whereis_name( :endpoint_node1 )
IO.inspect( to_pid )
send( to_pid, message )
end
end
defmodule Node2 do
def listen() do
server_pid = spawn( Node2, :exeute, [] )
IO.inspect( server_pid )
:global.register_name( :endpoint_node1, server_pid )
end
def exeute() do
receive do
message -> IO.inspect( message )
end
exeute()
end
def stop() do
Process.exit( :global.whereis_name( :endpoint_node1 ), :normal )
:global.unregister_name( :endpoint_node1 )
end
end
Node2.execute()の末尾で、execute()自身を呼び出す処理を入れることで、1度の待受で終わらず、繰り返し受信できるようになります(この処理は、再帰処理のように見えますが、実際は、末尾再帰最適化が行われ、再帰では無く、ループとしてコンパイルされます)
受信側のNode2モジュールは、Node2.listen()で待受開始し、Node2.stop()で待受終了できます
iex> recompile
iex> Node2.listen
送信側のNode1モジュールを使って、送信します
iex> recompile
iex> Node1.send( "send from Node1.send()" )
ノード2側で受信が確認できます
"send from Node1.send()"
字面だと分かりにくいかも知れないので、ノード1とノード2の処理結果を画面で見ると、こんな感じになります
なお、こういうクライアント/サーバを作るときは、「GenServer」を使うとラクできて良いですが、内部構造がラップされて送受信が分かりにくくなるので、今回は敢えて使わず、プリミティブなsend/receiveで作っています
Elixirの関数を別ノードに転送して実行…されない…
さて、いよいよ本題、Elixirの関数を別ノードに転送して実行してみます
まず、ノード1側に、転送する関数のモジュールを作ります
defmodule Remote do
def procedure() do
IO.puts( "PID=#{ inspect( self() ) }: I'm Remote.procedure()" )
end
end
「&」とアリティを指定して、関数を送信します
iex> recompile
iex> Node1.send( &Remote.procedure/0 )
ノード2には、関数がインスペクトされます
&Remote.procedure/0
関数が渡っていることが確認できたので、受信側で、受け取った関数を呼び出す改修を行います
defmodule Node2 do
…
def exeute() do
receive do
message ->
IO.inspect( message )
message.()
end
exeute()
end
…
ノード2の待受プロセスを再起動します
iex> recompile
iex> Node2.stop
iex> Node2.listen
再度、関数を送信します
iex> recompile
iex> Node1.send( &Remote.procedure/0 )
すると、「関数が存在しない」というエラーが出ます
19:22:32.976 [error] Error in process #PID<0.225.0> on node :"elixir_node2@127.0.0.1" with exit value:
{:undef, [{Remote, :procedure, [], []}, {Node2, :exeute, 0, [file: 'lib/node2.ex', line: 13]}]}
ふむ、どうやら関数自体が転送される訳では無いようです
関数は指定時には評価されていない
試しに、ノード2の待受プロセスを起動し直して、ノード1で未定義の関数を呼び出してみましょう
iex> Node2.listen
iex> Node1.send( &Non.exist/0 )
すると、ノード2では、やはりエラーが出ますが、ノード1では、特にエラーは出ません
つまり、「&」とアリティで指定する関数は、その時点では定義済みか確認されず、エラーも出ない、ということです…動的な関数バインディングですね
送信しているのは、「関数そのもの」では無く、「呼び出す関数」ということです
実行ノードにモジュールを転送すれば実行できる
ノード1にモジュールを置いてても、ノード2で実行することはできないので、ノード2にモジュールを転送してみましょう
以下コマンドで、接続されているノード全てに、モジュールを配布できます
iex> nl( Remote )
{:ok, [{:"elixir_node2@127.0.0.1", :loaded, Remote}]}
実際に配布されたか、ノード2で単品実行してみます
iex> Remote.procedure
PID=#PID<0.110.0>: I'm Remote.procedure()
:ok
それでは、関数呼び出しを送信してみましょう
iex> Node1.send( &Remote.procedure/0 )
&Remote.procedure/0
PID=#PID<0.234.0>: I'm Remote.procedure()
うまくいきました
オマケ:リモート端末に侵入した気分を味わう
以下のような送信を行うと、まるでリモート端末に侵入した気分が味わえると思います(実際に動かした方だけが体験できるよう、結果は伏せておきます)
iex> recompile
iex> Node1.send( &h/0 )
iex> recompile
iex> Node1.send( &pwd/0 )
iex> recompile
iex> Node1.send( &ls/0 )
終わり
Elixirの関数を別ノードに転送して実行してみました
ちなみに、試した結果、関数自体が転送される訳では無いことが分かりましたので、関数は、最初から受信側ノード(今回だとノード2)で定義すればOKです
それと、メッセージパッシングで双方向の通信を行うこともできます
- 送信側は、sendした後にreceiveで待受にする
- 受信側は、receiveハンドラ内で送信元PIDにsendする
余力があれば、チャレンジしてみてください
p.s.「いいね」よろしくお願いします
ページ左上の や のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!