OTP アプリケーションの構築

なぜそれが必要なのか?

A construction sign with a squid holdin a shovel, rather than a man doing so

シンプルな関数呼び出しでアプリケーション全体のスーパービジョンツリーが一度に起動するのを見た後、なぜ現状よりも複雑にする必要があるのか疑問に思うかもしれません。スーパービジョンツリーの概念は少し複雑で、システムの初回セットアップ時に、これらのツリーとサブツリーすべてをスクリプトで手動で起動するだけで済むように思えます。その後は、外に出て動物のような形の雲を探して午後を過ごすことができます。

これは全くその通りです。これは許容できる方法です(特に雲の部分は、最近はすべてがクラウドコンピューティングに関するものなので)。しかし、プログラマーやエンジニアによって作られたほとんどの抽象化と同様に、OTP アプリケーションは、多くのアドホックシステムが一般化され、整理された結果です。上記のようにスーパービジョンツリーを起動するためのスクリプトとコマンドの配列を作成し、一緒に働く他の開発者が独自のスクリプトとコマンドを持っている場合、すぐに大きな問題に遭遇するでしょう。そして、誰かが「誰もが同じ種類のシステムを使ってすべてを起動できればいいのにと思いませんか?そして、すべてが同じ種類のアプリケーション構造を持っていればもっといいと思いませんか?」と尋ねるでしょう。

OTP アプリケーションはまさにこの種の問題を解決しようとするものです。ディレクトリ構造、設定を処理する方法、依存関係を処理する方法、環境変数と設定を作成する方法、アプリケーションを起動および停止する方法、競合の検出とアプリケーションをシャットダウンせずにライブアップグレードを処理するための安全な制御方法を提供します。

そのため、これらの側面(およびそれらが提供する、一貫した構造やツールのような利点)を望まない限り、この章はあなたにとって興味深いものとなるでしょう。

私のもう一台の車はプールです

前の章で作成した `ppool` アプリケーションを再利用して、実際の OTP アプリケーションに変換します。

そのためには、まず `ppool` 関連のすべてのファイルを適切なディレクトリ構造にコピーします。

ebin/
include/
priv/
src/
 - ppool.erl
 - ppool_sup.erl
 - ppool_supersup.erl
 - ppool_worker_sup.erl
 - ppool_serv.erl
 - ppool_nagger.erl
test/
 - ppool_tests.erl

ほとんどのディレクトリは今のところ空のままです。「並行アプリケーションの設計」の章で説明したように、`ebin/` ディレクトリにはコンパイルされたファイル、`include/` ディレクトリには Erlang ヘッダー(`.hrl`)ファイル、`priv/` には実行可能ファイル、その他のプログラム、およびアプリケーションの動作に必要なさまざまな特定のファイルが格納され、`src/` には必要な Erlang ソースファイルが格納されます。

A pool with wheels and an exhaust pipe

以前持っていたテストファイルのために `test/` ディレクトリを追加しました。これは、テストはよくあることですが、必ずしもアプリケーションの一部として配布する必要はないためです。コードを開発し、マネージャーに正当化するときにのみ必要です(「テストは合格しました。アプリが人を殺した理由がわかりません」)。このような他のディレクトリは、ケースに応じて必要に応じて追加されます。一例として、アプリケーションに EDoc ドキュメントを追加する際に追加される `doc/` ディレクトリがあります。

4 つの基本的なディレクトリは `ebin/`、`include/`、`priv/`、`src/` で、実際の OTP システムがデプロイされる際に `ebin/` と `priv/` のみがエクスポートされますが、取得するほぼすべての OTP アプリケーションで共通です。

アプリケーションリソースファイル

ここからどこへ行くのでしょうか?まず、アプリケーションファイルを追加します。このファイルは、Erlang VM にアプリケーションとは何か、どこから始まりどこで終わるかを伝えます。このファイルは、すべてのコンパイル済みモジュールとともに `ebin/` ディレクトリにあります。

このファイルは通常 `<yourapp>.app`(この場合は `ppool.app`)という名前で、VM が理解できる形式でアプリケーションを定義する Erlang 項の束が含まれています(VM は推測するのが苦手です!)。

**注:** このファイルを `ebin/` の外に置いて、代わりに `<myapp>.app.src` という名前のファイルを `src/` の一部として持つことを好む人もいます。彼らが使用するビルドシステムは、すべてをクリーンに保つために、このファイルを `ebin/` にコピーするか、生成します。

アプリケーションファイルの基本構造は単純です

{application, ApplicationName, Properties}.

ここで、ApplicationName はアトム、Properties はアプリケーションを記述する `{Key, Value}` タプルのリストです。OTP はこれらを使用してアプリケーションの機能などを把握します。これらはすべてオプションですが、常に持ち歩くのに役立ち、一部のツールには必要です。実際、ここではそのサブセットのみを見て、必要に応じて他のものを紹介します。

{description, "アプリケーションの説明"}

これはシステムにアプリケーションが何であるかを簡単に説明します。このフィールドはオプションで、デフォルトは空の文字列です。少なくとも読みやすくなるので、常に説明を定義することをお勧めします。

{vsn, "1.2.3"}

アプリケーションのバージョンを示します。文字列は任意の形式を取ることができます。通常は `<major>.<minor>.<patch>` のようなスキームに固執することをお勧めします。アップグレードとダウングレードを支援するツールを使用する場合、文字列はアプリケーションのバージョンを識別するために使用されます。

{modules, ModuleList}

アプリケーションがシステムに導入するすべてのモジュールのリストが含まれています。モジュールは常に最大で 1 つのアプリケーションに属し、2 つのアプリケーションのアプリファイルに同時に存在することはできません。このリストにより、システムとツールはアプリケーションの依存関係を確認し、すべてが適切な場所にあること、およびシステムに既にロードされている他のアプリケーションと競合がないことを確認できます。標準の OTP 構造を使用し、 *rebar3* のようなビルドツールを使用している場合、これは自動的に処理されます。

{registered, AtomList}

アプリケーションによって登録されたすべての名前のリストが含まれています。これにより、OTP は複数のアプリケーションをまとめてバンドルしようとしたときに名前の衝突が発生するタイミングを知ることができますが、開発者が適切なデータを提供することを信頼することに完全に基づいています。これは常に当てはまるとは限らないことを私たちは皆知っているので、この場合は盲目的な信頼は使用しないでください。

{env, [{Key, Val}]}

これは、アプリケーションの設定として使用できるキーと値のリストです。実行時に `application:get_env(Key)` または `application:get_env(AppName, Key)` を呼び出すことで取得できます。最初のものは、呼び出しの時点でどのアプリケーションにいるかに関係なく、アプリケーションファイル内の値を見つけようとします。2 番目のものは、特定のアプリケーションを指定できます。これは必要に応じて(起動時または `application:set_env/3-4` を使用して)上書きできます。

全体として、これは設定データを格納するのに非常に便利な場所です。どこに格納するかなどをよく知らずに、さまざまな形式の設定ファイルを読み取る必要がありません。誰もが設定ファイルで Erlang 構文を使用することを好むわけではないため、人々はしばしば独自のシステムを展開する傾向があります.

{maxT, Milliseconds}

これは、アプリケーションが実行できる最大時間であり、その後シャットダウンされます。これはめったに使用されないアイテムであり、Milliseconds のデフォルトは `infinity` なので、通常はこれを気にする必要はありません。

{applications, AtomList}

アプリケーションが依存するアプリケーションのリスト。Erlang のアプリケーションシステムは、アプリケーションが起動する前に、それらがロードおよび/または起動されていることを確認します。すべてのアプリケーションは少なくとも `kernel` と `stdlib` に依存しますが、アプリケーションが `ppool` の起動に依存している場合は、リストに `ppool` を追加する必要があります。

**注:** はい、標準ライブラリと VM のカーネルはそれ自体がアプリケーションです。つまり、Erlang は OTP を構築するために使用される言語ですが、そのランタイム環境は OTP に依存して動作します。循環しています。これは、言語が公式に「Erlang/OTP」と名付けられている理由を理解するのに役立ちます。

{mod, {CallbackMod, Args}}

アプリケーションビヘイビア(次のセクションで説明します)を使用して、アプリケーションのコールバックモジュールを定義します。これは OTP に、アプリケーションの起動時に `CallbackMod:start(normal, Args)` を呼び出す必要があることを伝えます。この関数の戻り値は、OTP がアプリケーションの停止時に `CallbackMod:stop(StartReturn)` を呼び出すときに使用されます。人々は CallbackMod にアプリケーションと同じ名前を付ける傾向があります。

これで、今のところ(そしてあなたが書くほとんどのアプリケーションで)必要なことのほとんどを網羅しました。

プールの変換

これを実際に試してみませんか?前の章の `ppool` プロセスセットを基本的な OTP アプリケーションに変換します。そのためには、まずすべてを適切なディレクトリ構造に再配布します。5 つのディレクトリを作成し、次のようにファイルを配布します。

ebin/
include/
priv/
src/
	- ppool.erl
	- ppool_serv.erl
	- ppool_sup.erl
	- ppool_supersup.erl
	- ppool_worker_sup.erl
test/
	- ppool_tests.erl
	- ppool_nagger.erl

`ppool_nagger` をテストディレクトリに移動しました。これには正当な理由があります。それはデモケースに過ぎず、アプリケーションとは関係ありませんが、テストには依然として必要です。アプリがすべてパッケージ化されたら後で試して、すべてがまだ機能することを確認できますが、今のところは役に立ちません。

後でコンパイルして実行するのに役立つように、Emakefile(適切に `Emakefile` という名前で、アプリのベースディレクトリに配置)を追加します。

{"src/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.
{"test/*", [debug_info, {i,"include/"}, {outdir, "ebin/"}]}.

これは、コンパイラーに `src/` と `test/` のすべてのファイルに `debug_info` を含めるように指示し、`include/` ディレクトリを探すように指示し(必要な場合)、ファイルを `ebin/` ディレクトリに配置するように指示します。

そういえば、`ebin/` ディレクトリに アプリファイル を追加しましょう。

{application, ppool,
 [{vsn, "1.0.0"},
  {modules, [ppool, ppool_serv, ppool_sup, ppool_supersup, ppool_worker_sup]},
  {registered, [ppool]},
  {mod, {ppool, []}}
 ]}.

これは、必要なフィールドのみが含まれています。`env`、`maxT`、`applications` は使用されません。コールバックモジュール(`ppool`)の動作を変更する必要があります。具体的にはどうすればよいでしょうか?

まず、アプリケーションビヘイビアを見てみましょう。

**注:** すべてのアプリケーションは `kernel` および `stdlib` アプリケーションに依存していますが、それらは含めていません。Erlang VM を起動するとこれらのアプリケーションが自動的に起動するため、`ppool` は引き続き機能します。明示性のために追加したいと思うかもしれませんが、今のところ *必要* ありません。

Parody of Indiana Jones' scene where he substitutes a treasure for a fake weight. The piece of gold has 'generic' written on it, and the fake weight has 'specific' on it

アプリケーションビヘイビア

これまでに見てきたほとんどの OTP 抽象化と同様に、必要なのは事前に構築された実装です。Erlang プログラマーは、慣習としてのデザインパターンに満足していません。彼らは、それらのためのしっかりとした抽象化を望んでいます。これはアプリケーションのビヘイビアを提供します。ビヘイビアは常に、汎用コードを特定のコードから分離することです。それらは、特定のコードが独自の実行フローをあきらめ、汎用コードで使用されるコールバックの束としてそれ自体を挿入するという考えを表しています。簡単に言えば、ビヘイビアは退屈な部分を処理し、あなたは点を結びます。アプリケーションの場合、この汎用部分は非常に複雑であり、他のビヘイビアほど単純ではありません。

VMが最初に起動するたびに、アプリケーションコントローラと呼ばれるプロセスが(`application_controller`という名前で)起動されます。これは他のすべてのアプリケーションを起動し、それらのほとんどを統括します。実際、アプリケーションコントローラはすべてのアプリケーションのスーパーバイザのような役割を果たすと言えます。どのような種類の監視戦略があるかは、「カオスからアプリケーションへ」セクションで説明します。

注: アプリケーションコントローラは、技術的にはすべてのアプリケーションを統括しているわけではありません。1つの例外はカーネルアプリケーションで、これは`user`という名前のプロセスを起動します。`user`プロセスは実際にはアプリケーションコントローラのグループリーダーとして機能し、カーネルアプリケーションは特別な処理を必要とします。ここではこれについて気にする必要はありませんが、正確を期すために含めるべきだと感じました。

Erlangでは、IOシステムはグループリーダーと呼ばれる概念に依存しています。グループリーダーは標準入出力と標準出力を表し、すべてのプロセスによって継承されます。グループリーダーとIO関数を呼び出すプロセスが通信するための、隠れたIOプロトコルがあります。グループリーダーは、これらのメッセージをあらゆる入出力チャネルに転送する役割を担い、このテキストの範囲内では関係のない魔法を織り成します。

さて、誰かがアプリケーションを起動しようとすると、アプリケーションコントローラ(OTP用語ではしばしばACと表記されます)はアプリケーションマスターを起動します。アプリケーションマスターは、実際には各アプリケーションを担当する2つのプロセスであり、アプリケーションを設定し、アプリケーションのトップレベルのスーパーバイザとアプリケーションコントローラの間の仲介役として機能します。OTPは官僚主義であり、多くの階層の中間管理職が存在します!ほとんどのErlang開発者は実際に気にする必要がなく、ドキュメントもほとんど存在しないため(コードがドキュメントです)、ここでは詳細には触れません。アプリケーションマスターは、アプリのナニー(かなり狂ったナニーですが)のような役割を果たすとだけ覚えておいてください。それは子供と孫を見守り、何か問題が発生すると、逆上して家系図全体を終了します。子供を容赦なく殺すことは、Erlang開発者の間ではよくある話題です。

複数のアプリケーションを持つErlang VMは、次のようになります。

The Application controller stands over three application masters (in this graphic, in real life it has many more), which each stand on top of a supervisor process

これまでは、動作の一般的な部分を見てきましたが、具体的な部分はどうでしょうか?結局のところ、これは実際にプログラムする必要があるすべてです。アプリケーションコールバックモジュールが機能するために必要な関数はごくわずかです。`start/2`と`stop/1`です。

最初の関数は`YourMod:start(Type, Args)`という形式を取ります。今のところ、`Type`は常に`normal`になります(受け入れられる他の可能性は、後で説明する分散アプリケーションに関係しています)。`Args`は、アプリファイルからのものです。この関数はアプリのすべてを初期化し、`{ok, Pid}`または`{ok, Pid, SomeState}`の2つの形式のいずれかでアプリケーションのトップレベルスーパーバイザのPidを返すだけで済みます。`SomeState`を返さない場合、デフォルトで`[]`になります。

`stop/1`関数は、`start/2`によって返された状態を引数として取ります。これは、アプリケーションの実行が完了した後に実行され、必要なクリーンアップのみを実行します。

以上です。巨大な汎用部分と小さな特定部分。そのことに感謝してください。なぜなら、残りの部分を何度も書きたくないからです(必要であればソースを見てください!)アプリケーションをより詳細に制御するためにオプションで使用できる関数がいくつかありますが、今のところそれらは必要ありません。つまり、`ppool`アプリケーションに進むことができます!

カオスからアプリケーションへ

アプリファイルとアプリケーションの動作方法の一般的なアイデアがあります。2つの簡単なコールバック。ppool.erlを開き、次の行を変更します

-export([start_link/0, stop/0, start_pool/3,
         run/2, sync_queue/2, async_queue/2, stop_pool/1]).

start_link() ->
    ppool_supersup:start_link().

stop() ->
    ppool_supersup:stop().

代わりに次の行に変更します

-behaviour(application).
-export([start/2, stop/1, start_pool/3,
         run/2, sync_queue/2, async_queue/2, stop_pool/1]).

start(normal, _Args) ->
    ppool_supersup:start_link().

stop(_State) ->
    ok.

その後、テストがまだ有効であることを確認できます。古いppool_tests.erlファイル(前の章で作成し、ここで戻しました)を選択し、`ppool:start_link/0`への単一の呼び出しを次のように`application:start(ppool)`に置き換えます

find_unique_name() ->
    application:start(ppool),
    Name = list_to_atom(lists:flatten(io_lib:format("~p",[now()]))),
    ?assertEqual(undefined, whereis(Name)),
    Name.

また、`ppool_supersup`から`stop/0`を削除する時間を取る必要があります(そしてエクスポートを削除します)。OTPアプリケーションツールがそれを処理してくれるからです。

最後にコードを再コンパイルし、すべてのテストを実行して、すべてがまだ機能することを確認できます(後でその*eunit*の仕組みについて説明します。心配しないでください)

$ erl -make
Recompile: src/ppool_worker_sup
Recompile: src/ppool_supersup
...
$ erl -pa ebin/
...
1> make:all([load]).
Recompile: src/ppool_worker_sup
Recompile: src/ppool_supersup
Recompile: src/ppool_sup
Recompile: src/ppool_serv
Recompile: src/ppool
Recompile: test/ppool_tests
Recompile: test/ppool_nagger
up_to_date
2> eunit:test(ppool_tests).
  All 14 tests passed.
ok

いくつかの場所で`timer:sleep(X)`を使用してすべてを同期しているため、テストの実行には時間がかかりますが、上記のようにすべてが機能していることを示すはずです。良いニュースです。アプリは正常です。

新しく素晴らしいコールバックを使用して、OTPアプリケーションの素晴らしさを学ぶことができます

3> application:start(ppool).
ok
4> ppool:start_pool(nag, 2, {ppool_nagger, start_link, []}).
{ok,<0.142.0>}
5> ppool:run(nag, [make_ref(), 500, 10, self()]).
{ok,<0.146.0>}
6> ppool:run(nag, [make_ref(), 500, 10, self()]).
{ok,<0.148.0>}
7> ppool:run(nag, [make_ref(), 500, 10, self()]).
noalloc
9> flush().
Shell got {<0.146.0>,#Ref<0.0.0.625>}
Shell got {<0.148.0>,#Ref<0.0.0.632>}
...
received down msg
received down msg

ここでの魔法のコマンドは`application:start(ppool)`です。これは、アプリケーションコントローラにppoolアプリケーションを起動するように指示します。`ppool_supersup`スーパーバイザを起動し、その時点からすべてを通常どおりに使用できます。現在実行中のすべてのアプリケーションは、`application:which_applications()`を呼び出すことで確認できます

10> application:which_applications().
[{ppool,[],"1.0.0"},
 {stdlib,"ERTS  CXC 138 10","1.17.4"},
 {kernel,"ERTS  CXC 138 10","2.14.4"}]

なんと、`ppool`が実行されています。前述のように、すべてのアプリケーションは、どちらも実行中の`kernel`と`stdlib`に依存していることがわかります。プールを閉じたい場合は

11> application:stop(ppool).

=INFO REPORT==== DD-MM-YYYY::23:14:50 ===
    application: ppool
    exited: stopped
    type: temporary
ok

これで完了です。前の章の厄介な`** exception exit: killed`ではなく、少し有益なレポートとともにクリーンシャットダウンが実行されるようになったことに気付くはずです。

注: `application:start(MyApp)`の代わりに`MyApp:start(...)`のようなことをする人を見かけることがあります。これはテスト目的には機能しますが、実際にアプリケーションを持つことの多くの利点を損なっています。VMの監視ツリーの一部ではなくなり、環境変数にアクセスできなくなり、起動前に依存関係を確認しなくなります。できる限り`application:start/1`に固執するようにしてください。

これを見てください!アプリが*一時的*であることについて、それはどういう意味ですか?ErlangとOTPのものは、しばらくの間だけでなく、永遠に実行されることを期待して記述しています!VMはどうしてこんなことを言うのでしょうか?秘密は、`application:start`に異なる引数を指定できることです。引数に応じて、VMはアプリケーションの1つの終了に対して異なる反応を示します。VMが子供のために喜んで死ぬ愛情深い獣である場合もあります。他の場合には、それはむしろ冷酷で現実的な機械であり、種の生存のために多くの子供が死ぬことを容認します。

`application:start(AppName, temporary)`で起動されたアプリケーション
正常に終了:特別なことは何も起こりません。アプリケーションは停止しました。
異常終了:エラーが報告され、アプリケーションは再起動せずに終了します。
`application:start(AppName, transient)`で起動されたアプリケーション
正常に終了:特別なことは何も起こりません。アプリケーションは停止しました。
異常終了:エラーが報告され、他のすべてのアプリケーションが停止し、VMがシャットダウンします。
`application:start(AppName, permanent)`で起動されたアプリケーション
正常に終了:他のすべてのアプリケーションが終了し、VMがシャットダウンします。
異常終了:同じです。すべてのアプリケーションが終了し、VMがシャットダウンします。

アプリケーションに関して、監視戦略に新しいものが見られます。VMはもはやあなたを救おうとはしません。この時点で、重要なアプリケーションの1つの監視ツリー全体をクラッシュさせるほど、何かが非常に間違っていたに違いありません。これが起こると、VMはあなたのプログラムへのすべての希望を失います。狂気の定義は、毎回異なる結果を期待しながら同じことを何度も繰り返すことであることを考えると、VMは正気で死ぬことを好み、ただ諦めます。もちろん、本当の理由は何かが壊れていて修正する必要があることに関係していますが、あなたは私の言いたいことがわかるでしょう。クラッシュが発生した場合と同様に、他のアプリケーションに影響を与えることなく、`application:stop(AppName)`を呼び出すことで、すべてのアプリケーションを終了できることに注意してください。

ライブラリアプリケーション

フラットモジュールをアプリケーションにラップしたいが、起動するプロセスがなく、アプリケーションコールバックモジュールが必要ない場合はどうなりますか?

数分間髪を抜いて怒って泣いた後、残された唯一のことは、アプリケーションファイルからタプル`{mod, {Module, Args}}`を削除することです。それだけです。これは*ライブラリアプリケーション*と呼ばれます。その例が必要な場合は、Erlang `stdlib`(標準ライブラリ)アプリケーションがその1つです。

Erlangのソースパッケージがある場合は、`otp_src_<release>/lib/stdlib/src/stdlib.app.src`に移動して、以下を確認できます

{application, stdlib,
 [{description, "ERTS  CXC 138 10"},
  {vsn, "%VSN%"},
  {modules, [array,
	 ...
     gen_event,
     gen_fsm,
     gen_server,
     io,
	 ...
     lists,
	 ...
     zip]},
  {registered,[timer_server,rsh_starter,take_over_monitor,pool_master,
               dets]},
  {applications, [kernel]},
  {env, []}]}.

これはかなり標準的なアプリケーションファイルですが、コールバックモジュールがありません。ライブラリアプリケーションです。

アプリケーションをもっと深く掘り下げてみませんか?