スーパーバイザーを監督するのは誰?
悪い状態から良い状態へ
スーパーバイザーは、OTP で最も役立つ機能の 1 つです。基本的なスーパーバイザーについては、エラーとプロセス と 並行アプリケーションの設計 で既に説明しました。エラーが発生した場合に、障害のあるプロセスを再起動するだけでソフトウェアを稼働させ続ける方法として、スーパーバイザーを見てきました。
より詳細には、スーパーバイザーは *ワーカー* プロセスを開始し、それにリンクし、`process_flag(trap_exit,true)` で終了シグナルをトラップして、プロセスがいつ停止したかを知り、再起動します。これは再起動が必要な場合には有効ですが、かなり愚直でもあります。テレビの電源を入れるためにリモコンを使用していると想像してみてください。最初の試行でうまくいかない場合、正しく押せなかった場合や信号がうまく伝わらなかった場合に備えて、1、2 回試してみるかもしれません。スーパーバイザーがそのテレビの電源を入れようとしていた場合、リモコンに電池が入っていなかったり、テレビに合っていなかったとしても、永遠に試行し続けるでしょう。かなり愚かなスーパーバイザーです。
スーパーバイザーのもう 1 つの愚かな点は、一度に 1 つのワーカーしか監視できないことです。誤解しないでください。1 つのワーカーに対して 1 つのスーパーバイザーを用意することが役立つ場合もありますが、大規模なアプリケーションでは、これはスーパーバイザーのチェーンしか持つことができず、ツリーを持つことができないことを意味します。2 つまたは 3 つのワーカーを一度に必要とするタスクをどのように監督するのでしょうか?私たちの実装では、それは不可能でした。
幸いなことに、OTP スーパーバイザーは、そのようなケース(およびそれ以上)を処理するための柔軟性を備えています。ワーカーが諦める前に、一定期間内に何回再起動すべきかを定義できます。スーパーバイザーごとに複数のワーカーを持つことができ、障害が発生した場合に互いにどのように依存するかを決定するためのいくつかのパターンから選択することもできます。
スーパーバイザーの概念
スーパーバイザーは、使用と理解が最も簡単なビヘイビアの 1 つですが、優れた設計を作成するのが最も難しいビヘイビアの 1 つです。スーパーバイザーとアプリケーション設計にはさまざまな戦略がありますが、そこに行く前に、より基本的な概念を理解する必要があります。そうでなければ、かなり難しくなります。
これまで定義をあまり明確にせずに使用してきた単語の 1 つは、「ワーカー」という言葉です。ワーカーは、スーパーバイザーとは少し対照的に定義されています。スーパーバイザーは、子プロセスが停止したときに再起動することだけを確認するプロセスであるとすれば、ワーカーは実際の作業を担当するプロセスであり、その作業中に停止する可能性があります。通常、ワーカーは信頼されていません。
スーパーバイザーはワーカーと他のスーパーバイザーを監督できますが、ワーカーは別のスーパーバイザーの下以外では使用しないでください。
なぜすべてのプロセスを監督する必要があるのでしょうか?その考え方は単純です。何らかの理由で監督されていないプロセスを生成している場合、それらがなくなったかどうかをどのように確認できるでしょうか?何かを測定できない場合、それは存在しません。すべての監視ツリーから離れた虚空にプロセスが存在する場合、それが存在するかどうかをどのように知るのでしょうか?それはどのようにしてそこに到達したのでしょうか?再び発生するでしょうか?
それが発生した場合、メモリリークが非常にゆっくりと発生します。VM はメモリがなくなったために突然停止する可能性があり、非常にゆっくりとリークするため、再び発生するまで簡単に追跡できない場合があります。「注意して自分が何をしているかを知っていれば、問題ない」と言うかもしれません。確かに、問題ないかもしれません。そうでないかもしれません。本番システムでは、チャンスをつかむことは望ましくありません。Erlang の場合、そもそもガベージコレクションがあるのはそのためです。監視を続けることは非常に役立ちます。
それが役立つもう 1 つの理由は、アプリケーションを適切な順序で終了できることです。永遠に実行することを意図していない Erlang ソフトウェアを作成する場合があります。それでも、クリーンに終了させたいと思うでしょう。すべてがシャットダウンの準備ができていることをどのように知るのでしょうか?スーパーバイザーを使用すると、簡単です。アプリケーションを終了したいときはいつでも、VM のトップスーパーバイザーをシャットダウンします(これは `init:stop/1` などの関数で自動的に行われます)。次に、そのスーパーバイザーは各子プロセスに終了を要求します。子プロセスのいくつかがスーパーバイザーである場合、それらは同じことを行います。
これにより、VM が秩序正しくシャットダウンされます。これは、すべてのプロセスがツリーの一部になっていないと非常に困難です。
もちろん、プロセスが何らかの理由でスタックし、正しく終了しない場合があります。その場合、スーパーバイザーはプロセスを強制終了する方法を提供します。
これはスーパーバイザーの基本的な理論です。ワーカー、スーパーバイザー、監視ツリー、依存関係を指定するさまざまな方法、スーパーバイザーに子プロセスの試行または待機をいつ諦めるかを指示する方法などがあります。これはスーパーバイザーができることのすべてではありませんが、今のところ、実際に使用するために必要な基本的な内容を網羅できます。
スーパーバイザーの使用
これまでは非常に暴力的な章でした。親は子供を木に縛り付け、子供を強制的に働かせ、残酷に殺すことに時間を費やしています。しかし、それをすべて実装しなければ、本当のサディストにはなりません。
スーパーバイザーの使用は簡単だと言いましたが、冗談ではありません。提供するコールバック関数は 1 つだけです。`init/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]}}.
ここで、ChildSpec は子プロセスの仕様を表します。RestartStrategy は、`one_for_one`、`rest_for_one`、`one_for_all`、`simple_one_for_one` のいずれかになります。
one_for_one
one_for_one は直感的な再起動戦略です。基本的に、スーパーバイザーが多くのワーカーを監督していて、そのうちの 1 つが失敗した場合、その 1 つだけが再起動されることを意味します。監督されているプロセスが互いに独立しておらず、実際には関連していない場合、またはプロセスが再起動して状態を失っても兄弟プロセスに影響を与えない場合は、`one_for_one` を使用する必要があります。
one_for_all
one_for_all は、銃士とはほとんど関係ありません。単一のスーパーバイザーの下にあるすべてのプロセスが、正常に動作するために互いに大きく依存している場合に使用する必要があります。有限状態マシンに対する怒り の章で実装した取引システムの上にスーパーバイザーを追加することにしたとしましょう。トレーダーの 1 人がクラッシュした場合、状態が同期しなくなるため、2 人のトレーダーのうちの 1 人だけを再起動するのは実際には意味がありません。両方を一度に再起動する方が賢明な選択であり、`one_for_all` がそのための戦略となります。
rest_for_one
これは、より具体的な戦略です。チェーンでお互いに依存するプロセスを開始する必要がある場合(A が B を開始し、B が C を開始し、C が D を開始するなど)、`rest_for_one` を使用できます。同様の依存関係を持つサービスの場合にも役立ちます(X は単独で動作しますが、Y は X に依存し、Z は両方 に依存します)。`rest_for_one` 再起動戦略は、基本的に、プロセスが停止した場合、その後に開始された(依存する)すべてのプロセスが再起動されますが、逆はそうではありません。
simple_one_for_one
`simple_one_for_one` 再起動戦略は、最も単純な戦略ではありません。使用する際に詳しく説明しますが、基本的に、1 種類の子プロセスのみを受け入れるようにし、静的に開始するのではなく、スーパーバイザーに動的に追加する場合に使用する必要があります。
少し言い方を変えると、`simple_one_for_one` スーパーバイザーはそこに座っているだけで、1 種類の子プロセスしか生成できないことを知っています。新しい子プロセスが必要なときはいつでも、それを要求すると、それが得られます。この種のことを理論的には標準の `one_for_one` スーパーバイザーで行うことができますが、単純なバージョンを使用することには実際的な利点があります。
**注:** `one_for_one` と `simple_one_for_one` の大きな違いの 1 つは、`one_for_one` は開始された順序ですべての子プロセス(クリアしない場合は過去の子プロセスも含む)のリストを保持しますが、`simple_one_for_one` はすべての子プロセスに対して単一の定義を保持し、データを保持するために `dict` を使用することです。基本的に、プロセスがクラッシュした場合、多数の子プロセスがある場合、`simple_one_for_one` スーパーバイザーの方がはるかに高速になります。
再起動制限
RestartStrategy タプルの最後の部分は、MaxRestart と MaxTime の変数のペアです。基本的には、MaxTime 秒以内に MaxRestart 回を超える再起動が発生した場合、スーパーバイザーはコードを諦めてシャットダウンし、二度と戻らないように自身を kill します(それほど悪いということです)。幸いなことに、そのスーパーバイザーのスーパーバイザーはまだ子プロセスに希望を持っており、すべてを最初からやり直すかもしれません。
子プロセスの仕様
そして、戻り値の ChildSpec 部分です。ChildSpec は *子プロセスの仕様* を表します。先ほど、次の 2 つの子プロセスの仕様がありました。
[{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}.
ChildId
ChildId は、スーパーバイザーが内部的に使用する内部名です。デバッグの目的で、またスーパーバイザーのすべての子プロセスのリストを実際に取得する場合に役立つこともありますが、自分で使用することはめったにありません。Id には任意の項を使用できます。
StartFunc
StartFunc は、子プロセスを開始する方法を示すタプルです。これまでに何度か使用した標準の `{M,F,A}` 形式です。ここでの開始関数は OTP 準拠であり、実行時に呼び出し元にリンクすることが *非常に* 重要です(ヒント:独自のモジュールでラップされた `gen_*:start_link()` を常に使用します)。
Restart
Restart は、特定の子プロセスが停止したときにスーパーバイザーがどのように反応するかを指示します。これには 3 つの値を指定できます。
- permanent
- temporary
- transient
permanent プロセスは、何が何でも常に再起動する必要があります。以前のアプリケーションで実装したスーパーバイザーは、この戦略のみを使用していました。これは通常、ノード上で実行されている重要な長寿命プロセス(またはサービス)で使用されます。
一方、temporary プロセスは、再起動されるべきではないプロセスです。これらは、失敗することが予想され、それらに依存するコードがほとんどない短命のワーカー用です。
transient プロセスは、その中間です。正常に終了するまで実行され、その後は再起動されません。ただし、異常な原因で停止した場合(終了理由が `normal` 以外の場合)、再起動されます。この再起動オプションは、タスクを成功させる必要があるが、その後は使用されないワーカーによく使用されます。
単一のスーパーバイザーの下に 3 種類すべての子プロセスを混在させることができます。これは再起動戦略に影響を与える可能性があります。`one_for_all` 再起動は temporary プロセスが停止してもトリガーされませんが、permanent プロセスが最初に停止した場合、その temporary プロセスは同じスーパーバイザーの下で再起動される可能性があります。
Shutdown
本文の前半で、スーパーバイザの助けを借りてアプリケーション全体をシャットダウンできることに触れました。その方法を説明します。トップレベルのスーパーバイザが終了を要求されると、各Pidに対してexit(ChildPid, shutdown)を呼び出します。子がワーカーで、exitをトラップしている場合、自身のterminate関数を呼び出します。そうでない場合は、単に終了します。スーパーバイザがshutdownシグナルを受け取ると、同じ方法で自身の子に転送します。
子の仕様のShutdown値は、終了の期限を与えるために使用されます。特定のワーカーでは、ファイルを適切に閉じたり、サービスに離脱を通知したりする必要がある場合があります。このような場合、ミリ秒単位である程度のカットオフ時間を使用するか、本当に我慢強い場合はinfinityを使用することができます。時間が経過しても何も起こらない場合、プロセスはexit(Pid, kill)によって強制終了されます。子プロセスを気にせず、タイムアウトなしで結果なしに終了できる場合は、アトムbrutal_killも受け入れ可能な値です。brutal_killは、子プロセスがトラップ不可能で即座に終了するexit(Pid, kill)で強制終了されるようにします。
適切なShutdown値を選択することは、複雑で難しい場合があります。5000 -> 2000 -> 5000 -> 5000のようなShutdown値を持つスーパーバイザのチェーンがある場合、最後の2つは強制終了される可能性が高くなります。これは、2番目のスーパーバイザのカットオフ時間が短いためです。これは完全にアプリケーションに依存しており、この件に関して一般的なヒントはほとんどありません。
注意: simple_one_for_oneの子は、Shutdown時間に関してこのルールに従わないことに注意することが重要です。simple_one_for_oneの場合、スーパーバイザは単に終了し、各ワーカーはスーパーバイザが消滅した後、自分で終了する必要があります。
タイプ
タイプは、スーパーバイザに子がワーカーかスーパーバイザかを単に知らせます。これは、より高度なOTP機能を使用してアプリケーションをアップグレードする場合に重要になりますが、現時点ではこれについて気にする必要はありません。真実を伝えれば、すべてうまくいきます。スーパーバイザを信頼する必要があります!
モジュール
Modulesは、子ビヘイビアで使用されるコールバックモジュールの名前を1つだけ含むリストです。例外は、事前にIDがわからないコールバックモジュール(イベントマネージャーのイベントハンドラーなど)がある場合です。リリースなどのより高度な機能を使用する場合に、OTPシステム全体が誰に連絡するかを知るように、この場合、Modulesの値はdynamicである必要があります。
更新
バージョン18.0以降、スーパーバイザ構造は{#{strategy => RestartStrategy, intensity => MaxRestart, period => MaxTime}, [#{id => ChildId, start => StartFunc, restart => Restart, shutdown => Shutdown, type => Type, modules => Module}}の形式のマップとして提供できます。
これは既存の構造とほぼ同じですが、タプルの代わりにマップを使用します。supervisorモジュールはいくつかのデフォルト値を定義していますが、コードを保守する人にとって明確で読みやすいように、仕様全体を明示的に記述することをお勧めします。
これで、監視対象プロセスを開始するために必要な基本的な知識が得られました。休憩を取ってすべてを消化するか、さらに先に進むことができます!
テストしてみる
少し練習が必要です。そして、練習という意味では、バンドの練習が最適な例です。それほど完璧ではありませんが、スーパーバイザなどを実際に書いてみる口実として、かなり長いアナロジーを続けるので、しばらく我慢してください。
私たちは、ドラマー、シンガー、ベースプレーヤー、そして忘れ去られた80年代の栄光を偲んでキーター奏者という、一般的な楽器を演奏するプログラマーで構成された、_**RSYNC**というバンドを管理しています。「Thread Safety Dance」や「Saturday Night Coder」などのレトロなヒット曲のカバーをいくつか演奏しているにもかかわらず、バンドは会場を確保するのに苦労しています。状況全体にうんざりした私は、砂糖ラッシュに誘発された別のアイデア、つまりErlangでバンドをシミュレートするというアイデアを持ってあなたのオフィスに押し入ります。なぜなら、「少なくとも私たちの演奏は聞こえないだろうから」です。あなたはドラマー(このバンドで最も弱いリンクですが、正直なところ、他のドラマーを知らないため、彼と一緒にいます)と同じアパートに住んでいるため疲れていますが、あなたは受け入れます。
ミュージシャン
まず、個々のバンドメンバーを作成することができます。私たちのユースケースでは、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つのことが行われます。最初に、exitのトラップを開始します。Generic Serversの章の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_callとhandle_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}タプルにタイムアウトを設定します
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]).
ここでは多くの異なるメッセージがあります。normalの理由で終了した場合、stop/1関数を呼び出したため、ミュージシャンが自分の意思で去ったことを表示します。bad_noteメッセージの場合、ミュージシャンはクラッシュし、マネージャー(すぐに追加するスーパーバイザ)がゲームから追い出したためであると言います。
次に、スーパーバイザからのshutdownメッセージがあります。これが発生するたびに、スーパーバイザはすべての子を強制終了することを決定したか、私たちの場合、すべてのミュージシャンを解雇したことを意味します。残りの部分には一般的なエラーメッセージを追加します。
ミュージシャンの簡単なユースケースを次に示します
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は演奏していて、悪い音符の後にクラッシュします。万歳。goodミュージシャンで同じことを試してみると、すべての演奏を停止するためにmusicians:stop(Instrument)関数を呼び出す必要があります。
バンドスーパーバイザ
これで、スーパーバイザと連携できます。寛大なスーパーバイザ、怒っているスーパーバイザ、そして完全に嫌なスーパーバイザの3つのグレードがあります。それらの違いは、寛大なスーパーバイザは、依然として非常に怒りっぽい人ですが、バンドのメンバーを一度に1人ずつ(one_for_one)、失敗した人を解雇し、飽きるまで、全員を解雇してバンドをあきらめます。一方、怒っているスーパーバイザは、ミスをするたびに一部のメンバーを解雇し(rest_for_one)、全員を解雇してあきらめるまでの時間が短くなります。そして、嫌なスーパーバイザは、誰かがミスをするたびにバンド全体を解雇し、バンドがさらに低い頻度で失敗した場合にあきらめます。
-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番目のスーパーバイザは2回の失敗のみを受け入れ、嫌なスーパーバイザは非常に厳しい基準を設けます!
それでは、関数を完成させて、バンドの開始関数などを実際に実装しましょう
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
動的でない再起動戦略については以上です。
動的監視
これまでは、静的な監視を見てきました。ソースコードに直接持つすべての子を指定し、その後すべてを実行させました。これは、実際のアプリケーションでほとんどのスーパーバイザが設定される方法です。通常、アーキテクチャコンポーネントの監視に使用されます。一方、不特定のワーカーを管理するスーパーバイザがいます。それらは通常、オンデマンドで使用されます。受信する接続ごとにプロセスを生成する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)
- スーパーバイザの下のすべての子のリストを提供します。
出力を削除して、ミュージシャンでこれがどのように機能するかを見てみましょう(失敗するドラマーに勝つには、素早くする必要があります!)
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}]
そして、子どもたちをどのように動的に管理できるかを確認できます。これは、管理する必要がある動的なもの(これを開始したい、終了したいなど)で、数が少ない場合にうまく機能します。内部表現はリストであるため、多くの子にすばやくアクセスする必要がある場合は、これはうまく機能しません。
このような場合は、`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 が変更されています。`supervisor:start_child(Sup, Spec)` を実行して `erlang:apply(M,F,A)` を呼び出す代わりに、`supervisor:start_child(Sup, Args)` を実行して `erlang:apply(M,F,A++Args)` を呼び出すようになりました。
band_supervisor の場合は、次のように記述します。以下の節をどこかに追加してください。
init(jamband) ->
{ok, {{simple_one_for_one, 3, 60},
[{jam_musician,
{musicians, start_link, []},
temporary, 1000, worker, [musicians]}
]}};
この場合、すべてを一時的なものにしており、スーパーバイザーはかなり寛容です。
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` を使用してください。
更新
バージョン R14B03 以降、`supervisor:terminate_child(SupRef, Pid)` 関数を使用して子プロセスを終了できるようになりました。シンプルな one for one スーパービジョン方式は、完全に動的にすることが可能になり、単一タイプのプロセスを実行する多数のプロセスがある場合に、全体的に興味深い選択肢となっています。
スーパービジョン戦略と子プロセス仕様については以上です。今は「一体どうやって動くアプリケーションを作ればいいんだ?」と疑問に思っているかもしれませんが、もしそうなら、次の章に進んでシンプルなアプリケーションと短いスーパービジョンツリーを実際に構築し、現実の世界でどのように行われるかを確認できることを嬉しく思うでしょう。