20. 誰がスーパバイザを監視するのか?

20.1. 悪から善へ

../_images/watchmen.png

スーパバイザは、OTPのうち、あなたが使うようになるものとして、最も役に立つものです。 先の エラーとプロセス並列アプリケーションを設計する の章で基本的なスーパバイザについて見てきました。 これらを、エラーが起きたときにおかしなプロセスを再起動するだけでソフトウェアを稼働し続ける方法として、スーパバイザに触れました。

もうちょっと詳細に言うと、私たちが書いたスーパバイザはワーカプロセスを立ち上げて、それにリンクを張って、プロセスがいつ死んだかを知るために process_flag(trap_exit, true) で終了シグナルを捕捉して、死んだワーカプロセスを再起動します。 この動作は再起動を望んでいる場合はいいのですが、かなり間抜けな動作でもあります。 たとえば、リモコンをつかってテレビを点ける場合を想像してみましょう。 最初上手く点かなかった場合、ちゃんと押してなかったか赤外線信号が届かなかったのかと思いもう1、2度ボタンを押してみるでしょう。 私たちのスーパバイザでは、もしそのTVの電源を点けようとした場合、たとえリモコンの電池がもうない、またはそのテレビには使えないリモコンだったとわかっても、永遠に点けようとし続けてしまうでしょう。 なんて馬鹿なスーパバイザなんでしょう。

他にも私たち版のスーパバイザの残念なところは、同時に1つのワーカしか見ることができなかった点です。 誤解して欲しくないのですが、1つのワーカに対して1つのスーパバイザを持たせることはときどき役に立ちますが、それでは大きなアプリケーションでは、木構造ではなく1つのチェーン構造のスーパバイザしか持つことができません。 どのように同時に2、3つのワーカが必要なタスクを監視するのでしょうか。 私たちの実装では、それは成し得なませんでした。

OTPのスーパバイザであれば、幸いにも、そのような状況を扱える(そしてそれ以上の)柔軟性を提供してくれます。 たとえばワーカが再起動を断念する前に、一定時間内に何回再起動されるかを決めることができます。 また、スーパバイザ1つにつき2つ以上のワーカを割り当てる事ができ、失敗した時にお互いにどのように依存し合うかを2、3つのパターンから決定することさえ出来ます。

20.2. スーパバイザの概念

スーパバイザは使い方が最も単純で理解も最も簡単にできるビヘイビアの1つですが、それを用いて良い設計をするのが最も難しいビヘイビアでもあります。 スーパバイザとアプリケーション設計に関連した様々な戦略がありますが、より基本的な概念について理解せずに先に進むのはかなり厳しい道程ですので、まずはそこを理解する必要があります。

これまでの文章であまり定義せずに使ってきた「ワーカ」という単語があります。 ワーカはスーパバイザの対義語のようなものとして定義されています。 もしスーパバイザが子プロセスが死んだときに、確実にそれを再起動することしかしないプロセスだとしたら、ワーカは実際の仕事を行う責任があり、処理中に死ぬかもしれないようなプロセスです。 ワーカは通常信頼されていないのです。

スーパバイザはワーカや他のスーパバイザを監視することができ、ワーカは決してスーパバイザの下以外の場所では用いられません。

../_images/sup-tree.png

なぜすべてのプロセスは監視されるべきなのでしょうか。 そのアイデアは単純です。もし何らかの理由であなたが監視されていないプロセスを生成しているとして、どのようにしてそれらが生きているか死んでいるかを確認できるでしょうか。 何かが計測できない場合、それは存在しないのです。 もしあなたの監視ツリーの外の空白地帯にプロセスが存在する場合、そのプロセスが存在するかどうかをどのようにして知ることができるでしょう。 そのプロセスはどうやってそこに存在しているのでしょう。 このようなことがまた起きるのでしょうか。 もしそれが起きたら、とてもゆっくりメモリリークしていることが分かるでしょう。 とてもゆっくりメモリリークしてるのですが、メモリがなくなって突如VMが死ぬでしょう。そして、そんなことが何度も何度もおきるまで、容易には何が起きてるかを追活きれないでしょう。 もちろん、あなたは「注意して、何をやっているか意識すれば、万事上手く行く」と言うのでしょう。 そう、上手くいくのでしょう。あるいは上手くいかないでしょう。 製品システムでは、一か八かでは困りますよね。そしてErlangでは、これがガベージコレクションがそもそもある理由でもあります。 物事を監視しておくのはとても便利なのです。

また監視が便利なもう一つの理由は、アプリケーションを良い順番に終了させることができるという点です。 永遠に稼働させるつもりのないErlangソフトウェアを書くことがあるでしょう。 けれどもそれを綺麗に終了させたいとは思うでしょう。 シャットダウンの準備が整ったとどのようにして知るのでしょうか。 スーパバイザを使えば、造作もありません。 アプリケーションを終了させたいときはいつでも、VMの一番上にあるスーパバイザをシャットダウンすれば良いのです。(これは init:stop/1 のような関数で行うことができます) そしてそのスーパバイザが各子プロセスを終了させて良いか確認してきます。 子プロセスがいくつかスーパバイザであれば、同様に確認してきます:

../_images/sup-tree-shutdown.png

これによって、VMのシャットダウンという、すべてのプロセスが木構造の一部になっていないと非常に難しい場合もある処理を、順序良く行うことができます。

もちろん、プロセスが何らかの理由で固まってしまい、正しく終了しないこともあるでしょう。 そのようなときは、スーパバイザはプロセスを強制終了させる方法も持っています。

これがスーパバイザの基本的な理論です。 ワーカ、スーパバイザ、監視ツリー、異なる依存関係の検知方法、スーパバイザへの子プロセスの再起動または待機をいつ断念するか伝える方法などがあります。 これらがスーパバイザにできることのすべてではありませんが、いまのところは、実際にスーパバイザを使う上で必要な基本的な内容を網羅しています。

20.3. スーパバイザの利用

この章はこれまでで最も暴力的な章になります。親が子供を木に縛り付け、容赦無く殺すまで強制的に働かせることに時間を費やすのです。 しかし、それらすべてを実際に実装しなければ、本物のサディストにはなりません。

私がスーパバイザを使うのは単純です、と言ったとき、冗談を言っていたわけではありません。 提供するコールバック関数 init/1 というものが1つだけあります。この関数はなんらかの引数を取って、それで終わりです。 裏があるとすれば、この関数がとてつもなく複雑な戻り値を返すということです。 次にスーパバイザからの戻り値の例を載せてみます:

{ok, {{one_for_all, 5, 60},
      [{fake_id,
        {fake_mod, start_link, [SomeArg]},
        permanent,
        5000,
        worker,
        [fake_mod]},
       {other_id,
       {event_manager_mod, start_link, []},
        transient,
        infinity,
        worker,
        dynamic}]}}.

なんだって?おい、こいつはかなり複雑だぜ。 一般的な定義はもうちょっと取り組みやすい単純さです:

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

ここで、 ChildSpecs は子プロセスの仕様を表しています。 RestartStrategyone_for_one, one_for_rest, one_for_all, simple_one_for_one のいずれかです。

20.3.1. one_for_one

One for one(1対1)は直感的な再起動戦略です。 基本的に、スーパバイザがたくさんのワーカを監視していて、そのうちの1つが失敗したとき場合、その1つだけが再起動されるべきという意味です。 one_for_one は、監視されているプロセスが各々独立していて、お互い関係していない場合はいつでも、あるいはプロセスが再起動して状態が消えてしまっても、隣のプロセスに影響を与えない場合に使われるべきです。

../_images/restart-one-for-one.png

20.3.2. one_for_all

One for all(1対全部)は三銃士とは関係ありません。 これは、1つのスーパバイザ下にあるすべてのプロセスが、正常に動作するためにお互いに強く依存している場合にはいつでも使われます。 たとえば、前の章 Rage Against The Finite-State Machines で実装した取引システムの一番上にスーパバイザを追加することに決めたとしましょう。 このとき、取引をしている2人のユーザのFSMのうち1つがクラッシュした場合に、その1つだけを再起動したのでは意味がありません。なぜなら、彼らの状態が同期から外れてしまうからです。 両方共同時に再起動するのが健全な選択であり、 one_for_all はこのようなときのための戦略です。

../_images/restart-one-for-all.png

20.3.3. rest_for_one

この戦略はより特別な状況のためのものです。 チェーン状態に依存しているプロセスを起動する場合はいつでも(AをBが起動し、BがCを起動し、CがDを起動する、など)、 rest_for_one が使えます。 似た依存関係を盛ったサービスの場合にも便利です。(たとえば、Xは単独で動作しているが、YはXに依存していて、ZはXとYの両方に依存している) rest_for_one の再起動戦略は、基本的には、プロセスが死んだ時に、そのプロセスよりもあとに起動した(依存した)プロセスは再起動するけれど、それ以外はそのまま、というものです。

../_images/restart-rest-for-one.png

20.3.4. simple_one_for_one

simple_one_for_one の再起動戦略は、最も複雑なものです。 詳細についてはこれを使うようになるまでは見ませんが、基本的にはある種の子プロセスがあって、それらが静的に起動させるというよりも、動的にスーパバイザに追加したい場合に使われます。

違う言い方をすれば、 simple_one_for_one スーパバイザはただそこにいて、1種類の子供しか生成できないということがわかっています。 新しい子プロセスが欲しければ、そのスーパバイザに頼んで生成してもらいます。 これは理論的には通常の one_for_one スーパバイザにも出来ますが、 simple_one_for_one を使う実用的な利点があるのです。

Note

one_for_onesimple_one_for_one の最も大きな違いは、 one_for_one は持っている(あるいは削除していないなら持っていた)すべての子プロセスのリストを起動した順で保持している一方で、 simple_one_for_one はすべての子プロセスに対する1つの定義を持っていて、そのデータの保持はdictを使っています。 基本的に、プロセスがクラッシュしたときには、非常に多くの子プロセスを持っている場合には simple_one_for_one スーパーバイザのほうが断然速いです。

20.3.5. 再起動制限

RestartStrategy タプルに関して最後に MaxRestartMaxTime という変数の組について触れます。 基本的な考えとしては、 MaxTime (秒)以内に MaxRestart 回再起動がかかったら、スーパーバイザは再起動を断念して、自身をシャットダウンします。(なんと残念なんでしょう) 幸いにも、そのスーパーバイザのスーパーバイザが、まだその子プロセスをすべて再起動させる可能性はあります。

20.4. 子プロセスの仕様

次は、先の返り値の ChildSpecs の部分です。 ChildSpec は子プロセスの仕様を表しています。 先の返り値では次のような2つの ChildSpec を持っていました:

[{fake_id,
    {fake_mod, start_link, [SomeArg]},
    permanent,
    5000,
    worker,
    [fake_mod]},
  {other_id,
     {event_manager_mod, start_link, []},
     transient,
     infinity,
     worker,
     dynamic}]

子プロセスの仕様はより抽象的な形式として次のように表現できます:

{ChildId, StartFunc, Restart, Shutdown, Type, Modules}.

20.4.1. ChildId

ChildId はスーパーバイザの内部で使用される単なる内部的な名前です。 開発時に利用することは少ないですが、デバッグでの利用や、実際にあるスーパーバイザのすべての子プロセスのリストを取得するとき決めたときに利用できます。 どのような項もIDとして利用可能です。

20.4.2. StartFunc

StartFunc はスーパーバイザの起動方法を伝えるためのタプルです。 これは、これまで何度か使ってきた、通常の {M, F, A} の形式です。 ここで起動関数は、OTP準拠で、実行時はその呼び出し元とリンクしているということが非常に重要なので、留意しておいて下さい。(ヒント: gen_*:start_link() は常に独自モジュール内でラップされています)

20.4.3. Restart

Restart はスーパーバイザに特定の子プロセスが死んだときにどのような処理を行うかを伝えます。 これは3つの値を取ります:

  • permanent (永続的)
  • temporary (一時的)
  • transient (暫定的)

永続的プロセスは、それがどんなプロセスであろうと、常に再起動されるべきです。 私たちが先のアプリケーションで実装したスーパーバイザはこの戦略のみを使っています。 この戦略は通常ノード上で致命的な、長期間稼働するプロセス(またはサービス)に用いられます。

一方で、一時的なプロセスは、決して再起動されるべきでないプロセスです。 これらのプロセスは稼働期間が短く失敗することが前提のもので、依存するコードが少ないワーカのためにあります。

暫定的プロセスはその中間にあります。 これは、正常に終了するまでは稼働し、その後は再起動されません。 一方で、異常終了した場合(終了の原因が正常の場合以外すべて)は、再帰動作されます。 この再起動方法は、タスクを完了させる必要があるけれども、それが終わったら必要なくなるワーカに対してしばしば使われます。

1つのスーパーバイザ下にこれら3種類のうちどれを使ったプロセスでも子プロセスとして持つことが出来ます。 これは再起動戦略にも影響します。つまり one_for_all は、ある一時的プロセスが死んだ時には作用しませんが、同じスーパーバイザ下で先に永続的プロセスが死んだ場合には、その一時的プロセスは再起動されるでしょう。

20.4.4. Shutdown

本章の最初のほうで、スーパーバイザの助けを借りてアプリケーション全体を終了させることができると言いました。 この節はその方法について説明します。 最上位のスーパーバイザが終了するように命じられたとき、スーパーバイザは exit(ChildId, shutdown) を各Pidに対して呼び出します。 その子プロセスがワーカで終了を捕捉したら、自身の終了関数を呼びます。 または、単純に死にます。 スーパーバイザがshutdownシグナルを受け取ったとき、子プロセスに同様にメッセージを転送します。

したがって、子プロセスの仕様にある Shutdown という値は、終了の期限を設定するために使われます。 あるワーカでは、適切にファイルを閉じたり、サービスに退出するなどの事をしなければならないと知っているでしょう。 このような場合、ミリ秒単位なのか、あるいは辛抱強ければ無制限なのか、いずれにせよ特定の制限時間を設定したいときもあるでしょう。 もし時間が経って何も起きなければ、プロセスは exit(Pid, kill) が呼び出されて強制終了されます。 子プロセスに関して特に気にすることがなく、子プロセスが特にどんな結果も出さず、特に時間制限も必要なければ、 brutal_kill も使えるでしょう。 brutal_kill は、子プロセスが exit(Pid, Kill) で殺されるように、子プロセスを強制終了します。これは捕捉はできず、即座に行われます。

Shutdown の値を適切に選ぶことは、ときどき複雑または扱いにくいものです。 チェーン構造でつながったスーパーバイザの Shutdown の値が、たとえばそれぞれ 5000 -> 2000 -> 5000 -> 5000 だった場合、最後の2つは強制的に殺されることが起こりえます。なぜなら、2つ目のほうが短い切断時間だからです。 これは完全にアプリケーションに依存した値で、この点に関してはほとんどヒントはありません。

Note

simple_one_for_one の子プロセスが、この Shutdown 時間に関係する規則を配慮していないということを注意することは重要です。 simple_one_for_one の場合、スーパーバイザは終了だけして、スーパーバイザがいなくなった後はワーカは各々自分で終了するように残されています。

20.4.5. Type

Typeは単純にスーパーバイザに子供がワーカなのかスーパーバイザなのかを知らせます。 これはアプリケーションをより進んだOTPの機能で更新する場合に重要ですが、当面は気にする必要はありません―本当のことを言っていますし、すべて問題ありません。 スーパーバイザを信頼してください!

20.4.6. Modules

Modules は1つの要素、子供のビヘイビアに使われるコールバックモジュール名のリストです。 例外は、事前にコールバックモジュールを知らない場合です。(イベントマネージャ内のイベントハンドラとか) この場合、 Modules の値は動的で、OTPシステム全体がリリースのような、より応用的な機能を使うときに誰に聞いたら良いかわかるようにすべきです。

やったね、これでスーパーバイザプロセスを起動するのに必要な基本的な知識を得ることができました。 休憩をして、すべてを消化しましょう。あるいは、先に進んでもっと学びましょう!

../_images/take-a-break1.png

20.5. テストしましょう

ある練習は順序に従っています。 練習に関して言えば、完璧な例はバンドの練習です。 えっと、そんなに完璧ではないんですが、しばらく私に付き合ってください。 なぜならスーパーバイザなどを書いてみる口実としてこのアナロジーを使っていくからです。

私たちは *RSYNC という名前のバンドを面倒を見ています。このバンドは、プログラマで結成されていて、ドラム、ボーカル、ベース、ショルダーキーボードという有名な楽器で構成されています。いまは忘れ去られた80年代の栄光を追悼しています。 「スレッドセーフティダンス」「サタデーナイトコーダー」といった往年の名曲のリバイバルカバーがあるにもかかわらず、バンドはライブハウスを見つけるのに一苦労していました。 すべての状況に悩んで、私はErlangでバンドをシミュレーションしてみるというアイデアに大興奮してあなたのオフィスに飛び込んで来ました。「少なくとも私たちのバンドの名前すら聞かない」という状況をなんとかしたかったのです。 あなたはそのドラマーと同じアパートに住んでいたので、あなたは疲れています。(正直に言うと、ドラマーはバンドでは一番下手くそでしたが、バンドメンバが他にドラマーを知らなかったので、彼を切れずにいました)なので、この件を受け入れました。

20.5.1. Musicians

まずできることは、個々のバンドメンバを書くことです。 私たちのユースケースでは、musiciansモジュールは gen_server を実装します。 各ミュージシャンはパラメータとして楽器と演奏レベルを持ちます。(これで、ドラマーは下手くそな一方で、他のメンバーは問題ないと言えます。) いったんミュージシャンが一人生成されたら、彼は演奏を始めます。 また、必要があれば演奏を止めるオプションもあります。 このようなモジュールとインターフェースができました:

-module(musicians).
-behaviour(gen_server).

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

-record(state, {name="", role, skill=good}).
-define(DELAY, 750).

start_link(Role, Skill) ->
    gen_server:start_link({local, Role}, ?MODULE, [Role, Skill], []).

stop(Role) -> gen_server:call(Role, stop).

ミュージシャンが演奏する標準的な時間として使うために ?DELAY マクロを定義しました。 レコードの定義からわかるように、それぞれに名前を与えなければいけません:

init([Role, Skill]) ->
    %% To know when the parent shuts down
    process_flag(trap_exit, true),
    %% sets a seed for random number generation for the life of the process
    %% uses the current time to do it. Unique value guaranteed by now()
    random:seed(now()),
    TimeToPlay = random:uniform(3000),
    Name = pick_name(),
    StrRole = atom_to_list(Role),
    io:format("Musician ~s, playing the ~s entered the room~n",
              [Name, StrRole]),
    {ok, #state{name=Name, role=StrRole, skill=Skill}, TimeToPlay}.

init/1 関数では2つのことが行われています。 まず、終了を捕捉しています。 汎用サーバの章での terminate/2 の説明を思い出せば、 terminate/2 がサーバの親がその子供終了させるときに呼ばれるようにしたいのであれば、これをする必要があるとわかります。 init/1 関数の残りの部分はランダムシードを設定して(これで各プロセスが異なるランダム値を持つことができます)、それから自身にランダムな名前を生成します。 名前を生成する関数は次のようになります:

%% Yes, the names are based off the magic school bus characters'
%% 10 names!
pick_name() ->
    %% the seed must be set for the random functions. Use within the
    %% process that started with init/1
    lists:nth(random:uniform(10), firstnames())
    ++ " " ++
    lists:nth(random:uniform(10), lastnames()).

firstnames() ->
    ["Valerie", "Arnold", "Carlos", "Dorothy", "Keesha",
     "Phoebe", "Ralphie", "Tim", "Wanda", "Janet"].

lastnames() ->
    ["Frizzle", "Perlstein", "Ramon", "Ann", "Franklin",
     "Terese", "Tennelli", "Jamal", "Li", "Perlstein"].

いいですね! 実装に進みましょう。 このモジュールは handle_callhandle_cast については非常に平凡なものになります:

handle_call(stop, _From, S=#state{}) ->
    {stop, normal, ok, S};
handle_call(_Message, _From, S) ->
    {noreply, S, ?DELAY}.

handle_cast(_Message, S) ->
    {noreply, S, ?DELAY}.

唯一持てる関数呼び出しはミュージシャンサーバを止めるものだけです。これには私たちも大賛成です。 私たちが予期せぬメッセージを受け取った場合、それには返信せず、呼び出し元はクラッシュします。 私たちの問題ではありません。 {noreply, S, ?DELAY} タプルでタイムアウトを設定します。これは次に見るような1つの単純な理由のためです:

handle_info(timeout, S = #state{name=N, skill=good}) ->
    io:format("~s produced sound!~n",[N]),
    {noreply, S, ?DELAY};
handle_info(timeout, S = #state{name=N, skill=bad}) ->
    case random:uniform(5) of
        1 ->
            io:format("~s played a false note. Uh oh~n",[N]),
            {stop, bad_note, S};
        _ ->
            io:format("~s produced sound!~n",[N]),
            {noreply, S, ?DELAY}
    end;
handle_info(_Message, S) ->
    {noreply, S, ?DELAY}.

サーバがタイムアウトするたびに、私たちのミュージシャンは楽譜を演奏します。 もしメンバーの調子が良ければ、すべては順調です。 もし調子が悪ければ、5回のチャンスのうち1回を使って、悪い演奏をし、その結果ミュージシャンサーバはクラッシュします。 再度、無制限の関数呼び出しの最後に ?DELAY タイムアウトを設定します。

そのあと、 ‘gen_server’ビヘイビアで必要なので、空の code_change/3 コードバックを追加します:

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

そしてterminate関数も設定できます:

terminate(normal, S) ->
    io:format("~s left the room (~s)~n",[S#state.name, S#state.role]);
terminate(bad_note, S) ->
    io:format("~s sucks! kicked that member out of the band! (~s)~n",
              [S#state.name, S#state.role]);
terminate(shutdown, S) ->
    io:format("The manager is mad and fired the whole band! "
              "~s just got back to playing in the subway~n",
              [S#state.name]);
terminate(_Reason, S) ->
    io:format("~s has been kicked out (~s)~n", [S#state.name, S#state.role]).
../_images/bus.png

ここで多くの異なるメッセージが出てきました。 もし私たちが正常な理由で終了したら、それは私たちが stop/1 関数を呼び出して、自分の自由意志で退出したミュージシャンの名前を表示します。 bad_note メッセージの場合は、ミュージシャンはクラッシュして、私たちは「マネージャー(すぐ後ほどに追加するスーパバイザのこと)が彼をゲームから蹴りだしたせいだ」と言うでしょう。 それから、スーパバイザからやってくる終了メッセージもあります。 このメッセージを受け取ったときは、スーパバイザがすべての子プロセスを殺すと決めた場合、あるいは私たちの場合でいえば、すべてのミュージシャンを解雇した場合となります。 その後、それ以外の場合の汎用的なエラーメッセージを追加します。

次に、musicianモジュールの簡単なユースケースを示します:

1> c(musicians).
{ok,musicians}
2> musicians:start_link(bass, bad).
Musician Ralphie Franklin, playing the bass entered the room
{ok,<0.615.0>}
Ralphie Franklin produced sound!
Ralphie Franklin produced sound!
Ralphie Franklin played a false note. Uh oh
Ralphie Franklin sucks! kicked that member out of the band! (bass)
3>
=ERROR REPORT==== 6-Mar-2011::03:22:14 ===
** Generic server bass terminating
** Last message in was timeout
** When Server state == {state,"Ralphie Franklin","bass",bad}
** Reason for termination ==
** bad_note
** exception error: bad_note

Ralphieが演奏をし、悪演奏でクラッシュしました。やったね。 いいミュージシャンで同じ事をする場合には、すべての演奏を止めるために musicians:stop(instrument) 関数を呼び出す必要があります。

20.5.2. Band Supervisor

ようやくスーパーバイザを扱えます。 これから作るスーパーバイザには3種類あります。おおらかなもの(lenient)、怒りっぽいもの(angry)、完全な間抜け(jerk)の3つです。 これら3つの違いは、まずおおらかなスーパーバイザは、それでも不愉快なやつではあるけれども、演奏を失敗したバンドのメンバーを彼がうんざりして、バンドを諦めたときに1人解雇します。( one_for_one ) 一方で、怒りっぽいスーパーバイザは、間違えるごとにバンドメンバーのうち何人かを解雇し( rest_for_one )、おおらかなスーパーバイザよりも諦めるまでの時間が短いです。 間抜けなスーパーバイザは、先ほどの2人よりも諦める頻度は低いものの、誰かが間違えるとごとにバンド全員を解雇します。

-module(band_supervisor).
-behaviour(supervisor).

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

start_link(Type) ->
    supervisor:start_link({local,?MODULE}, ?MODULE, Type).

%% The band supervisor will allow its band members to make a few
%% mistakes before shutting down all operations, based on what
%% mood he's in. A lenient supervisor will tolerate more mistakes
%% than an angry supervisor, who'll tolerate more than a
%% complete jerk supervisor
init(lenient) ->
    init({one_for_one, 3, 60});
init(angry) ->
    init({rest_for_one, 2, 60});
init(jerk) ->
    init({one_for_all, 1, 60});

initの定義はまだ終わりではありませんが、ここまでの定義で私たちがほしい個々のスーパーバイザの雰囲気を設定することができました。 おおらかなものは、1つのミュージシャンを再起動させるだけで、60病間で4つのミスをした時に失敗となります。 おこりっぽいものは、2つのミスしか認めず、間抜けなものは非常に厳しい基準を持っています!

では、init関数の続きの実装を終えて、バンドを始動させる関数などの実際の実装を始めましょう:

init({RestartStrategy, MaxRestart, MaxTime}) ->
    {ok, {{RestartStrategy, MaxRestart, MaxTime},
         [{singer,
           {musicians, start_link, [singer, good]},
           permanent, 1000, worker, [musicians]},
          {bass,
           {musicians, start_link, [bass, good]},
           temporary, 1000, worker, [musicians]},
          {drum,
           {musicians, start_link, [drum, bad]},
           transient, 1000, worker, [musicians]},
          {keytar,
           {musicians, start_link, [keytar, good]},
           transient, 1000, worker, [musicians]}
          ]}}.

ご覧のとおり、3人の良いミュージシャンである、ボーカル、ベーシスト、ショルダーキーボーディストがいます。 ドラマーが困り者なのです。(彼があなたを不愉快にさせてしまうのです) ミュージシャンは異なる Restart を持っています。( permanent, transient, temporary のいずれか)ですので、自分の意志で退出したとしても、バンドはボーカルなしには決して演奏できませんが、ベーシストなしでは依然として良い演奏をすることもできます。なぜなら、気さくに言えば、誰がベーシストに拍手を送るでしょうか。

これで、機能的な band_supervisor モジュールができました。試してみましょう:

3> c(band_supervisor).
{ok,band_supervisor}
4> band_supervisor:start_link(lenient).
Musician Carlos Terese, playing the singer entered the room
Musician Janet Terese, playing the bass entered the room
Musician Keesha Ramon, playing the drum entered the room
Musician Janet Ramon, playing the keytar entered the room
{ok,<0.623.0>}
Carlos Terese produced sound!
Janet Terese produced sound!
Keesha Ramon produced sound!
Janet Ramon produced sound!
Carlos Terese produced sound!
Keesha Ramon played a false note. Uh oh
Keesha Ramon sucks! kicked that member out of the band! (drum)
... <snip> ...
Musician Arnold Tennelli, playing the drum entered the room
Arnold Tennelli produced sound!
Carlos Terese produced sound!
Janet Terese produced sound!
Janet Ramon produced sound!
Arnold Tennelli played a false note. Uh oh
Arnold Tennelli sucks! kicked that member out of the band! (drum)
... <snip> ...
Musician Carlos Frizzle, playing the drum entered the room
... <snip for a few more firings> ...
Janet Jamal played a false note. Uh oh
Janet Jamal sucks! kicked that member out of the band! (drum)
The manager is mad and fired the whole band! Janet Ramon just got back to playing in the subway
The manager is mad and fired the whole band! Janet Terese just got back to playing in the subway
The manager is mad and fired the whole band! Carlos Terese just got back to playing in the subway
** exception error: shutdown

魔法のようです! ドラマーだけが解雇され、しばらくしてほかのメンバーも解雇されましたね! そして、地下鉄(イギリスの方ならチューブですね)のホームへ行ってしまいました!

他のスーパバイザも試して見ることもできます。そしてそれらは同じ結果になるでしょう。 唯一の違いは再起動戦略です:

5> band_supervisor:start_link(angry).
Musician Dorothy Frizzle, playing the singer entered the room
Musician Arnold Li, playing the bass entered the room
Musician Ralphie Perlstein, playing the drum entered the room
Musician Carlos Perlstein, playing the keytar entered the room
... <snip> ...
Ralphie Perlstein sucks! kicked that member out of the band! (drum)
...
The manager is mad and fired the whole band! Carlos Perlstein just got back to playing in the subway

おこりっぽいものは、ドラマーがミスしたときにドラマーとショルダーキーボーディストが解雇されました。 間抜けなもののビヘイビアでは比較するものはなにもありません:

6> band_supervisor:start_link(jerk).
Musician Dorothy Franklin, playing the singer entered the room
Musician Wanda Tennelli, playing the bass entered the room
Musician Tim Perlstein, playing the drum entered the room
Musician Dorothy Frizzle, playing the keytar entered the room
... <snip> ...
Tim Perlstein played a false note. Uh oh
Tim Perlstein sucks! kicked that member out of the band! (drum)
The manager is mad and fired the whole band! Dorothy Franklin just got back to playing in the subway
The manager is mad and fired the whole band! Wanda Tennelli just got back to playing in the subway
The manager is mad and fired the whole band! Dorothy Frizzle just got back to playing in the subway

動的でない再起動戦略はこれがほぼすべてです。

20.6. 動的な監視

これまで見てきた監視というのは静的なものでした。 ソースコード内ですべての子供をきちんと明記して、その後にすべてを実行していました。 これは、あなたが実世界のアプリケーションで書くほとんどのスーパバイザに当てはまることでしょう。つまり通常そういったスーパバイザは構造的なコンポーネントの監視のために存在します。 一方で、不確定のワーカ上で動作するスーパバイザもあります。 それらは通常必要に応じて生成されます。 受け取った接続ごとにプロセスを生成するWebサーバを想像すればわかりやすいかもしれません。 この場合、持っているすべての異なるプロセスを見渡す動的なスーパバイザが欲しくなるでしょう。

ワーカが、 one_for_one, rest_for_one, one_for_all の戦略を使ってスーパバイザに追加される度に、子プロセスのPidやその他情報といった仕様がスーパバイザ内にあるリストに追加されます。 その後子プロセスの仕様はそれらを再起動する際に使われます。 以上のような動作をするように、次のようなインターフェースが存在します:

start_child(SupervisorNameOrPid, ChildSpec):
 この関数は子プロセスの仕様をリストに追加し、その仕様で子プロセスを起動します。
terminate_child(SupervisorNameOrPid, ChildId):
 子プロセスを終了または強制終了させます。子プロセスの仕様はスーパバイザないに残ります。
restart_child(SupervisorNameOrPid, ChildId):
 物事が動作するように、子プロセスの仕様を使います。
delete_child(SupervisorNameOrPid, ChildId):
 指定された子プロセスの仕様を取り除きます。
check_childspecs([ChildSpec]):
 子プロセスの仕様が正しいか確認します。 これは start_child/2 を使う前に使うことができます。
count_children(SupervisorNameOrPid):
 スーパバイザ下にあるすべての子供の数を数えて、どの子プロセスが稼働していて、仕様がいくつあって、そのうちスーパバイザはいくつあって、ワーカはいくつあるか、という参照用の小さなリストを与えてくれます。
which_children(SupervisorNameOrPid):
 スーパバイザ下のすべての子プロセスのリストを与えてくれます。

これがmusiciansモジュールでどのように動作するか見てみましょう。出力は削除してあります。(失敗ばかりのドラマーをしのぐために早くなる必要があります)

1> band_supervisor:start_link(lenient).
{ok,0.709.0>}
2> supervisor:which_children(band_supervisor).
[{keytar,<0.713.0>,worker,[musicians]},
 {drum,<0.715.0>,worker,[musicians]},
 {bass,<0.711.0>,worker,[musicians]},
 {singer,<0.710.0>,worker,[musicians]}]
3> supervisor:terminate_child(band_supervisor, drum).
ok
4> supervisor:terminate_child(band_supervisor, singer).
ok
5> supervisor:restart_child(band_supervisor, singer).
{ok,<0.730.0>}
6> supervisor:count_children(band_supervisor).
[{specs,4},{active,3},{supervisors,0},{workers,4}]
7> supervisor:delete_child(band_supervisor, drum).
ok
8> supervisor:restart_child(band_supervisor, drum).
{error,not_found}
9> supervisor:count_children(band_supervisor).
[{specs,3},{active,3},{supervisors,0},{workers,3}]

そして、どのように動的に子プロセスを管理するかご覧いただけたことでしょう。 これは動的に管理する(起動、終了など)必要があるもので、かつそれが小数ならならなんでも使えます。 内部的な表現はリストになっているので、多くの子プロセスに素早くアクセスるう必要があるときは、あまり上手くいきません。

../_images/guitar-case.png

このような場合、必要なのは simple_one_for_one です。 simple_one_for_one の問題は、子供を手動で再起動したり、削除したり、終了したりができないということでした。 幸いにも、柔軟性がないことで、いくつかの利点もあります。 すべての子プロセスは辞書の中に保持され、それによって参照が速くなります。 スーパバイザ下のすべての子プロセスのための単一の仕様もあります。 これにより、自分で子プロセスを削除したり、子プロセスの仕様を保存したりすることが決して無いという点で、メモリと時間が節約できます。

simple_one_for_one スーパバイザを書くことにおいて、ほとんどは、他のスーパバイザを書くことに似ていますが、1点だけ異なることがあります。 {M,F,A} タプル内の引数のリストはすべてではなく、 supervisor:start_child(Sup, Args) を使って呼び出すときに追加される対象となります。 そうです、 supervisor:start_child/2 はAPIを変更します。 つまり erlang:apply(M,F,A) を呼び出す supervisor:start_child(Sup, Spec) を使う代わりに、 erlang:apply(M,F,Args++A) を呼び出す supervisor:start_child(Sup, Args) があります。

次に、 band_supervisor でどのように書けるか載せます。 次のような節を追加しただけです:

init(jamband) ->
    {ok, {{simple_one_for_one, 3, 60},
         [{jam_musician,
           {musicians, start_link, []},
           temporary, 1000, worker, [musicians]}
         ]}};

ここではすべて temporary にして、スーパバイザはかなりおおらかなものにしました:

1> supervisor:start_child(band_supervisor, [djembe, good]).
Musician Janet Tennelli, playing the djembe entered the room
{ok,<0.690.0>}
2> supervisor:start_child(band_supervisor, [djembe, good]).
{error,{already_started,<0.690.0>}}

おおっと! これは、私たちの gen_server 内にジャンベ(djembe)奏者をジャンベとして起動呼び出しの一部に追加したからです。 それらに名前を付けない、もしくはそれぞれに違う名前を付けた場合、問題は起きません。 実際に、代わりにドラム(drum)と名付けたものではこうなります:

3> supervisor:start_child(band_supervisor, [drum, good]).
Musician Arnold Ramon, playing the drum entered the room
{ok,<0.696.0>}
3> supervisor:start_child(band_supervisor, [guitar, good]).
Musician Wanda Perlstein, playing the guitar entered the room
{ok,<0.698.0>}
4> supervisor:terminate_child(band_supervisor, djembe).
{error,simple_one_for_one}

いいですね。 前に言ったように、子プロセスをそのように制御する方法はありません。

5> musicians:stop(drum).
Arnold Ramon left the room (drum)
ok

このほうが上手く動きます。

(たまに当てはまりませんが)一般的なヒントとして、監視する子プロセスがほとんどなく、かつ/またはそれらがどんな速度で実行されてもよくてまれにしか実行されないと確実にわかっている場合にのみ、通常のスーパバイザを動的に使う、とお伝えしました。 他の動的な監視には、できるだけ simple_one_for_one を使いましょう。

update:

R14B03から、子プロセスを supervisor:terminate_child(SupRef, Pid) を使って終了させることができるようになりました。 simple_one_for_one 監視スキームは、多くのプロセスが1種類のプロセスで動いているときに、完全に動的でかつオールラウンドで関心の高い選択となりました。

以上が、監視戦略と子プロセスの仕様でした。 いま「これで一体全体どんなアプリケーションがかけるようになるんだ?」と思ったかもしれません。そして、もしそうなら、次の章にいってみると幸せになれるでしょう。次の章では、実際に1つのアプリケーションを短い監視ツリーで作り、実世界ではどのように使われるかを見ていきます。