22. OTPアプリケーションを作る

22.1. なんでそれが欲しかったんだっけ?

../_images/construction.png

アプリケーションの監視ツリー全体が1つの関数呼び出しで同時に起動するのを見た後に、なぜもっと物事をすでにあるものよりも複雑にしたいのだろうと疑問に思うことでしょう。 監視ツリーの背後にある概念はいくばくか複雑で、システムが最初に立ち上がったとき、すべてのツリーとサブツリーをスクリプトで手動で起動したと思うことが出来ました。 そのあとは、一仕事終えたと外に行って、午後を丸々動物の形に似た雲を探すことで過ごしました。

まあ、そんな感じもいいでしょう。 そうしたい気持ちは分かります。(特に、雲を探しに行く部分はよくわかります。なんてったっていまの流行りはクラウドコンピューティングですからね) しかしながら、プログラマーやエンジニアによって成されたたいていの抽象化に関しては、OTPアプリケーションはたくさんの汎用化され整理されたアドホックシステムの結果です。 もし、あなたの監視ツリーを起動するために、あなた自身で上で描写したような一連のスクリプトとコマンドを作ろうとし、一緒に働いている他の開発者は彼ら自身のスクリプトを作ろうとしたら、あっという間にめんどくさい問題を抱えることになるでしょう。 そして誰かが「もしみんながすべてを起動するのに同じようなシステムを使ったらいいと思いません?さらにいえば、あらゆるアプリケーションが同じような構造をしていたらもっといいと思いません?」とか言い出すのです。

OTPアプリケーションは、まさにこのような問題を解決するためのものです。 ディレクトリ構造や、設定の扱い方、依存関係の処理方法、環境変数や設定の生成、アプリケーションの起動と停止、そして衝突の検知やアプリケーションを落とすことなくライブアップデートなどをを安全に制御する方法などを提供してくれます。

もし、あなたがこのような機能(一貫した構造や、そのために開発されたツールなどのOTPが提供する細やかな機能)を欲しない、というのでなければ、この章は面白いものになるでしょう。

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

前の章で書いた 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ソースファイルを。

../_images/carpool.png

前に作ったテストファイル用に test/ ディレクトリを追加したことに注意してください。 その理由は、テストは一般的ではあるけれども、アプリケーションの一部として配布する際には必ずしも必要ないものだからです。―テストは開発中には必要で、マネージャにあなたの書いたコードが正しい事を訴えるためだけに必要です。(「テストは通りました。なぜそのアプリケーションが人を殺したのかが全然わかりません。」) このようなディレクトリは場合によって必要になったときに追加されます。 1例としては doc/ ディレクトリです。これは EDoc ドキュメントをアプリケーションに追加するときに作られるディレクトリです。

作るべき4つのディレクトリとしては ebin/, include/, priv/, srv/ で、これらはこれから見るであろうすべてのOTPアプリケーションでかなり共通な構成です。もっとも本物のOTPシステムがデプロイされたとき ebin/priv/ だけがエクスポートされるわけですが。

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

さて、これからどうしましょう。 最初にすることはアプリケーションファイルを追加することです。 このファイルはErlang VMにアプリケーションが何かを伝えます。つまり、どこからアプリケーションが開始して、どこでアプリケーションが終了するかなどです。 このファイルは、すべてのコンパイル済みモジュールと一緒に ebin/ ディレクトリで保存されます。

このファイルは通常 <yourapp>.app (今回は ppool.app )という名前が付けられ、VMが理解できるようにアプリケーションを定義する大量のErlang項が書かれています。(VMは推測するということがものすごく苦手です!)

Note

このファイルを ebin/ の外に置き、 src/ の中に <myapp>.app.src を置く方を好む人もいます。 どのようなビルドシステムも、すべてを綺麗に保つために、それを ebin/ にコピーするか生成します。

アプリケーションファイルの基本構成は単純に:

22.3.1. {application, ApplicationName, Properties}.

ApplicationName はアトム、 Properties はアプリケーションを表す {Key, Value} タプルのリストです。 これらはOTPがアプリケーションが何をして何をしないのかを理解するために使われます。これらはすべてオプションなのですが、いつもツールによってはいつも役に立ったり、必要だったりします。 事実、ここではそのサブセットだけ触れることにして、必要になったらほかのものも紹介します:

22.3.2. {description, "Some description of your application"}

これは、システムにアプリケーションが何であるかの短い説明文を与えます。 フィールドは任意で、デフォルトは空文字列になっています。 いつも説明文を書くことをおすすめします。理由は単純に可読性が上がるからです。

22.3.3. {vsn, "1.2.3"}

これはアプリケーションのバージョンを知らせるものです。 文字列はどんなフォーマットも受け付けます。 通常は、 <major>.<minor>.<minor> やそれに似た形式にするのが得策でしょう。 アップグレードやダウングレードをするツールを使うときに、その文字列がアプリケーションのバージョンを識別する際に使われます。

22.3.4. {modules, ModuleList}

これは、アプリケーションがシステムに導入するすべてのモジュールのリストです。 モジュールは常に最大1つのアプリケーションに属し、2つ以上のアプリケーションのappファイルには存在できません。 このリストによって、システムとツールがアプリケーションが依存しているものを見て、すべてが必要とされている場所にあるかを確認し、他のアプリケーションがすでにシステムにロードしたモジュールと衝突することがないかを確認することができます。 もし標準のOTPの構造を使っていて、 rebar のようなビルドツールを使っているのであれば、この処理は自動で行われます。

22.3.5. {registered, AtomList}

これはアプリケーション内に登録されたすべての名前のリストです。 このリストによって、たくさんのアプリケーションをひとまとめにしようとしたときに、いつ名前の衝突があるかをOTPが知ることができますが、リスト自体はまったくもって開発者の善意に委ねられています。 常に開発者が正しい情報を記載するとは限らないと知っているので、この情報は盲信するべきではありません。

22.3.6. {env, [{Key, Val}]}

これはアプリケーションの設定用に用いられるキーと値のリストです。 これらの値は実行時に application:get_env(Key) または application:get_env(AppName, Key) を呼び出すことで取得できます。 前者は、呼び出した際に所属しているアプリケーションのアプリケーションファイルから値を探そうとします。後者は特定のアプリケーションを指定することが出来ます。 この値は必要があれば上書きすることが可能です。(起動時でも application:set_env/3-4 でも可能です)

これは設定データを保存する上では、大量かつ任意のフォーマットで、どこに何を保存すべきかわからないような設定ファイルを使うよりは、かなり便利な場所です。 設定ファイルにErlangの構文を使いたがらない人は、意地でも独自のシステムを使いたがります。

22.3.7. {maxT, Milliseconds}

これはアプリケーションが稼働できる最大の時間で、それを過ぎるとアプリケーションは終了されます。 これは滅多に使われない項目で、 Milliseconds の初期値は infinity となっています。したがって、この項目を気にする必要はまずありません。

22.3.8. {applications, AtomList}

あなたのアプリケーションが依存しているアプリケーションのリストです。 Erlangのアプリケーションシステムは、あなたのアプリケーションがこれらのアプリケーションのロードと起動の両方またはいずれか一方がされた後に、あなたのアプリケーションが起動されるようにします。 すべてのアプリケーションは少なくとも kernelstdlib に依存しているので、もしあなたのアプリケーションが ppool に依存している場合には、 ppool をリストに追加すべきです。

Note

そうです、標準ライブラリとVMのカーネルはそれ自身がアプリケーションで、これはErlangはOTPを作るために使われている言語ですが、Erlangのランタイム環境は動作するためにOTPに依存しているということになります。 これは循環参照ですね。 この言語が「Erlang/OTP」と公式に呼ばれているのも、納得できますね。

22.3.9. {mod, {CallbackMod, Args}}

これは、アプリケーションビヘイビアを使って、アプリケーションのコールバックモジュールを定義します。(アプリケーションビヘイビアについては次の章で説明します。) これはOTPに、アプリケーション起動時に CallbackMod:start(normal, Args) を呼び出すべきだと伝えます。 またアプリケーション停止時には CallbackMod:stop(Args) を呼び出します。 CallbackMod はアプリケーション名にちなんで命名するのが慣例です。

以上で、ここまで(とたいていのアプリケーション)で必要なことはカバーしました。

22.3.10. プールを変換する

これを実践で使ってみたらどうなるのでしょうか。 前の章の 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_naggertest/ ディレクトリに移したことにお気づきでしょう。 これには納得がいく理由があります。 ppool_nagger はデモ以上のことはせず、アプリケーションに何も影響を及ぼさないけれども、テストには必要だからです。 アプリケーションがパッケージングされたら、実際に試してみましょう。それでもすべてきちんと動作することが分かることでしょう。しかしいまのところは役に立ちません。

後ほどコンパイルして実行するときに使えるようにEmakefile( Emakefile と命名され、アプリケーションのベースディレクトリに配置されるでしょう)を追加します:

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

これはコンパイラに src/test/ 内のすべてのファイルの debug_info を含め、(必要であれば) include/ ディレクトリを見にいって、ファイルを ebin/ ディレクトリに入れるように伝えます。

これに関して、 app ファイルを 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 )の動作を変更する必要があります。 どうしたらいいのでしょうか。

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

Note

たとえすべてのアプリケーションが kernelstdlib アプリケーションに依存していたとしても、それをインクルードしませんでした。 それでもErlang VMがこれらのアプリケーションを自動的に起動するので ppool は動作します。 明示的にしたいと思い、 kernelstdlib を追加したくなるかもしれませんが、いまはその必要はありません。

../_images/indiana.gif

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

これまで見てきたOTPによる抽象化のように、私たちが欲しかったものは事前定義済みの実装です。 Erlangプログラマには命名規則としてのデザインパターンは嬉しくありません。Erlangプログラマはしっかりとした抽象化が必要なのです。 ビヘイビアはいつも汎用コードを特定の用途のコードから切り離すことであったことを思い出して下さい。 特定のコードが独自の実行フローを持つことを辞めさせ、汎用コードに使われる大量のコールバックとして表現させることに寄与しています。 簡単にいえば、ビヘイビアはあなたが点と点とをつなぐ中での退屈な部分を扱ってくれます。 アプリケーションの場合は、この汎用的な部分が非常に複雑で、他のビヘイビアほど単純ではありません。

VMが最初に起動したときはいつでも、アプリケーションコントローラと呼ばれるプロセスが起動します。(これは application_controller という名前がついています。) アプリケーションコントローラは他のすべてのアプリケーションを起動し、それらの上位にいます。 事実、アプリケーションコントローラはすべてのアプリケーションのスーパバイザのように振る舞うと言えます。 どのような監視戦略があるのかは カオスからアプリケーションへ の章で見ていきます。

Note

アプリケーションコントローラは技術的にはすべてのアプリケーションの上位にあるわけではありません。 例外の1つにカーネルアプリケーションがあります。これは user という名前のプロセスを起動します。 user プロセスは事実アプリケーションコントローラに対してグループリーダーのように振る舞いまい、したがってカーネルアプリケーションは特別扱いを必要とします。 これに関して気にする必要はありませんが、正確を期すために説明しました。

Erlangでは、入出力システムはグループリーダーと呼ばれる概念に依存しています。 グループリーダーは標準入出力を表わしていて、すべてのプロセスに継承されています。 グループリーダーと入出力関数を呼び出しているプロセスがやりとりするために隠し入出力プロトコルがあります。 グループリーダーはそれらのメッセージを入力/出力チャンネルに転送し、本書の範疇の以内では私たちには関係のない魔法を織りなす役割を担っています。

いずれにせよ、だれかがアプリケーションを起動したいと思ったとき、アプリケーションコントローラ(OTP用語では AC と記述されます)がアプリケーションマスターを起動します。 アプリケーションマスターは事実個々のアプリケーションの世話をする2つのプロセスです。これらのプロセスはアプリケーションの最上部にあるスーパバイザとアプリケーションコントローラの間の仲介人のように振る舞います。 OTPは官僚で、多くの中間管理層があります! たいていのErlang開発者は詳しい内容は気にしませんし、それに関する資料がほとんど無いので(コードがドキュメントです)、この詳細に関しては深入りしません。 アプリケーションマスターはアプリケーションの子守り(かなり狂った子守ですが)のようなものだとだけ理解してください。 この子守は、見ている子供の子供や孫まで見渡し、すべてが終わった時には凶暴になって家系を根絶やしにします。 Erlangerにとって子供を虐殺するのはよくある話題です。

たくさんのアプリケーションがあるErlang VMはこのような状態になっているでしょう:

../_images/application-controller.png

ここまで、まだビヘイビアの汎用的な部分を見ていましたが、特定の用途に関する部分はどう実装されるのでしょうか。 結局、ここが私たちが実際にプログラムしなければいけない部分です。 アプリケーションコールバックモジュールは非常に少ない関数のみを必要としています。 start/2stop/1 です。

前者は、 YourMod:start(Type, Args) のような形式で呼び出します。 いまは、 Type は常に normal で(他に考えられる値も分散アプリケーションでは処理する必要があります。これに関しては後で見ていきます。) Argsapp ファイルから取得されるものです。 start/2 関数はアプリケーションに関するすべての初期化を行い、アプリケーションの最上位のスーパバイザのPidを返す必要があります。戻り値の形式は {ok, Pid}{ok, Pid, SomeState} のいずれかです。 SomeState を返さない場合は、デフォルトの [] となります。

stop/1 関数は start/2 によって返される状態を引数にとります。 この関数はアプリケーションが稼働を辞めたあとに動作し、必要な後片付けだけ行います。

以上です。 汎用部分が非常に大きく、特定用途の部分は非常に小さいですね。 start/2stop/1 以外のコードを頻繁には書きたくないでしょうから、書くべき場所が少ないことに感謝しましょう。(ちょっとソースコードを見てみましょう!) アプリケーションの制御をもうちょっと踏み込んで行いたい場合には、さらにいくつか任意で使用できる関数があるのですが、いまのところこは必要ありません。 さて、 ppool アプリケーションを先に進めましょう!

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

これまでappファイルとアプリケーションの動作に関する知識を得ました。 また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/0application: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 が動いています。 先に触れたように、すべてのアプリケーションは kernelstdlib に依存していることが、両方共稼働していることから確認できます。 プールを閉じたい場合は次のようにします:

11> application:stop(ppool).

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

これで終わりです。 前の章の ** exception exit: killed from でごちゃごちゃしたレポートよりも、すっきり短く情報が得やすいレポートを見ることで、綺麗に終了できたことが確認できるでしょう。

Note

application:start(MyApp) の代わりに MyApp:start(...) のような関数を使っているコードを見ることがあるでしょう。 これはテスト用途にはうまくいきますが、実際にアプリケーションを作る際の多くの利点を無駄にしています。 application:start(MyApp) としないことで、 MyApp はVMの監視ツリーからは外れ、環境変数にアクセスできず、起動する前の依存関係の確認もしません。 できるだけ application:start/1 を使うようにしましょう。

この終了ログを見てください! 私たちのアプリケーションが temporary になっているのはなぜでしょう。 ErlangとOTPの流儀に沿うように書いたのは、ちょっとの間ではなく、永遠に稼働するようにしたかったからです! どうしてVMはこんなことをいってるんでしょうか。 秘密は application:start に異なる引数を与えることができることにあります。 引数に応じて、VMはアプリケーションを終了するときに異なる反応をします。 ある状況ではVMは子供の犠牲になる愛情あふれる野獣になり、他の状況では絶滅しそうな動物が死にかけているのを甘んじて受け入れる冷徹で実利的なマシンと化します。

application:start(AppName, temporary) で起動したアプリケーション:
正常終了の場合:特になにも起きません。アプリケーションは停止します。 異常終了の場合:エラーが報告され、アプリケーションは再起動せずに終了します。
application:start(AppName, transient) で起動したアプリケーション:
正常終了の場合:特になにも起きません。アプリケーションは停止します。 異常終了の場合:エラーが報告され、他のすべてのアプリケーションは停止され、VMが終了します。
application:start(AppName, permanent) で起動したアプリケーション:
正常終了の場合:他のアプリケーションは終了し、VMが終了します。 異常終了の場合:正常終了時と同様で、すべてのアプリケーションは終了し、VMが終了します。

アプリケーションになった場合、いくつか新しい監視戦略が出てきました。 もはやVMはあなたを守ってはくれません。 ここでは、なにかとってもとってもおかしな事が起きて、監視ツリー内の致命的なアプリケーションのところまでそれが伝播して、十分クラッシュさせうるものが起きたとしましょう。 これが起きたとき、VMはプログラム中ですべての希望を失ってしまいました。 もし狂気の定義が、毎回違う結果を期待しながら何度も同じ事を繰り返すいうことであれば、VMは正気で死に、ただ諦めることでしょう。 もちろん、本当の理由は修正が必要な壊れた部分と関係あるのですが、私の言いたいことはわかるでしょう。 すべてのアプリケーションは、クラッシュが起きても application:stop(AppName) を呼び出すことで、他のアプリケーションに影響を与えることなく終了できることも押さえておきましょう。

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

アプリケーション内でフラットなモジュールをラップしたいけれど、起動するプロセスがなく、それゆえにアプリケーションコールバックモジュールが必要ない場合にはどうしたらいいでしょうか。

2、3分怒髪天を逆撫でながら叫んだ後、残された方法は {mod, {Module, Args}} をアプリケーションファイルから削除することだけです。 それで終わりです。 これはライブラリアプリケーションと呼ばれます。 例を見たければ、Erlangの stdlib (標準ライブラリ)アプリケーションがその一例です。

もし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, []}]}.

非常に標準的なアプリケーションファイルですが、コールバックモジュールがないことがわかりますね。 これがライブラリアプリケーションです。

アプリケーションについてもっと深く見てみませんか。