13
12

More than 5 years have passed since last update.

Elixir: AgentとNodeを組み合わせて使う

Posted at

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を返すような関数を用意してやると...

two@vagrant-centos7
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"
one@vagrant-centos7
iex(one@vagrant-centos7)3> Agent.get(Message3, &(&1))
"create by node two"

片方は名前で、もう片方はpidでという形になりますが, 同様Agentから値を取ることができます.
更新もためしてみると,

one@vagrant-centos7
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"
two@vagrant-centos7
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プロセスを立ち上げ

one@vagrant-centos7
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の値を取得してみると...

two@vagrant-centos7
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"

無事, 値を取得することができました.
値の更新もためしてみると

two@vagrant-centos7
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"
one@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

13
12
0

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
13
12