16. OTPとは何か?

16.1. それはOpen Telecom Platformだ!

../_images/hullo.png

OTPというのはOpen Telecom Platformの略です。今となってはもうテレコムとは関係ないのですが。(テレコムアプリケーションの性質に関して言いたいことはあるのですが、いまはやめときましょう) Erlangの偉大さの半分が並列と分散によるものだとしたら、もう半分はエラー処理の能力から来るものです。 そうしたときにOTPフレームワークはやはり全体の半分に関わっています。

前の章では、言語の組み込み関数(リンク、モニター、タイムアウト、終了の捕捉など)を使ってどのように並列アプリケーションを書くかについて、よくある方法の例をいくつか見てきました。 やるべき事の順番、競合条件をどのように避けるか、プロセスはいつでも死ぬ可能性があることを常に意識するなど、ところどころでいくつか「わかった」ことがありました。 ホットコードローディングや名前付きプロセス、supervisorの追加、名前の付け方などについてもやりました。

これらのことをすべて手でやるには時間がかかりますし、時々エラーの原因となります。 予期せず忘れられてしまうようなものもありますし、落とし穴もあります。 OTPフレームワークはこういったことを本質的な事柄をライブラリのセットにまとめることで面倒を見てくれます。こういったライブラリは細心の注意を払って作成され、何年もの間バグとの戦いにおいて完全防御されています。 あらゆるErlangプログラマは使うべきです。

OTPフレームワークはアプリケーション作成を助けるように設計されたモジュールのセットであり標準です。 ほとんどのErlangプログラマは結局はOTPを使うことになり、あなたが出会うほとんどのErlangアプリケーションはこれらの標準にならう傾向にあるでしょう。

16.2. 共通プロセス、抽象化

これまでのプロセス例でなんども行なってきたものの中に、特定のタスクに合わせてすべてを分割するというのがありました。 大抵のプロセスでは、新しいプロセスをspawnする担当の関数、初期値を与える関数、メインループなどがありました。

どんなプロセスが使われていようとも、これらの部分はすべての並列プログラムで普通に存在すると分かりました。

../_images/common-pattern.png

OTPフレームワークの裏ではエンジニアと計算機科学者はこれらのパターンを見極めて、それらをたくさんの共通ライブラリにまとめました。 これらのライブラリは私たちが使ってきた抽象化とほとんど同様のコード(メッセージをタグ付けするのに参照を使うなど)からビルドされています。これらには何年も実地で使われているという利点と、自分で実装した時よりもずっと多くの注意が払われているという利点があります。 このライブラリにはプロセスを安全にspawnしたり初期化したり、フォールトトレランスな形でそれらにメッセージを送ったりすることが出来ます。 面白いことに、自分自身でこういったライブラリを使う必要はほとんどありません。 それらのライブラリが持っている抽象化はとても基礎的で普遍的なものなので、その上にもっとずっと面白いものを作られました。 これらのライブラリは私たちが使うものの中の一部に過ぎません。

../_images/abstraction-layers.png

続く章では、プロセスの共通の使い方をいくつかみて、それからそれらがどのように抽象化されるか確認したあと、汎用化させます。それから、それら一つ一つがOTPフレームワークのどのビヘイビアに対応するかも確認し、それらをどのように使うか見ていきます。

16.3. 基本的なサーバ

最初にお伝えする共通パターンはすでに使ったものです。 イベントサーバ を書くときに クライアント-サーバモデル と呼べるものを使いました。 イベントサーバははクライアントからの呼び出しを受け取って、呼び出しに対して処理をして、プロトコルに指示があれば返信します。

この章では、とても単純なサーバを使います。このサーバはサーバの本質的な性質に注目しています。 これが kitty_server です:

%%%%% Naive version
-module(kitty_server).

-export([start_link/0, order_cat/4, return_cat/1, close_shop/1]).

-record(cat, {name, color=green, description}).

%%% Client API
start_link() -> spawn_link(fun init/0).

%% Synchronous call
order_cat(Pid, Name, Color, Description) ->
    Ref = erlang:monitor(process, Pid),
    Pid ! {self(), Ref, {order, Name, Color, Description}},
    receive
        {Ref, Cat} ->
            erlang:demonitor(Ref, [flush]),
            Cat;
        {'DOWN', Ref, process, Pid, Reason} ->
            erlang:error(Reason)
    after 5000 ->
        erlang:error(timeout)
    end.

%% This call is asynchronous
return_cat(Pid, Cat = #cat{}) ->
    Pid ! {return, Cat},
    ok.

%% Synchronous call
close_shop(Pid) ->
    Ref = erlang:monitor(process, Pid),
    Pid ! {self(), Ref, terminate},
    receive
        {Ref, ok} ->
            erlang:demonitor(Ref, [flush]),
            ok;
        {'DOWN', Ref, process, Pid, Reason} ->
            erlang:error(Reason)
    after 5000 ->
        erlang:error(timeout)
    end.

%%% Server functions
init() -> loop([]).

loop(Cats) ->
    receive
        {Pid, Ref, {order, Name, Color, Description}} ->
            if Cats =:= [] ->
                Pid ! {Ref, make_cat(Name, Color, Description)},
                loop(Cats);
               Cats =/= [] -> % got to empty the stock
                Pid ! {Ref, hd(Cats)},
                loop(tl(Cats))
            end;
        {return, Cat = #cat{}} ->
            loop([Cat|Cats]);
        {Pid, Ref, terminate} ->
            Pid ! {Ref, ok},
            terminate(Cats);
        Unknown ->
            %% do some logging here too
            io:format("Unknown message: ~p~n", [Unknown]),
            loop(Cats)
    end.

%%% Private functions
make_cat(Name, Col, Desc) ->
    #cat{name=Name, color=Col, description=Desc}.

terminate(Cats) ->
    [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
    ok.

これがkittyサーバ/ストアです。ビヘイビアは究極的に単純です: 猫を記述したら猫を得るのです。 もし誰かが猫を返したら、リストに追加されて、クライアントが実際に望んだものの代わりに、自動的に次の順番として送ります。(このkitty storeにはお金のためにいるのであり、笑顔のためではありません):

1> c(kitty_server).
{ok,kitty_server}
2> rr(kitty_server).
[cat]
3> Pid = kitty_server:start_link().
<0.57.0>
4> Cat1 = kitty_server:order_cat(Pid, carl, brown, "loves to burn bridges").
#cat{name = carl,color = brown,
description = "loves to burn bridges"}
5> kitty_server:return_cat(Pid, Cat1).
ok
6> kitty_server:order_cat(Pid, jimmy, orange, "cuddly").
#cat{name = carl,color = brown,
description = "loves to burn bridges"}
7> kitty_server:order_cat(Pid, jimmy, orange, "cuddly").
#cat{name = jimmy,color = orange,description = "cuddly"}
8> kitty_server:return_cat(Pid, Cat1).
ok
9> kitty_server:close_shop(Pid).
carl was set free.
ok
10> kitty_server:close_shop(Pid).
** exception error: no such process or port
     in function  kitty_server:close_shop/1

モジュールのソースコードを見直してみると、前に使ったパターンが見られます。 モニターを上げ下げしている場所、タイマーを使っている場所、データを受け取る場所、メインループを使う場所、初期化関数を扱う場所などが見られます。 これらは結局いつも繰り返し使っているので抽象化できるできるべきです。

クライアントAPIを見てみましょう。最初に気がつくことは、同期呼び出しはともに非常に似ているということです。 これらの呼び出しは前の節で触れたように抽象化ライブラリになりやすいものです。 いまのところ、これらをkittyサーバの一般的な部分をすべて集めた新しいモジュールの1つの関数に抽象化するだけにします:

-module(my_server).
-compile(export_all).

call(Pid, Msg) ->
    Ref = erlang:monitor(process, Pid),
    Pid ! {self(), Ref, Msg},
    receive
        {Ref, Reply} ->
            erlang:demonitor(Ref, [flush]),
            Reply;
        {'DOWN', Ref, process, Pid, Reason} ->
            erlang:error(Reason)
    after 5000 ->
        erlang:error(timeout)
    end.

これはメッセージとPIDを引数にとり、関数に渡して、安全にメッセージを転送します。 これから、この送るメッセージを関数の呼び出しに単純に置き換えます。 なので、新しいkittyサーバを書き換えて、抽象化された my_server と一緒にするなら、このような書き始めになるでしょう:

-module(kitty_server2).
-export(start_link/0, order_cat/4, return_cat/1, close_shop/1]).

-record(cat, {name, color=green, description}).

%%% Client API
start_link() -> spawn_link(fun init/0).

%% Synchronous call
order_cat(Pid, Name, Color, Description) ->
    my_server:call(Pid, {order, Name, Color, Description}).

%% This call is asynchronous
return_cat(Pid, Cat = #cat{}) ->
    Pid ! {return, Cat},
    ok.

%% Synchronous call
close_shop(Pid) ->
    my_server:call(Pid, terminate).

次の大きなコードの塊は call/2 関数ほど自明ではありません。 これまでに書いたすべてのプロセスにはすべてのメッセージがパターンマッチされるループがありました。 これはちょと面倒な点ですが、パターンマッチをループ自身から切り離さないといけません。 これを素早く行うには、次を追加するのがいいでしょう:

loop(Module, State) ->
    receive
        Message -> Module:handle(Message, State)
    end.

特定のモジュールはこのような形になります:

handle(Message1, State) -> NewState1;
handle(Message2, State) -> NewState2;
...
handle(MessageN, State) -> NewStateN.

今度はいいですね。しかしまだ綺麗に出来る方法があります。 kitty_server モジュールを注意深く読んでいれば(たぶん皆さんはしていると思いますが?!)、同期呼び出しと非同期呼び出しの方法があったことに気づいたと思います。 これは一般的なサーバ実装においてどんな呼び出し方がどのような実装になるかをはっきりさせるためには非常に役に立つでしょう。

これを実現するには my_server:loop/2 で異なるメッセージをマッチする必要があります。 つまり同期呼び出しが、関数の2行目あるメッセージに sync というアトムを追加することで明示的に行われるように call/2 関数を少し書き換えてやる必要がある、ということです。

call(Pid, Msg) ->
    Ref = erlang:monitor(process, Pid),
    Pid ! {sync, self(), Ref, Msg},
    receive
        {Ref, Reply} ->
            erlang:demonitor(Ref, [flush]),
            Reply;
        {'DOWN', Ref, process, Pid, Reason} ->
            erlang:error(Reason)
    after 5000 ->
        erlang:error(timeout)
    end.

これで非同期呼び出し用に新しい関数を提供できます。 cast/2 関数はこうなります:

cast(Pid, Msg) ->
    Pid ! {async, Msg},
    ok.

これで、 loop はこうなります:

loop(Module, State) ->
    receive
        {async, Msg} ->
            loop(Module, Module:handle_cast(Msg, State));
        {sync, Pid, Ref, Msg} ->
            loop(Module, Module:handle_call(Msg, Pid, Ref, State))
    end.
../_images/sink.png

そして、同期/非同期の概念には適さないメッセージ(事故で送信されてしまったものとか)を扱う、あるいはデバッグ用の関数やホットコードリロードのような物のための特定のスロットも追加することが可能です。

上の loop で1つ残念な点は抽象化が漏れている点です。 my_server を使うプログラマは同期メッセージやそれに対して返信をする時に参照について知る必要があります。 これでは抽象化の意味がありません。これを使うために退屈な詳細な実装をすべて知るようがあるのです。 それをさっと直してみました:

loop(Module, State) ->
    receive
        {async, Msg} ->
            loop(Module, Module:handle_cast(Msg, State));
        {sync, Pid, Ref, Msg} ->
            loop(Module, Module:handle_call(Msg, {Pid, Ref}, State))
    end.

PidRef という変数をタプルの中に入れることで、他の関数には From というような名前の変数を1つだけ渡してやればよくなります。 これでユーザは変数の内部については何も知る必要がなくなります。 代わりに、 From に何が含まれているかわかっている関数を提供します:

reply({Pid, Ref}, Reply) ->
    Pid ! {Ref, Reply}.

あとやるべきことはモジュール名に渡す開始関数( start, start_link, init )とそれ以外の関数を決めることです。 決まったら、モジュールの見た目はこのようになるでしょう:

-module(my_server).
-export([start/2, start_link/2, call/2, cast/2, reply/2]).

%%% Public API
start(Module, InitialState) ->
    spawn(fun() -> init(Module, InitialState) end).

start_link(Module, InitialState) ->
    spawn_link(fun() -> init(Module, InitialState) end).

call(Pid, Msg) ->
    Ref = erlang:monitor(process, Pid),
    Pid ! {sync, self(), Ref, Msg},
    receive
        {Ref, Reply} ->
            erlang:demonitor(Ref, [flush]),
            Reply;
        {'DOWN', Ref, process, Pid, Reason} ->
            erlang:error(Reason)
    after 5000 ->
        erlang:error(timeout)
    end.

cast(Pid, Msg) ->
    Pid ! {async, Msg},
    ok.

reply({Pid, Ref}, Reply) ->
    Pid ! {Ref, Reply}.

%%% Private stuff
init(Module, InitialState) ->
    loop(Module, Module:init(InitialState)).

loop(Module, State) ->
    receive
        {async, Msg} ->
            loop(Module, Module:handle_cast(Msg, State));
        {sync, Pid, Ref, Msg} ->
            loop(Module, Module:handle_call(Msg, {Pid, Ref}, State))
    end.

次にするのは kitty サーバの再実装です。 ここで、 kitty_server2my_server に対応するコールバックモジュールです。 すべての呼び出しが my_server にリダイレクトされる以外は前の実装と同様のインターフェースを維持します:

-module(kitty_server2).

-export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).
-export([init/1, handle_call/3, handle_cast/2]).

-record(cat, {name, color=green, description}).

%%% Client API
start_link() -> my_server:start_link(?MODULE, []).

%% Synchronous call
order_cat(Pid, Name, Color, Description) ->
    my_server:call(Pid, {order, Name, Color, Description}).

%% This call is asynchronous
return_cat(Pid, Cat = #cat{}) ->
    my_server:cast(Pid, {return, Cat}).

%% Synchronous call
close_shop(Pid) ->
    my_server:call(Pid, terminate).

モジュールの先頭に2つ目の -export() を追加したことに注意して下さい。 これらはちゃんと動作するために my_server が呼び出す必要がある関数です:

%%% Server functions
init([]) -> []. %% no treatment of info here!

handle_call({order, Name, Color, Description}, From, Cats) ->
    if Cats =:= [] ->
        my_server:reply(From, make_cat(Name, Color, Description)),
        Cats;
       Cats =/= [] ->
        my_server:reply(From, hd(Cats)),
        tl(Cats)
    end;

handle_call(terminate, From, Cats) ->
    my_server:reply(From, ok),
    terminate(Cats).

handle_cast({return, Cat = #cat{}}, Cats) ->
    [Cat|Cats].

あと必要があるのはプライベート関数を最追加することです:

%%% Private functions
make_cat(Name, Col, Desc) ->
    #cat{name=Name, color=Col, description=Desc}.

terminate(Cats) ->
    [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
    exit(normal).

ok を前に作った terminate/1 内の exit(normal) に置き換えることを忘れないようにして下さい。 そうしないと、サーバは動き続けてしまいます。

コードがコンパイルしてテストできるようになり、前と全く同様に動作するようになったはずです。 コードは極めて似ていますが、変わったところを見てみましょう。

16.4. 特殊 対 一般

いま行ったことは、(概念的な意味で)OTPのコアを理解することでした。 これはまさにOTPがなにか、というものを示しています。つまり、すべての一般的なコンポーネントを取り出して、ライブラリに入れて、コードがちゃんと動くことを確認して、使えそうな時に再利用可能にするということです。 残されているのは、特定の事象、つまりアプリケーションごとに変わるものに注目することです。

見てすぐに分かる通り、 kitty サーバでやったことだけを考えると、労力を削減できた量は大してありません。 単に抽象化のための抽象化をしたようなものです。もし顧客に納品するものが kitty サーバだけなら、最初のバージョンのほうが好ましいです。 より大きなアプリケーションを作る場合は汎化した部分と特有の部分を切り離す価値があるでしょう。

では、これから暫くの間はサーバ上で動作している幾つかのErlangソフトウェアがある、という想像をしてみましょう。 私たちのソフトウェアには幾つかの kitty サーバが走っていて、ほかにも病院プロセス(壊れた kitty プロセスを送ると直してくれる)と、 kitty 美容院、ペットフードや消耗品などのサーバ、などがあるとします。 これらのほとんどがサーバ-クライアントパターンで実装できます。 時間が経つに連れ、この複雑なシステムが、いくつもの異なるサーバが走っているものになります。

サーバを追加することはコードの観点では複雑さを追加することになり、テストの観点からすると維持と理解が難しくなるとことになります。 それぞれの実装は異なり、別々の人に異なるスタイルでプログラムされているのでしょう。 しかしながら、もしこれらのサーバ全てが共通の my_server の抽象化を共有していたら、著しく複雑さを減らすことができるでしょう。 モジュールの基礎概念を理解しました。(「なるほど、それがサーバか!」と)そこにはテスト、ドキュメントなどに関する1つの汎用的な実装があるのです。 残りの努力はその上に載る個々の特有の実装に費やせば良いのです。

../_images/dung.png

つまり、バグをトラックして解決するための時間を大幅に減らせるということです。 (すべてのサーバに対して1箇所だけ修正すればいいだけです) これはまた、生み出すバグの数も減らせるということです。 もし my_server:call/3 を書きなおす、もしくはプロセスのメインループをいつも書きなおすとしたら、時間の浪費というだけではなく、途中の処置を忘れる確率が一気に高まって、結果バグにつながります。 バグを少なくすれば、夜中に電話で対応に呼び出される回数も減りますし、それは私達みんなにとって確実に幸せなことです。 マイルは貯まるかもしれませんが、休みの日に喜んでオフィスに行ってバグを直したい人はいないでしょう。

他に汎用部分を切り分けることで他に面白いことといえば、素早く個々のモジュールをずっと簡単にテストできるようにするというところでしょう。 古い kitty サーバの実装で単体テストをしようと思ったら、テストごとにプロセスをspawnして、それに正しい状態を与えて、メッセージを送って、期待通りの返信が来るのを待つ、という流れを踏む必要があります。 一方で、2番目の kitty サーバは ‘handle_call/3‘ と ‘handle_cast/2‘ を使って関数を呼び出して、新しい状態として何を出力するかを見ればよいだけです。 サーバの設定と状態の操作をしてやる必要がないのです。関数の引数に渡してやればよいだけです。 これはまたサーバの汎用部分ずっとテストしやすくなった、ということを覚えておいて下さい。 確認したい動作だけするようなとても簡単な関数を実装してやれば良いのです。

共通の抽象化を使うことの「隠れた」利点は、もし皆がプロセスに全く同じバックエンドを使えば、誰かが単一のバックエンドをちょっとだけ速くするような最適化をしたときに、それを使っているすべてのプロセスが速くなるということです。 この原理を実践するために、普通は多くの人が同じ抽象化を使って労力をそれに費やす必要があります。 幸運にもErlangコミュニティでは、それこそがまさにOTPフレームワークに起きていることです。

私たちのモジュールの話に戻りましょう。まだ触れていないことがたくさんあります。名前付きプロセス、タイムアウトの設定、デバッグ情報の付与、予期しないメッセージの処理、ホットコードローディングを結びつけ、特定のエラーハンドリング、より多くのレシピの抽象化、多くのサーバシャットダウンの対応、スーパーバイザーを確実にうまく使えるようにするなどです。 これらすべてをここで扱うには量が多すぎますが、実際に納品する製品には必要なことです。 繰り返しになりますが、なぜこれらすべてを行うのかを自分で検証するのは少々リスクがあります。 幸運にも、あなた(そしてアプリケーションをサポートする人々)には、Erlang/OTPチームがこれらすべてを gen_server ビヘイビアで面倒を見てくれています。 gen_server は、何年にも及ぶテストと製品での使用がされているという点以外には、 my_server にステロイドを追加したようなものです。