スポンサーリンク

2016年3月16日

Using OTP with Elixir (Part5)

とある錬金術師の万能薬(Elixir)

Goal

  • ElixirからOTPを利用したサンプルを作成する
  • OTPを利用したシステムの基本的なアーキテクチャを習得する
  • OTPを使うとはどういうことなのか疑問を解消する

Dev-Environment

  • OS: Windows8.1
  • Erlang: Eshell V7.2.1, OTP-Version 18.1
  • Elixir: v1.2.0

Using OTP with Elixir (Part5)

  • 汎用イベントハンドラで使われる考え方
  • エラーロガーの仕組み
  • アラーム管理
  • アプリケーションサーバの構築
  • 監視ツリーの作成とサーバの追加(いまここ!)
  • アプリケーションのパッケージ化(これはやるか分からない…)
前回は面積サーバを作成しました。
今回は、Supervisorを使った監視ツリーを構築します。

File: lib/sellaprime_supervisor.ex

defmodule SellaprimeSupervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init([]) do
    GenEvent.swap_handler(:alarm_handler, :alarm_handler, :swap, MyAlarmHandler, :xyz)

    children = [
      worker(AreaServer, [], [id: :tag1,
                              restart: :permanent,
                              shutdown: 10000,
                              function: :start_link,
                              modules: [:area_server]]),
      worker(PrimeServer, [], [id: :tag2,
                               restart: :permanent,
                               shutdown: 10000,
                               function: :start_link,
                               modules: [:prime_server]])
    ]
    supervise(children, strategy: :one_for_one)
  end
end

Note:

- :id
後でワーカプロセスを参照するために使うアトムタグ

- :restart
再起動のオプションを設定している。
値として、:permanent、:temporary、:transientが指定できる。
:permanentは、プロセスの再起動を必ず行う。
:temporaryは、プロセスの再起動は行われない。
:transientは、プロセスが異常終了した場合のみ再起動する。
といった設定ができる。

- :shutdown
シャットダウン時間を指定している。(ミリ秒単位)
ワーカーが終了処理についやすことができる時間の指定になる。指定した時間より長くなるとワーカは殺される。
ほかの値としては、:brutal_killと:infinityを指定することができる。

- :function
子プロセスを起動する際の関数を指定している。
今回は、それぞれのサーバに定義しているstart_linkを指定している。

- :modules
子プロセスがGenServerかSupervisorである場合、
コールバックモジュールの名前を指定することができる。
GenEventの場合、:dynamicを指定する。
それでは、実行してみましょう。

Example:

> iex --erl "-boot start_sasl -config elog" -S mix

iex(1)> SellaprimeSupervisor.start_link
"*** my_alarm_handler init:"
{:xyz, {:alarm_handler, []}}
"Elixir.AreaServer starting\n"
"Elixir.PrimeServer starting\n"
{:ok, #PID<0.109.0>}
iex(2)> AreaServer.area({:square, 10})
100
iex(3)> AreaServer.area({:rectangle, 10, 20})
"Elixir.AreaServer stopping\n"
"Elixir.AreaServer starting\n"
** (exit) exited in: GenServer.call(AreaServer, {:area, {:rectangle, 10, 20}}, 5000)
    ** (EXIT) an exception was raised:
        ** (RuntimeError) oops!!
            (otp_system_sample) lib/area_server.ex:42: AreaServer.compute_area/1
            (otp_system_sample) lib/area_server.ex:19: AreaServer.handle_call/3
            (stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
            (stdlib) gen_server.erl:661: :gen_server.handle_msg/5
            (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
    (elixir) lib/gen_server.ex:564: GenServer.call/3
iex(3)>
17:32:29.335 [error] GenServer AreaServer terminating
** (RuntimeError) oops!!
    (otp_system_sample) lib/area_server.ex:42: AreaServer.compute_area/1
    (otp_system_sample) lib/area_server.ex:19: AreaServer.handle_call/3
    (stdlib) gen_server.erl:629: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:661: :gen_server.handle_msg/5
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Last message: {:area, {:rectangle, 10, 20}}
State: 1

iex(3)> AreaServer.area({:square, 25})
625
iex(4)> PrimeServer.new_prime(20)
Generating a 20 digit prime ..............
76351346187249262229
iex(5)> PrimeServer.new_prime(120)
Generating a 120 digit prime .
17:38:46.905 [error] *** Tell the Engineer to turn on the fan

..........................................
45890410408048094824...(長いので割愛)

17:38:47.323 [error] *** Denger over. Turn off the fan

iex(6)> :rb.start([max: 20])
rb: reading report...done.
{:ok, #PID<0.120.0>}
iex(7)> :rb.list
  No                Type              Process       Date     Time
  ==                ====              =======       ====     ====
  20            progress             <0.23.0> 2016-03-16 17:32:16
  19            progress             <0.69.0> 2016-03-16 17:32:16
  18            progress             <0.69.0> 2016-03-16 17:32:16
  17            progress             <0.69.0> 2016-03-16 17:32:16
  16            progress             <0.23.0> 2016-03-16 17:32:16
  15         info_report             <0.23.0> 2016-03-16 17:32:17
  14            progress             <0.98.0> 2016-03-16 17:32:17
  13            progress             <0.98.0> 2016-03-16 17:32:17
  12            progress             <0.98.0> 2016-03-16 17:32:17
  11            progress             <0.98.0> 2016-03-16 17:32:17
  10            progress             <0.23.0> 2016-03-16 17:32:17
   9            progress             <0.23.0> 2016-03-16 17:32:17
   8            progress              <0.9.0> 2016-03-16 17:32:23
   7            progress              <0.9.0> 2016-03-16 17:32:23
   6               error              <0.9.0> 2016-03-16 17:32:29
   5        crash_report  'Elixir.AreaServer' 2016-03-16 17:32:29
   4   supervisor_report              <0.9.0> 2016-03-16 17:32:29
   3            progress              <0.9.0> 2016-03-16 17:32:29
   2               error             <0.30.0> 2016-03-16 17:38:46
   1               error             <0.30.0> 2016-03-16 17:38:47
:ok
iex(8)> :rb.show(5)

CRASH REPORT  <0.110.0>                                     2016-03-16 17:32:29
===============================================================================
Crashing process
   initial_call                      {'Elixir.AreaServer',init,['Argument__1']}
   pid                                                                <0.110.0>
   registered_name                                          'Elixir.AreaServer'
   error_info
         {exit,{#{'__exception__' => true,
                 '__struct__' => 'Elixir.RuntimeError',
                 message => <<"oops!!">>},
               [{'Elixir.AreaServer',compute_area,1,
                                     [{file,"lib/area_server.ex"},{line,42}]},
                {'Elixir.AreaServer',handle_call,3,
                                     [{file,"lib/area_server.ex"},{line,19}]},
                {gen_server,try_handle_call,4,
                            [{file,"gen_server.erl"},{line,629}]},
                {gen_server,handle_msg,5,
                            [{file,"gen_server.erl"},{line,661}]},
                {proc_lib,init_p_do_apply,3,
                          [{file,"proc_lib.erl"},{line,240}]}]},
              [{gen_server,terminate,7,[{file,"gen_server.erl"},{line,826}]},
               {proc_lib,init_p_do_apply,3,
                         [{file,"proc_lib.erl"},{line,240}]}]}
   ancestors                          ['Elixir.SellaprimeSupervisor',<0.107.0>]
   messages                                                                  []
   links                                                            [<0.109.0>]
   dictionary                                                                []
   trap_exit                                                               true
   status                                                               running
   heap_size                                                                987
   stack_size                                                                27
   reductions                                                               569

:ok
iex(9)> :rb.show(2)

ERROR REPORT  <0.34.0>                                      2016-03-16 17:38:46
===============================================================================

*** Tell the Engineer to turn on the fan
:ok
iex(10)> :rb.show(1)

ERROR REPORT  <0.34.0>                                      2016-03-16 17:38:47
===============================================================================

*** Denger over. Turn off the fan
:ok
うん、クラッシュレポートもちゃんと出てますね。
問題なしです。

Note:

ErlangとElixirの監視戦略には、幾つか選択肢がある。

- one_for_one
- one_for_all
- rest_for_one
- simple_one_for_one

内、知っているone_for_oneとone_for_allについて簡単にまとめる。
(他二つはよく分かってないともいう...確か、どなたかが記事を上げていた気がする...)

"one_for_one"は、1対1の監視ツリー。
以下ようなの監視ツリーがあった時、
プロセス3がクラッシュした場合、そのプロセスだけ再起動する。

監視
|
+-プロセス1
|
+-プロセス2
|
+-プロセス3(クラッシュ!?)
|
+-プロセス4

上記がこうなる...

監視
|
+-プロセス1
|
+-プロセス2
|
+-プロセス3(...再起動)
|
+-プロセス4

"one_for_all"は1対全の監視ツリー。
以下ようなの監視ツリーがあった時、
プロセス3がクラッシュした場合、全プロセスを再起動する。

監視
|
+-プロセス1
|
+-プロセス2
|
+-プロセス3(クラッシュ!?)
|
+-プロセス4

上記がこうなる...

監視
|
+-プロセス1(...再起動)
|
+-プロセス2(...再起動)
|
+-プロセス3(...再起動)
|
+-プロセス4(...再起動)

※監視ツリーにて監視されているプロセスはワーカー(worker)と言う。
※SupervisorでSupervisorも監視することができる。
ちょいと疑問なんですけど、同じクラッシュが繰り返された場合ってどこまで耐えられるのでしょうか???
(クラッシュ->再起動->クラッシュ->再起動…ループってな具合ですね)
一応、無限クラッシュしないような仕組みはあるみたいですけど…さて、どこまでやってくれるのだろうか。
以上、監視ツリーを構成してからの起動でした。
次は、アプリケーションのパッケージングなんですが、
Elixir側だと…ライブラリ化するあたりをやればいいかと思っています。
しかし、そういった内容の記事だと以前、@ma2ge氏がQiitaに投稿をしてくれていますので、
私の方でやる必要はないですね。
以前、その記事を参考にライブラリ化してHexにアップしたことありますし…
何か違うのか調査してみます。
これ以降の記事が上がらなかったら、やらないんだなっと思って下さい。ノシ

Bibliography

人気の投稿