Phoenix Channelで作る最先端Webアプリ - topic-subtopic編 - Qiita
Phoenix Channelで作る最先端Webアプリ - ETS編 - Qiita
Phoenix Channelで作る最先端Webアプリ - Fault Tolerance編 - Qiita
Phoenix Channelで作る最先端Webアプリ - 地図(Geo)拡張編 - Qiita
Phoenix Channelで作る最先端Webアプリ - DynamicSupervisor編 - Qiita
Phoenix Channelで作る最先端Webアプリ - Elixier Application編 - Qiita
Phoenix Channelで作る最先端Webアプリ - Reactチャット編 - Qiita
今回の技術的なポイントは以下の通りです。
1.Elixir ApplicationのFault Toleranceを確認する
今回は、今まで作成したシステムのFault Tolerance性を確認します。確認のために少しコードを追加しますが、基本的にはコードの追加はありません。特にListenTweets.ListenerSupervisorとListenTweets.Listenerに注目します。ListenTweets.ListenerSupervisorはDynamicSupervisorとして動的にListenTweets.Listenerプロセスを生成します。ListenTweets.Listenerプロセスが死んだらrestartします。この状況を確認します。本記事はFault Tolerance性の事実を確認するだけでなく、iex上で確認する方法も示します。どちらも重要ですね。
まず本質とは関係ありませんが、pid_from_key/1の定義をdefpからdefに変更し、iexシェルから使えるようにします。確認作業を効率的に行うための処置です。
defmodule ListenTweets.ListenerSupervisor do
use DynamicSupervisor
#
def pid_from_key(key) do
key
|> Listener.via_tuple()
|> GenServer.whereis()
end
#
1.restart: :temporaryの場合
次にlistener.exを変更します。child_specをrestart: :temporaryに変更し、プロセスが停止してもSupervisorが再起動しないように指定します。また、iexシェルからこのプロセスにメッセージを送り、40を引数で割るように指示できるようにします。ゼロで割ればクラッシュするのでFault Tolerance性を確認できます。メッセージはhandle_info({ :div, a}, key)で受けます。
defmodule ListenTweets.Listener do
use GenServer, restart: :temporary #child_spec
#
def handle_info({ :div, a}, key) do
ans = 40 / a
IO.puts ("ans=#{ans}")
{ :noreply, key }
end
#
end
次にコマンドでReactChat Applicationを立ち上げます。ブラウザを開き「さんど」でjoinします。キーワード「らーめん」を登録します。
iex -S mix phx.server
「らーめん」でListenTweets.Listenerプロセスが起動されているのを確認します。
iex(2)> alias ListenTweets.{Listener,ListenerSupervisor}
[ListenTweets.Listener, ListenTweets.ListenerSupervisor]
iex(3)> Supervisor.count_children(ListenerSupervisor)
%{active: 1, specs: 1, supervisors: 0, workers: 1}
iex(4)> via=Listener.via_tuple("らーめん")
{:via, Registry, {Registry.Listener, "らーめん"}}
iex(5)> state_data = :sys.get_state(via)
"らーめん"
ListenTweets.Listenerプロセスのpidを取得して、メッセージを送ってみます。
iex(6)> pid=ListenerSupervisor.pid_from_key("らーめん")
#PID<0.410.0>
iex(7)> send pid, {:div,2}
{:div, 2}
iex(8)> ans=20.0
割り算の正しい結果を表示しました。次にゼロ割を行いクラッシュを発生させます。
iex(9)> send pid, {:div,0}
{:div, 0}
iex(10)> [error] GenServer {Registry.Listener, "らーめん"} terminating
** (ArithmeticError) bad argument in arithmetic expression
(listen_tweets) lib/listen_tweets/listener.ex:48: ListenTweets.Listener.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:div, 0}
State: "らーめん"
次のコマンドでプロセスが再起動されていないことが確認できます。 Registryの登録自体は解除されていないようですね。しかし状態を表示しようとするとプロセスが無いと言われてしまいます。
iex(11)> Supervisor.count_children(ListenerSupervisor)
%{active: 0, specs: 0, supervisors: 0, workers: 0}
iex(12)> via=Listener.via_tuple("らーめん")
{:via, Registry, {Registry.Listener, "らーめん"}}
iex(13)> state_data = :sys.get_state(via)
** (exit) exited in: :sys.get_state({:via, Registry, {Registry.Listener, "らーめん"}})
** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
(stdlib) sys.erl:303: :sys.send_system_msg/2
(stdlib) sys.erl:112: :sys.get_state/1
2.restart: :transientの場合
次にrestart: :transientの場合を見ます。これはクラッシュ時にのみ再起動するものでした。
次にコマンドでReactChat Applicationを立ち上げます。ブラウザを開き「さんど」でjoinします。キーワード「らーめん」を登録します。
iex -S mix phx.server
「らーめん」でListenTweets.Listenerプロセスが起動されているのを確認します。
iex(2)> alias ListenTweets.{Listener,ListenerSupervisor}
[ListenTweets.Listener, ListenTweets.ListenerSupervisor]
iex(3)> Supervisor.count_children(ListenerSupervisor)
%{active: 1, specs: 1, supervisors: 0, workers: 1}
iex(4)> via=Listener.via_tuple("らーめん")
{:via, Registry, {Registry.Listener, "らーめん"}}
iex(5)> state_data = :sys.get_state(via)
"らーめん"
前回と同じ結果です。割り算のメッセージも送ってみます。
iex(6)> pid=ListenerSupervisor.pid_from_key("らーめん")
#PID<0.416.0>
iex(8)> send pid, {:div,2}
{:div, 2}
iex(9)> ans=20.0
正しく動作しています。PID<0.416.0>を確認しておきます。次にクラッシュを起こしてみましょう。
iex(11)> send pid, {:div,0}
{:div, 0}
iex(12)> [error] GenServer {Registry.Listener, "らーめん"} terminating
** (ArithmeticError) bad argument in arithmetic expression
(listen_tweets) lib/listen_tweets/listener.ex:48: ListenTweets.Listener.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:div, 0}
State: "らーめん"
[info] JOIN "room:lobby" to ReactChatWeb.RoomChannel
Transport: Phoenix.Transports.WebSocket (1.0.0)
Serializer: Phoenix.Transports.WebSocketSerializer
Parameters: %{"user_name" => "らーめん"}
[info] Replied room:lobby :ok
{"phx_reply", %{"response" => %{}, "status" => "ok"}}
{"presence_state",
%{
"さんど" => %{
"metas" => [
%{"online_at" => 1521615018, "phx_ref" => "JVpRNwpIIrM="},
%{"online_at" => 1521615059, "phx_ref" => "0ceOoFXYJys="}
]
}
}}
{"presence_diff",
%{
"joins" => %{
"らーめん" => %{
"metas" => [%{"online_at" => 1521615218, "phx_ref" => "LU8XKUzi6/M="}]
}
},
"leaves" => %{}
}}
前と同じようにエラーを起こしてプロセスが死んでいるようですが、すぐに再起動されjoinしているのがわかります。もう少し確認してみましょう。プロセスも動いており、状態の表示も問題ありません。但しPID<0.491.0>となっており、プロセス番号が変わっていることから、このプロセスが再起動されていることがわかります。
iex(14)> Supervisor.count_children(ListenerSupervisor)
%{active: 1, specs: 1, supervisors: 0, workers: 1}
iex(15)> via=Listener.via_tuple("らーめん")
{:via, Registry, {Registry.Listener, "らーめん"}}
iex(16)> state_data = :sys.get_state(via)
"らーめん"
iex(18)> pid=ListenerSupervisor.pid_from_key("らーめん")
#PID<0.491.0>
以上でFault Toleranceを確認することができました。
ただし以下の注意点を記述しておきます。ListenTweets.ListenerはGenServerとして実装しているけれども、状態(state)はスタート時に渡されたkeyを保持しているだけです。クラッシュ時は、その都度keyを渡されてrestartしているので、状態に関する問題は起きませんでした。しかし一般的にGenServerをリスタートするときは、初期状態ではなく、クラッシュした時点での状態を復旧させる必要があります。幸いなことに、Elixirにおいてこれは簡単です。ETSというDBがすぐに使えるからです。ETSはメモリ上に保存するので超高速に状態を保存・復旧させることが可能です。前にMnesiaというRDBについて書いたのですけども、本当にElixir(OTP)の環境は素晴らしいとしか言いようがありません。
Elixirの分散処理(Node)とMnesia- Qiita
ElixirのMnesiaラッパー、Amnesiaを使ってみる- Qiita
以上で終わります。