LoginSignup
11
4

More than 5 years have passed since last update.

Elixirの関数を別ノードに転送して実行する

Last updated at Posted at 2018-11-17

fukuoka.ex代表のpiacereです
今回もご覧いただいて、ありがとうございます:bow:

Elixirのメッセージパッシングには、関数を指定して送信する機能があることを、こないだ知りました

これを使って、メッセージパッシング経由で、別ノードでの関数を呼び出してみる検証を行います

なお、今回の記事は、ノード間をいったり来たりしながら、コードを書いたり、モジュールを動かしたりするので、書いている通りに実行しないと、すぐに迷子になるので、読み飛ばしながら実行するクセのある方は、注意しながら試してください
(思った以上に、書いてある通りにやらない方が多いこと、最近、分かってきました…)

特に、コードに書いてある「ノードxx側」は、キチンと確認しながら、進めていきましょう

2つのノードを接続する

シェルを2枚、起動し、各シェルで各々Elixir PJを作成します

ノード1側
mix new node1
cd node1
ノード2側
mix new node2
cd node2

シェルを2枚、並べて表示します
image.png

次に、各々のiexをノード名付きで起動します

ノード1側
iex --name elixir_node1@127.0.0.1 -S mix
ノード2側
iex --name elixir_node2@127.0.0.1 -S mix

立ち上げた時点では、この2つのノードは、接続していないため、以下コマンドで確認しても、ノードのリストは空です

ノード1側
iex> Node.list
[]

これを、以下コマンドで接続します

ノード1側
iex> Node.connect( :"elixir_node2@127.0.0.1" )
[]

すると、ノードのリストに出てくるようになります

ノード1側
iex> Node.list
[:"elixir_node2@127.0.0.1"]

反対に、ノード2側からも、ノード1に接続していることが分かります

ノード2側
iex> Node.list
[:"elixir_node1@127.0.0.1"]

ノード間で通信する

それでは、ノード1を送信側、ノード2を受信側として、ノード間の通信を行ってみましょう

まずノード2のiexのプロセスで、受信用の待受をします

ノード2側
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」に向けて、メッセージ送信します

ノード1側
iex> to_pid = :global.whereis_name( :endpoint_node1_test )
#PID<14973.110.0>
iex> send( to_pid, "send from Node1" )

すると、ノード2側で、「received」と表示されます

ノード2側
iex> receive do _ -> IO.puts( "received" ) end
received
:ok

こんな感じで、Elixirでは、カンタンにメッセージパッシングが実現できます、素晴らしい

なお待受側は、1度、処理を行うと、その後は、普通に終了するため、再度メッセージ送信しても、ノード2は、もう反応しなくなります(上記の:okで終了しています)

ノード1側
iex> send( :global.whereis_name( :endpoint_node1_test ), "send from Node1(2nd time)" )

ただし、メッセージが送られたことそのものは、プロセス(≒ここではiexを起動しているプロセス)で保持されているため、ノード2の待受を再度行うと、即座に待受処理が走ります

ノード2側
iex> receive do _ -> IO.puts( "received(2nd time)" ) end
received(2nd time)
:ok

「sendで投げ、プロセスにキューイングされ、receiveでキューから取り出す」という機構が、キチンと機能していることが分かります

さて、受信はしているものの、送信側から送られたメッセージは無視しているため、メッセージも受け取り、受信側で表示してみましょう

ノード2側
iex> receive do message -> IO.puts( "received(3rd time): #{ message }" ) end

再度、送信します

ノード1側
iex> send( :global.whereis_name( :endpoint_node1_test ), "send from Node1(2nd time)" )

受け取ったメッセージが表示されるようになりました

ノード2側
received(3rd time): send from Node1(3rd time)
:ok

サーバプロセス化(受信し続ける)

上記の処理を、モジュール化しながら、待受側を1度の受信で終わらない処理に変え、iexとは別のサーバプロセスで起動できるように変えます

以下の各ノードのモジュールファイルを作成してください

lib/node1.ex ※ノード1側
defmodule Node1 do
    def send( message ) do
        to_pid = :global.whereis_name( :endpoint_node1 )
        IO.inspect( to_pid )

        send( to_pid, message )
    end
end
lib/node2.ex ※ノード2側
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()で待受終了できます

 ※ノード2側
iex> recompile
iex> Node2.listen

送信側のNode1モジュールを使って、送信します

 ※ノード1側
iex> recompile
iex> Node1.send( "send from Node1.send()" )

ノード2側で受信が確認できます

 ※ノード2側
"send from Node1.send()"

字面だと分かりにくいかも知れないので、ノード1とノード2の処理結果を画面で見ると、こんな感じになります
image.png

なお、こういうクライアント/サーバを作るときは、「GenServer」を使うとラクできて良いですが、内部構造がラップされて送受信が分かりにくくなるので、今回は敢えて使わず、プリミティブなsend/receiveで作っています

Elixirの関数を別ノードに転送して実行…されない…

さて、いよいよ本題、Elixirの関数を別ノードに転送して実行してみます

まず、ノード1側に、転送する関数のモジュールを作ります

lib/remote.ex ※ノード1側
defmodule Remote do
    def procedure() do
        IO.puts( "PID=#{ inspect( self() ) }: I'm Remote.procedure()" )
    end
end

「&」とアリティを指定して、関数を送信します

 ※ノード1側
iex> recompile
iex> Node1.send( &Remote.procedure/0 )

ノード2には、関数がインスペクトされます

 ※ノード2側
&Remote.procedure/0

関数が渡っていることが確認できたので、受信側で、受け取った関数を呼び出す改修を行います

lib/node2.ex ※ノード2側
defmodule Node2 do
    
    def exeute() do
        receive do
            message -> 
                IO.inspect( message )
                message.()
        end
        exeute()
    end
    

ノード2の待受プロセスを再起動します

 ※ノード2側
iex> recompile
iex> Node2.stop
iex> Node2.listen

再度、関数を送信します

 ※ノード1側
iex> recompile
iex> Node1.send( &Remote.procedure/0 )

すると、「関数が存在しない」というエラーが出ます

 ※ノード2側
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で未定義の関数を呼び出してみましょう

 ※ノード2側
iex> Node2.listen
 ※ノード1側
iex> Node1.send( &Non.exist/0 )

すると、ノード2では、やはりエラーが出ますが、ノード1では、特にエラーは出ません

つまり、「&」とアリティで指定する関数は、その時点では定義済みか確認されず、エラーも出ない、ということです…動的な関数バインディングですね

送信しているのは、「関数そのもの」では無く、「呼び出す関数」ということです

実行ノードにモジュールを転送すれば実行できる

ノード1にモジュールを置いてても、ノード2で実行することはできないので、ノード2にモジュールを転送してみましょう

以下コマンドで、接続されているノード全てに、モジュールを配布できます

 ※ノード1側
iex> nl( Remote )
{:ok, [{:"elixir_node2@127.0.0.1", :loaded, Remote}]}

実際に配布されたか、ノード2で単品実行してみます

 ※ノード2側
iex> Remote.procedure
PID=#PID<0.110.0>: I'm Remote.procedure()
:ok

それでは、関数呼び出しを送信してみましょう

 ※ノード1側
iex> Node1.send( &Remote.procedure/0 )
 ※ノード2側
&Remote.procedure/0
PID=#PID<0.234.0>: I'm Remote.procedure()

うまくいきました

画面で見ると、こんな感じになりました
image.png

オマケ:リモート端末に侵入した気分を味わう

以下のような送信を行うと、まるでリモート端末に侵入した気分が味わえると思います(実際に動かした方だけが体験できるよう、結果は伏せておきます):kissing_smiling_eyes:

 ※ノード1側
iex> recompile
iex> Node1.send( &h/0 )
 ※ノード1側
iex> recompile
iex> Node1.send( &pwd/0 )
 ※ノード1側
iex> recompile
iex> Node1.send( &ls/0 )

終わり

Elixirの関数を別ノードに転送して実行してみました

ちなみに、試した結果、関数自体が転送される訳では無いことが分かりましたので、関数は、最初から受信側ノード(今回だとノード2)で定義すればOKです

それと、メッセージパッシングで双方向の通信を行うこともできます

  • 送信側は、sendした後にreceiveで待受にする
  • 受信側は、receiveハンドラ内で送信元PIDにsendする

余力があれば、チャレンジしてみてください

p.s.「いいね」よろしくお願いします

ページ左上の image.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

11
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4