このブログは、旧・はてなダイアリー「檜山正幸のキマイラ飼育記 メモ編」(http://d.hatena.ne.jp/m-hiyama-memo/)のデータを移行・保存したものであり、今後(2019年1月以降)更新の予定はありません。

今後の更新は、新しいブログ http://m-hiyama-memo.hatenablog.com/ で行います。

アプリケーションのメカニズムと作り方

悔やまれた件、もう一度調べた。基本的には、「OTP設計原理」を読めばよいのだ。

次の概念を理解しよう。

  1. アプリケーション
  2. OTP-ST(監視ツリー)
  3. OTP-STアプリケーション
  4. トップスーパーバイザ
  5. アプリケーションコントローラ・プロセス
  6. アプリケーションマスター・プロセス
  7. アプリケーション・コールバックモジュール

Erlangのアプリケーション概念は多様/多義的、ここでは最も狭く厳密な定義を扱う。それがOTP-ST(supervison tree)アプリケーション。OTPの正統的なアプリケーション概念だ。

アプリケーションは基本的にモジュールの集まりだが、実行時にはプロセスが生成される。特にOTP-STアプリケーションでは必ずプロセスが使われる。よって、モジュールとプロセスをどのように編成/構造化するかの規約が必要になる、それが「設計原理」に書いてあるわけ。よほどのことがない限りOTPフレームワーク(ビヘイビア群)を使うことになる。

アプリケーションコントローラとspecial process

アプリケーションコントローラはカーネルの一部で、システム(ERTS)に1つだけ常に存在する(登録名:application_controller、PIDは5番くらい*1)。

アプリケーションコントローラは、システム内に存在するOTP-STアプリケーション(以下、単にアプリケーション)全てを完全に掌握している。アプリケーションコントローラに管理されないアプリケーションは野良アプリケーションということになる。

OTP用語ではないが、アプリケーションコントローラに管理されるアプリケーション、モジュール、プロセスをオフィシャルと形容することにしよう。オフィシャルモジュールは、必ず1つのアプリケーションに属さなくてはならない。複数のアプリケーションで同一モジュールを共有することはできない。ただし、これは所属関係の話で、利用(コール)はどこからでもできる。同様に、オフィシャルプロセスはただ1つのアプリケーションに所属する。ちなみに、プロセスを持たないアプリケーションはライブラリアプリケーションと呼ばれる(純粋ライブラリ・アプリケーションという感じだが)。

  • application:get_application(Pid | Module) -> {ok, Application} | undefined
  • application:get_application() ≡ application:get_application(self())

get_applicationが単値の関数であることから、所属関係の事情はわかるだろう。get_applicationがundefinedを返せば、それは野良モジュール/プロセス(カーネルプロセスinitとかは話が別)。おそらくsystools内に所属の重複をチェックするツールがあるはず。

オフィシャルプロセスは必ずしも名前を持たなくてもよい。が、proc_libの関数で生成されたspecial process(OTP用語)で、プロセスディクショナリに管理情報(メタデータ)を持つ。special processは、このメタデータで統制される。


> {dictionary, DList} = erlang:process_info(pid(0, 4, 0), dictionary).
{dictionary,[{'$ancestors',[<0.1.0>]},
{'$initial_call',{gen,init_it,
[gen_event,<0.1.0>,<0.1.0>,
{local,error_logger},
[],[],[]]}}]}
> {value, Ances} = lists:keysearch('$ancestors', 1, DList).
{value,{'$ancestors',[<0.1.0>]}}
>

トップスーパーバイザApp_sup

正当なアプリケーションはST(監視ツリー)を持つ。STのルートプロセスはトップスーパーバイザと呼ばれる。supervisorビヘイビアのコールバックモジュールがトップスーパーバイザの実行仕様を与える。慣例的に、トップスーパーバイザのコールバックモジュール名は App_sup とする。ただし、App_supはinit(Arg)(と慣例的便利関数)を公開するだけの簡単なものだ*2

supervisor:start_link(App_sup, Arg)、または supervisor:start_link(SupName, App_sup, Arg)(登録名が必要なとき)がSTを始動させるトリガーとなる。うまくアプリケーションのST(リンクされたプロセス群)が始動すれば {ok,Pid} が返る。Pidはトップスーパーバイザ・プロセスのPid。

App_supは、慣例として、コールバックinit(Arg)以外に次を公開する。


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

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

App_sup:start_link(Arg)のArgはコールバックinit(Arg)のArgとしてそのまま渡される。可変なArgが不要なら、App_sup:start_link/0 だけ公開すればよい。

アプリケーションコールバックモジュールApp_app

App_sup:start_link()は誰がコールするか? コントローラーが直接コールするのではなくて、アプリケーションコールバックモジュールApp_appのstart(Type, Arg)を経由する。コールバックApp_app:start/1の引数仕様:

  • Typeの値は通常normal、それ以外の値({takeover,Node} | {failover,Node})は分散アプリケーションのときのみ。
  • Argは、トップスーパーバイザのArgとは無関係!。コントローラが、アプリケーション仕様(もとは.appファイル)から取り出した値をArgとしてApp_app:start(Type, Arg)に渡す。アプリケーション仕様にstart引数が定義されてなければ、デフォルト値の[]が渡される。

アプリケーションコールバックは次のテンプレートでほとんど十分:


%% App_app.erl
-module(App_app).
-behaviour(application).

-export([start/2, stop/1]).

start(_Type, _Arg) ->
App_sup:start_link().

stop(_State) ->
ok.

App_app:start/2は、成功時に {ok, Pid}か{ok, Pid, State}を返せる。通常は、supervisor:start_link/{2, 3} が {ok,Pid} | ignore | {error,Error} を返すので、それをそのまま使う。もし、Stateを付けると、それがstop/1の引数に渡される。stop/1の引数のデフォルト値は[]だが、不要なら無視する。

非常に重要な注意: App_app:stop/1 はアプリケーションのSTシャットダウンが完了した後に呼び出される。STの終了処理(terminate(Reason, State)コールバック)とは何の関係もない。STではできない後始末を行うのだ。

アプリケーションマスター・プロセス

アプリケーションコールバックモジュールApp_appのstart/2(引数はたいてい使わない)から、トップスーパーバイザを始動するApp_sup:start_link/0(その実体は、supervisor:start_link(Mod, Arg)など)が呼ばれる。当然に、App_app:start/2をコールした当のプロセスはアプリケーションのトップスーパーバイザとリンクされる。

では、リンクによりSTを監視するハイパーバイザー(スーパー・スーパーバイザ)は誰なのだろう? コントローラか? そうではない。アプリケーションごとに“影の支配者”となるアプリケーションマスター・プロセスが作られ、そのマスターがApp_app:start/2をコールする。


コントローラ
+-(生成)-> マスタープロセス
+-(start/2)->コールバックモジュール
+-(start_link/0など)->トップスーパーバイザ
+
<--(戻り値:{ok, Pid})--------------------------+

コントローラは、自分の配下であるマスタープロセスを使って、アプリケーションSTを完全に制御する。モジュールApp_appは、STの始動とシャットダウン後の後始末に登場するだけで、稼働中のSTでは何の役割も果たさない。プロセスリンクは、マスタープロセス(ハイパーバイザー)とトップスーパーバイザプロセス間で確立される。

シャットダウン

アプリケーションのスタートは、アプリケーションマスタープロセスが当該アプリケーションのアプリケーションコールバックモジュールのstart/2を呼ぶところから始まる。では、アプリケーションの終了はどうなるか。予想とは違い、stopメッセージは使わない。参考までに標準的なstopメッセージの方式は:


handle_cast(stop, State) ->
{stop, normal, State}.

ワーカープロセスで上記のごとくstopメッセージをサポートするのは、特に必要はない。スーパーバイザから管理下の子ワーカを止めるには、supervisor:terminate_child/2が使える。


supervisor:terminate_child(SupRef, Id) -> Result
where
SupRef = Name | {Name,Node} | {global,Name} | pid(),
Name = Node = atom(),
Id = term(),
Result = ok | {error,Error},
Error = not_found | simple_one_for_one .

スーパーバイザを止めるには、シャットダウン戦略に従ったシャットダウン手順を踏む必要がある。これは、シグナルexit(Target, shutdown)を使う。おおよその手順は:

  1. コントローラがアプリケーションマスターにシャットダウンを指示する。
  2. アプリケーションマスターはトップスーパーバイザにshutdownシグナルを送る。
  3. トップスーパーバイザは配下のスーパーバイザ/ワーカーを止める。
  4. トップスーパーバイザが死んでEXITシグナルをアプリケーションマスターに送る。
  5. アプリケーションマスターも死ぬ。

原則的に、トップスーパーバイザの終了はアプリケーションコントローラを使う。

*1:多少の欠番があって、init, erl_prim_loader, error_loggerの次かな。

*2:コールバックモジュールじゃなくて、単にsupervisor:{start, start_link}にデータを渡してもいいような気がするくらいだ。