すごいErlangゆかいに学んだメモ 17章　スーパーバイザー

使うのは簡単だが、設計は難しい。
スーパーバイザでプロセスを監視することで、ゾンビプロセスの発生を防ぎ、メモリリークを防げる。
また、監視下に置かれたアプリケーションの終了処理を正常な順番で行えるようになる。
トップにあるスーパーバイザをシャットダウンすると、その子プロセスに終了処理が伝播していく。

よってプロセス監視ツリーには以下の要素が必要

- スーパーバイザープロセス
    - 他のスーパーバイザとワーカープロセスを監視する
- 監視されるワーカープロセス
    - ワーカーは常にスーパーバイザの監視下で使用される
- 依存関係の指定方法
- 子プロセスに対する再起動戦略
- 再起動を断念するタイミング
    - 無限に再起動するのを防ぐ

## supervisor ビヘイビアの関数

### init/1
ビヘイビア関数は `init/1` のみ.

返り値の定義は以下。

```erlang
{ok, {{RestartStrategy, MaxRestart, MaxTime},[ChildSpecs]}}.
```

- RestartStrategy
    - 再起動戦略
- MaxRestart と MaxTime
    - MaxTime(秒) 以内に MaxRestart回以上再起動された場合、スーパーバイザはシャットダウンされる
- ChildSpec
    - 子プロセスの仕様を決めるタプルのリスト

実際の返り値の例:

```erlang
{ok, {{one_for_all, 5, 60},  %% {RestartStrategy, MaxRestart, MaxTime}
      %% [ChildSpecs]
      [{fake_id,
        {fake_mod, start_link, [SomeArg]},
        permanent,
        5000,
        worker,
        [fake_mod]},
       {other_id,
       {event_manager_mod, start_link, []},
        transient,
        infinity,
        worker,
        dynamic}]}}.
```

### 再起動戦略について
返り値の `RestartStrategy` の部分。

#### one_for_one (１対１)
- 1つが死んだら、その１つが再起動される
- 各ワーカーが独立していて、他のプロセスに影響しない場合はこれ

![one_for_one](https://www.ymotongpoo.com/works/lyse-ja/_images/restart-one-for-one.png)

#### one_for_all (１対全部)
- 1つがクラッシュしたら、他の全てのプロセスを含め全て再起動する
- 監視下のプロセスが互いに依存している場合はこれ

![one_for_all](https://www.ymotongpoo.com/works/lyse-ja/_images/restart-one-for-all.png)

#### rest_for_one
- あるプロセスがクラッシュした場合に、そのプロセスより後に起動されたプロセスも含めて再起動する
- チェーンのようにプロセスが依存している場合はこれ
    - 例えば AがBを起動してBがCを起動するとき
    - BがクラッシュしたらBとCを再起動する

![rest_for_one](https://www.ymotongpoo.com/works/lyse-ja/_images/restart-rest-for-one.png)

#### simple_one_for_one
- 監視下のプロセスが１種類のみで、それを動的に追加したいときに使う
- 名前ほどシンプルではない


### 子プロセスの仕様(ChildSpec)

```erlang
[{fake_id,  % ChildID
  {fake_mod, start_link, [SomeArg]},  % StartFunc
  permanent,    % Restart
  5000,         % Shutdown
  worker,       % Type
  [fake_mod]},  % Modules
 {other_id, {event_manager_mod, start_link, []},
  transient,
  infinity,
  worker,
  dynamic}
```

- ChildID
    - 子プロセスの名前
    - デバッグ時などに便利
- StartFunc
    - スーパーバイザの起動方法を指定するタプル
    - `{Mod, Func, Arity}` の形式
    - 起動関数をOTP準拠にして、ランタイムの呼び出し元とリンクするべき
        - gen_*:start_link は常に自分のモジュール内でラップして使うべき
- Restart
    - 子プロセスが死んだ時の処理方法をスーパーバイザに伝える
        - permanet(永続)
            - 常に再起動
            - 長時間可動するプロセスに対して使う
        - temporary(一時的)
            - 再起動しない
            - 失敗することも想定される短命なプロセスに使う
        - transient(暫定的)
            - 永続と一時の中間の扱い
            - 正常終了するまでは常に再起動
                - つまり異常終了の場合は再起動
            - 正常終了後は再起動しない
- Shutdown
    - シャットダウン処理時の待機時間
        - `infinite` で無期限にもできる
    - 指定した時間をすぎると `exit(Pid, kill)` が送信されプロセスが強制終了される
- Type
    - 子プロセスのタイプを指定する
        - supervisor
            - 子のスーパーバイザ
        - supervisor_bridge
            - ?
        - worker
            - 上記２つ以外のOTPプロセス
- Modules
    - 子プロセスのビヘイビアが利用するコールバックモジュール
        - １要素のリストで指定する
    - 事前にモジュールが不明な場合(イベントハンドラ等)は `dynamic` と指定する

----
## supervisorの関数

### supervisor:start_link/2-3
```erlang
start_link(モジュール, 引数)
start_link(名前, モジュール, 引数)
```

- 引数
    - `init/1` を呼び出すコールバックモジュール
        - 例えば同じモジュール内にコールバック関数が定義されていたら `?MODULE` と指定する
    - コールバック関数に渡す引数
    - `{local, 名前}` でサーバーの登録名を指定できる
- 返り値
    - {ok, pid()}
    - ignore 
    - {error, startlink_err()}
        - {already_started, pid()}
        - {shutdown, term()}
        - term()

----

## supervisor の動作確認

- gen_serverのコード

```erlang
-module(echo_server).
-behavior(gen_server).

%% API
-export([start_link/1, stop/2, echo/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).


%% gen_server callbacks
init(Name) ->
    %% 親が子を終了する時にわかるように紐づける
    process_flag(trap_exit, true),
    io:format("echo_server が起動しました:~p~n", [Name]),
    {ok, Name}.

handle_call({echo, Message}, _From, Name) ->
    io:format("echo_server ~p: echo ~p~n", [Name, Message]),
    {reply, ok, Name};
handle_call({stop, Reason}, _From, Name) ->
    {stop, Reason, ok, Name};
handle_call(_Message, _From, State) ->
    {noreply, State}.

handle_cast(_Message, State) ->
    {noreply, State}.

handle_info(Message, State) ->
    io:format("不明なメッセージを受信しました: ~p~n", [Message]),
    {noreply, State}.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

terminate(normal, Name) ->
    io:format("echo_server が終了しました:~p~n", [Name]);
terminate(Reason, Name) ->
    io:format("echo_server ~p が ~p の理由で終了しました.~n", [Name, Reason]).


%% api functions
start_link(Name) ->
    gen_server:start_link({local, Name}, ?MODULE, Name, []).

stop(Pid, Reason) ->
    gen_server:call(Pid, {stop, Reason}).

echo(Pid, Message) ->
    gen_server:call(Pid, {echo, Message}).
```

- supervisor のコード

```erlang
-module(echo_server_sup).
-behaviour(supervisor).

-export([start_link/3]).
-export([init/1]).

% supervisor api
start_link(RestartStrategy, MaxRestart, MaxTime) ->
    supervisor:start_link({local,?MODULE}, ?MODULE, {RestartStrategy, MaxRestart, MaxTime}).

% callback function
init({simple_one_for_one, _MaxRestart, _MaxTime}) ->
    {ok, {{simple_one_for_one, 3, 60},
        [{dynamic_echo_server_sup,
            {echo_server, start_link, []},
            temporary, 1000, worker, [echo_server]}
        ]}};
init({RestartStrategy, MaxRestart, MaxTime}) ->
    {ok, {{RestartStrategy, MaxRestart, MaxTime},
        [{id_process1,
            {echo_server, start_link, [process1]},
            permanent, 1000, worker, [echo_server]},
         {id_process2,
            {echo_server, start_link, [process2]},
            temporary, 1000, worker, [echo_server]},
         {id_process3,
            {echo_server, start_link, [process3]},
            transient, 1000, worker, [echo_server]},
         {id_process4,
            {echo_server, start_link, [process4]},
            transient, 1000, worker, [echo_server]}
        ]}}.
```


プロセスの種類は以下

|名前|種類|
|:--|:--|
|process1|permanent|
|process2|temporary|
|process3|transient|
|process4|transient|

### one_for_one の動作確認

```erlang
%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}

%% one_for_one 戦略のスーパーバイザを起動する
3> {ok, Pid} = echo_server_sup:start_link(one_for_one, 5, 60).
echo_server が起動しました:process1
echo_server が起動しました:process2
echo_server が起動しました:process3
echo_server が起動しました:process4
{ok,<0.72.0>}

%% permanent プロセスを終了する
4> echo_server:stop(whereis(process1), normal).
echo_server が終了しました:process1
echo_server が起動しました:process1
ok
5> echo_server:stop(whereis(process1), kill).  
echo_server process1 が kill の理由で終了しました.
echo_server が起動しました:process1
ok
6>
=ERROR REPORT==== 23-Mar-2018::01:11:26 ===
** Generic server process1 terminating
** Last message in was {stop,kill}
** When Server state == process1
** Reason for termination ==
** kill
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]

6> echo_server:stop(whereis(process1), exit).
echo_server process1 が exit の理由で終了しました.

=ERROR REPORT==== 23-Mar-2018::01:11:35 ===
** Generic server process1 terminating
** Last message in was {stop,exit}
** When Server state == process1
** Reason for termination ==
** exit
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
echo_server が起動しました:process1
ok

7> echo_server:stop(whereis(process1), shutdown).
echo_server process1 が shutdown の理由で終了しました.
echo_server が起動しました:process1
ok

%% temporary プロセスを終了する(再起動しない)
8> echo_server:stop(whereis(process2), exit).  
echo_server process2 が exit の理由で終了しました.

=ERROR REPORT==== 23-Mar-2018::01:12:09 ===
** Generic server process2 terminating
** Last message in was {stop,exit}
** When Server state == process2
** Reason for termination ==
** exit
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
ok

%% transient プロセスを終了する
9> echo_server:stop(whereis(process3), normal).
echo_server が終了しました:process3
ok
10> echo_server:stop(whereis(process4), shutdown).
echo_server process4 が shutdown の理由で終了しました.
ok

%% 子プロセスの確認
11> supervisor:which_children(Pid).
[{id_process4,undefined,worker,[echo_server]},
 {id_process3,undefined,worker,[echo_server]},
 {id_process1,<0.84.0>,worker,[echo_server]}]
12> supervisor:count_children(Pid).
[{specs,3},{active,1},{supervisors,0},{workers,3}]
```

### rest_for_one の動作確認

```erlang
%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}

%% rest_for_one 戦略のスーパーバイザの起動
3> {ok, Pid} = echo_server_sup:start_link(rest_for_one, 2, 60).
echo_server が起動しました:process1
echo_server が起動しました:process2
echo_server が起動しました:process3
echo_server が起動しました:process4
{ok,<0.72.0>}

%% permanent プロセスの終了(後続のプロセスもまきこまれて異常終了したので再起動される)
4> echo_server:stop(whereis(process1), normal).
echo_server が終了しました:process1
echo_server process4 が shutdown の理由で終了しました.
ok
echo_server process3 が shutdown の理由で終了しました.
echo_server process2 が shutdown の理由で終了しました.
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4
5> exit(whereis(process1), kill).
echo_server process4 が shutdown の理由で終了しました.
true
echo_server process3 が shutdown の理由で終了しました.
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4

%% transient プロセスを終了する
6> echo_server:stop(whereis(process3), normal).
echo_server が終了しました:process3
ok
```

### one_for_all の動作確認

```erlang
%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}

%% one_for_all スーパーバイザの起動
3> {ok, Pid} = echo_server_sup:start_link(one_for_all, 5, 60).
echo_server が起動しました:process1
echo_server が起動しました:process2
echo_server が起動しました:process3
echo_server が起動しました:process4
{ok,<0.72.0>}

%% permanent プロセスを終了する(全部のプロセスがまきこまれて終了し、再起動される)
4> echo_server:stop(whereis(process1), normal).
echo_server が終了しました:process1
echo_server process4 が shutdown の理由で終了しました.
ok
echo_server process3 が shutdown の理由で終了しました.
echo_server process2 が shutdown の理由で終了しました.
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4

%% transient のプロセスを終了する
5> echo_server:stop(whereis(process3), normal).
echo_server が終了しました:process3
ok

%% transient のプロセスを normal, shutdown 以外で終了する
6> echo_server:stop(whereis(process4), kill).
echo_server process4 が kill の理由で終了しました.
echo_server process1 が shutdown の理由で終了しました.
ok
echo_server が起動しました:process1
echo_server が起動しました:process3
echo_server が起動しました:process4

=ERROR REPORT==== 23-Mar-2018::01:20:00 ===
** Generic server process4 terminating
** Last message in was {stop,kill}
** When Server state == process4
** Reason for termination ==
** kill
** Client <0.60.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,169}]},
    {gen_server,call,2,[{file,"gen_server.erl"},{line,202}]},
    {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
    {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
    {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
    {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
```

### 動作確認のまとめ
- `transient` なプロセスは `shutdown`, `normal` だと再起動されない
    - ただし `rest_for_one`, `one_for_all` によって `shutdown` された場合は再起動される
- `temporary` なプロセスは `rest_for_one`, `one_for_all` にまきこまれて `shutdown` しても再起動しない
- `temporary` なプロセスが異常終了しても `rest_for_one`, `one_for_all` はトリガーしない

## simple_one_for_one の動作確認

```erlang
%% モジュールの読み込み
1> c(echo_server).
{ok,echo_server}
2> c(echo_server_sup).
{ok,echo_server_sup}
3> {ok, Pid} = echo_server_sup:start_link(simple_one_for_one, 5 ,60).
{ok,<0.72.0>}

%% プロセスを動的に追加する
4> supervisor:start_child(echo_server_sup, [process1]).
echo_server が起動しました:process1
{ok,<0.74.0>}
5> supervisor:start_child(echo_server_sup, [process2]).
echo_server が起動しました:process2
{ok,<0.76.0>}
6> supervisor:start_child(echo_server_sup, [process3]).
echo_server が起動しました:process3
{ok,<0.78.0>}

%% プロセスを終了させる
7> supervisor:terminate_child(echo_server_sup, whereis(process1)).
echo_server process1 が shutdown の理由で終了しました.
ok
```

## 参考リンク
- [公式ドキュメント](http://erlang.org/doc/man/supervisor.html)
- [リンクされたプロセスの終了時の挙動](https://qiita.com/KOU_CHANG/items/a75bf7f882fe1f8cc9dd)

## 参考文献
- [すごいErlangゆかいに学ぼう](https://www.ohmsha.co.jp/book/9784274069123/)