Agent
例えばHTTPのセッション情報のように, 複数の処理にまたいで, 結果を保存しておいて
次の処理の際に引き続き使いたいというような要件は, いくらでもあると思われます.
DBを使ってこれらの値を保存しておくこともできるのですが, Elixirにはもっと手軽に使えるAgentというモジュールが用意されています.
使い方は, start
関数で初期化し
iex(4)> {:ok, count} = Agent.start(fn -> 0 end)
{:ok, count} = Agent.start(fn -> 0 end)
{:ok, #PID<0.82.0>}
get
関数で保存された値を取得し
iex(5)> Agent.get(count, fn n -> n end)
Agent.get(count, fn n -> n end)
0
update
関数で値の更新を行います.
iex(6)> Agent.update(count, fn n -> n + 1 end)
Agent.update(count, fn n -> n + 1 end)
:ok
iex(7)> Agent.get(count, fn n -> n end)
Agent.get(count, fn n -> n end)
1
どの関数も, 保存されている/保存したい値をそのまま指定するのではなく, 関数で指定するようになっているためちょっと見慣れないかもしれませんが, それは慣れるしかないと思います.
なお, get
, update
に渡す関数の第一引数にはAgentが現在保存している値が渡されます.
start
関数の返り値がプロセスのIDであることからもわかるように, 生成されるAgentも独立したプロセスとなっているようです.
また, start
関数の引数として名前を渡してやることで, 実行時に決まるpidではなく, 固定のシンボルでAgentにアクセスできるようになります.
iex(8)> Agent.start(fn -> 100 end, name: Counter)
Agent.start(fn -> 100 end, name: Counter)
{:ok, #PID<0.90.0>}
iex(9)> Agent.get(Counter, &(&1))
Agent.get(Counter, &(&1))
100
ちなみに, ここで指定する名前をCounterではなくcounterにするとエラーになりました.
どうも大文字から始まる未定義のシンボルは自動的に:Elixirというアトムと紐付けられて, 正式なシンボルとして登録される
みたいなことが書かれているのを読んだのですが詳細はわかりません.
Node
Nodeとは, Elixirが動いているErlangVMのことです.
つまり
$ iex
Erlang/OTP 18 [erts-7.2] [source-e6dd627] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
とコマンドラインからIEXを立ち上げた際に生成されるプロセス郡が, 一つのNodeとなるのです.
Nodeは名前があり, 起動時に指定することもできます.
$ iex
Erlang/OTP 18 [erts-7.2] [source-e6dd627] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Node.self
:nonode@nohost #<-- デフォルトのNode名
iex(2)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
$ iex --sname one
Erlang/OTP 18 [erts-7.2] [source-e6dd627] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(one@vagrant-centos7)1> Node.self
:"one@vagrant-centos7" #<-- VM起動時に指定した名前が設定されている
この名前を使って, 異なる複数のNodeを接続することができます.
[vagrant@vagrant-centos7 phoenix_sample]$ iex --sname two
Erlang/OTP 18 [erts-7.2] [source-e6dd627] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.3.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(two@vagrant-centos7)1> Node.self
:"two@vagrant-centos7"
iex(two@vagrant-centos7)2> Node.list
[] #<-- 接続中のノードリストは空
iex(two@vagrant-centos7)3> Node.connect :"one@vagrant-centos7"
true
iex(two@vagrant-centos7)4> Node.list
[:"one@vagrant-centos7"] #<-- :"one@vagrant-centos7"という名前のノードと接続している
このように他のNodeと接続状態にすることで, ノードをまたいだプロセスへの生成やアクセスなどを行うことができるようになります.
異なるNodeで生成されたAgentにアクセスすることはできるか?
異なるNodeで生成されたAgentへ、別Nodeからアクセスできるか試してみます.
まずひとつ目のNodeでAgentを生成して, 値を設定します.
iex(one@vagrant-centos7)1> Node.self
:"one@vagrant-centos7"
iex(one@vagrant-centos7)2> Agent.start(fn -> "on one@vagrant-centos7" end, name: Message)
{:ok, #PID<0.66.0>}
iex(one@vagrant-centos7)3> Agent.get(Message, &(&1))
"on one@vagrant-centos7"
次に, 別のNodeを立ち上げて, 上記のAgentにアクセスしてみようとすると...
iex(two@vagrant-centos7)1> Node.list
[]
iex(two@vagrant-centos7)2> Node.connect(:"one@vagrant-centos7")
true
iex(two@vagrant-centos7)3> Node.list
[:"one@vagrant-centos7"]
iex(two@vagrant-centos7)4> Agent.get(Message, &(&1))
** (exit) exited in: GenServer.call(Message, {:get, #Function<6.54118792/1 in :erl_eval.expr/5>}, 5000)
** (EXIT) no process
(elixir) lib/gen_server.ex:564: GenServer.call/3
このように, 単純に名前を指定してのアクセスはできません.
しかし
defmodule RemoteAgent do
def start(node, sender, name) do
Node.spawn(node, RemoteAgent, :spwan_agent, [sender, name])
receive do
{:ok, agent_id} -> agent_id
end
end
def spwan_agent(sender, name) do
{:ok, agent} = Agent.start(fn -> "create by node two" end, name: name)
send sender, {:ok, agent}
end
end
このような, 異なるNodeでAgentを生成して, そのpidを返すような関数を用意してやると...
iex(two@vagrant-centos7)4> pid = RemoteAgent.start :"one@vagrant-centos7", self, Message3
# PID<8246.93.0>
iex(two@vagrant-centos7)5> Agent.get(pid, &(&1))
"create by node two"
iex(one@vagrant-centos7)3> Agent.get(Message3, &(&1))
"create by node two"
片方は名前で、もう片方はpidでという形になりますが, 同様Agentから値を取ることができます.
更新もためしてみると,
iex(one@vagrant-centos7)4> Agent.update(Message3, fn n -> "updated on one" end)
:ok
iex(one@vagrant-centos7)5> Agent.get(Message3, &(&1))
"updated on one"
iex(two@vagrant-centos7)6> Agent.get(pid, &(&1))
"updated on one"
こちらも問題ないようです.
もう少し簡潔に書きなおしてみる
ここまでで, 異なるNodeで動いているAgentにもやりようによってはアクセスできることがわかりましたが, どうも書き方が煩雑な点が気になります.
もっと簡潔に, データを取りたいときは
RemoteAgent.get()
と呼び出し, データを保存したいときは
RemoteAgent.update("new value")
と呼び出せるようにしたいところです.
ということで, そんな感じにアクセスできるように書き直したバージョンが↓のコード.
defmodule RemoteAgent do
@name :agent_register
def start(init \\ "") do
{:ok, agent_id} = Agent.start(fn -> init end)
pid = spawn(RemoteAgent, :agent, [agent_id])
:global.register_name @name, pid
:ok
end
def agent(agent_id) do
receive do
{client, :get} ->
send client, {:ok, Agent.get(agent_id, &(&1))}
agent(agent_id)
{client, :update, new_value} ->
Agent.update(agent_id, fn n -> new_value end)
send client, {:ok, Agent.get(agent_id, &(&1))}
agent(agent_id)
end
end
def get() do
send :global.whereis_name(@name), {self, :get}
receive do
{:ok, value} -> value
end
end
def update(value) do
send :global.whereis_name(@name), {self, :update, value}
receive do
{:ok, _} -> :ok
end
end
end
早速試してみましょう.
まずサーバNodeにてRemoteAgentプロセスを立ち上げ
iex(one@vagrant-centos7)4> RemoteAgent.start("this is init string")
:ok
iex(one@vagrant-centos7)5> RemoteAgent.get
"this is init string"
次にクライアントNodeからサーバNodeに接続して, RemoteAgentの値を取得してみると...
iex(two@vagrant-centos7)2> Node.list
[]
iex(two@vagrant-centos7)2> RemoteAgent.get #<-- サーバNodeへ接続前にアクセスしようとすると...
** (ArgumentError) argument error
:erlang.send(:undefined, {#PID<0.63.0>, :get})
lib/remote_agent.ex:29: RemoteAgent.get/0
iex(two@vagrant-centos7)3> Node.connect(:"one@vagrant-centos7")
true
iex(two@vagrant-centos7)4> Node.list
[:"one@vagrant-centos7"]
iex(two@vagrant-centos7)5> RemoteAgent.get
"this is init string"
無事, 値を取得することができました.
値の更新もためしてみると
iex(two@vagrant-centos7)6> RemoteAgent.update("update from two@vagrant-centos7")
:ok
iex(two@vagrant-centos7)7> RemoteAgent.get
"update from two@vagrant-centos7"
iex(one@vagrant-centos7)6> RemoteAgent.get
"update from two@vagrant-centos7"
両方のNodeで値が更新されていることが確認できました.
すこしコードの解説をしますと, このコードではデータを保存するAgentを管理するプロセスを一つ用意し, 任意のNodeからのアクセスをすべてこの管理プロセスで扱うようにしています.
そして, この管理プロセスを:global
モジュールに登録しておくことで, 任意のNodeから一意に取得できるようになっています.
(この:global
モジュールが何者かというと, プロセスに対してグローバルな名前をつけて管理するErlangのモジュールのようです. もっともErlangについてはまだ語れませんので, ここではそういうものがあると思ってください)
管理プロセスの登録/取得部分をモジュール内部に隠蔽することによって, モジュールを使用するクライアントに対しては簡潔なインタフェイスを提供でき
また, このモジュールのクライアントが増えても何も問題が起きないようになっている, と思います.
ちなみに, デザインパターンの一つにMediatorパターンというものがあるのですが, これも一種のMediatorパターンになるんじゃないかと思います.
Agentを使わない実装
Elixirでは, モジュールは状態を持ちませんが, プロセスは状態を持つことができます.
RemoteAgentモジュールの中で値(状態)を保存するためにAgentを使っているわけですが
実際のところ, Agentを使わなくてもRemoteAgent(を走らせているプロセス)に値を持たせることはできるので, そう書きなおしてみます.
defmodule RemoteAgent2 do
@name :agent_register2
def start(init \\ "") do
pid = spawn(RemoteAgent2, :agent, [init])
:global.register_name @name, pid
:ok
end
def agent(state) do
receive do
{client, :get} ->
send client, {:ok, state}
agent(state)
{client, :update, new_state} ->
send client, {:ok, new_state}
agent(new_state)
end
end
def get() do
send :global.whereis_name(@name), {self, :get}
receive do
{:ok, state} -> state
end
end
def update(new_state) do
send :global.whereis_name(@name), {self, :update, new_state}
receive do
{:ok, _} -> :ok
end
end
end
使い方は変わらず, 記述もより簡潔になったと思います.
まとめ
異なるNodeをまたいでAgentを使えないかと思って書きだした結果, Agentが必要なくなるという結果に終わってしまいましたが, 異なるノードのプロセス間から共通の値へのアクセスをできる仕組みはできました.
ですが, 今のままでは保存できるのは単一の値だけですし, RemoteAgentそのものも一つの系の中に一つしか存在できなくなってしまっています.
実際に使っていくには, 任意の型(リストやマップ)の保存にも対応しておく必要がありますし, 複数のRemoteAgentを名前をつけて生成できるようにする必要もあるかと思います.
またいつか, こちらの機能の改修などまた書きたいと思います.
参考
Elixir.Node: http://elixir-lang.org/docs/stable/elixir/Node.html
Elixir.Agent: http://elixir-lang.org/docs/stable/elixir/Agent.html
Erlang.Global: http://erlang.org/doc/man/global.html
Mediator パターン: http://www.ie.u-ryukyu.ac.jp/~e085739/java.it.18.html