17. クライアントとサーバ

17.1. コールバック・トゥ・ザ・フューチャー

../_images/cbttf.png

まず最初に見ていくOTPビヘイビアは最もよく使われるものの一つです。 名前は gen_server で、これは前の章で my_server に書いたものとちょっと似たインターフェースを持っています。 使う関数は2、3しかないです。その代わりにあなたのモジュールに gen_server が使う関数がいくつかないといけません。

17.1.1. init

最初の1つは init/1 関数です。これはサーバの状態を初期化して、サーバが依存する1度きりのタスクをすべて行うという点で、 my_server で使ったものと似ています。 この関数は {ok, State}, {ok, State, TimeOut, {ok, State, hibernate, {stop, Reason} あるいは ignore です。

通常の {ok, State} の返り値は State がそれ以降維持される状態としてプロセスのメインループに直接渡されることを過去に言っているので、特に説明の必要はありません。 TimeOut 変数はサーバがメッセージを受け取るのに締め切りが欲しい時にタプルに追加して下さい。 もし締め切りまでにメッセージが1つもこなかったら、特別なメッセージ (timeout というアトム)がサーバに送られます。これは handle_info/2 に処理されるべきです。(後述します)

一方で、返信を受け取るまでに長い時間を確保したくて、メモリが心配なときは、タプルに hibernate というアトムを追加できます。 ハイバネーションは基本的に、処理能力を犠牲にして、メッセージを受け取るまでプロセスの状態のサイズを減らします。 もしハイバネーションを使うことに懐疑的であれば、使う必要はないでしょう。

{stop, Reason} は初期化の途中でおかしなことが起きた時に返されるべきです。

Note

ここでプロセスのハイバーネーションに関してもう少し技術的な話をします。これは理解したり心配するには全く及ばない話です。BIFの erlang:hibernate(M,F,A) が呼ばれた時、現在走っているプロセスのコールスタックは破棄されます。(関数は返り値を二度と返しません) ガベージコレクションが作動したらプロセス内のデータサイズまで縮んだ継続ヒープだけが残ります。 これは基本的にすべてのデータを小さくするので、プロセスが取る場所は小さくなります。

プロセスがメッセージを受け取ると、 A を引数に取った関数 M:F が呼ばれ、処理が再開します。

Note

init/1 が動いている間、サーバを生成したプロセス内の処理はブロックされます。 これはプロセスが、すべてちゃんと動いていることを確かめるために gen_server モジュールから自動的に送られてくる’ready‘メッセージを待っているからです。

17.1.2. handle_call

関数 handle_call/3 は同期メッセージと連携するときに使われます。(同期メッセージの送り方についてはすぐに触れます) この関数は Request, From, State の3つの引数を取ります。 my_server で実装した handle_call/3 ととても良く似ていますね。 最も大きな違いはメッセージの返し方です。 私たちが行った独自のサーバ抽象化では、プロセスにメッセージを返すために my_server:reply/2 を使う必要がありました。 gen_server では、8つの戻り値があり、それらはタプル形式です。

戻り値がたくさんあるので、一覧で並べてみました:

{reply,Reply,NewState}
{reply,Reply,NewState,Timeout}
{reply,Reply,NewState,hibernate}
{noreply,NewState}
{noreply,NewState,Timeout}
{noreply,NewState,hibernate}
{stop,Reason,Reply,NewState}
{stop,Reason,NewState}

すべてにおいて、 Timeouthibernateinit/1 に対して行なっていたのと同様です。 Reply に何が入っていようと、それがと最初にサーバを呼び出したところに送り戻されます。 noreply に3つのオプションがあることに注意して下さい。 noreply を使うと、サーバの全体的な部分は自分自身に送る返信に関する処理をしている、と想定するでしょう。 この処理は、 my_server:reply/2 で使われたのと同様に gen_server:reply/2 で行うことができます。

たいていの場合は、タプルを返すだけで良いでしょう。 さらに、 noreply を使う正当な理由が理由がいくつかあります。 たとえばあなたに返信を送る他のプロセスが欲しい時はいつでも、あるいは確認応答(‘メッセージ受け取ったよ!’)を送りたいけれど、まだ(今回は返信しない)プロセスが続くとき、などです。 もしここに挙げたのような事をしたいのであれば、絶対に gen_server:reply/2 を使う必要があると言えましょう。なぜなら、そうしなければ呼び出しがタイムアウトして、クラッシュの原因となってしまうからです。

17.1.3. handle_cast

handle_cast/2 のコールバックは my_server で実装したものととても似た動作をします。 MassageState を引数に取り、非同期呼び出しを処理します。 そのコールバックではやりたいことは、 handle_call/3 で出来たこととほぼ同様になんでもできます。 一方で、戻り値は reply がないタプルだけが有効な値となります:

{noreply,NewState}
{noreply,NewState,Timeout}
{noreply,NewState,hibernate}
{stop,Reason,NewState}

17.1.4. handle_info

私達のインターフェースに合わないメッセージは上手く扱えないことに関して私が言ったことは覚えていますよね。 そこで handle_info/2 の登場です。 handle_cast/2 にとてもよく似ていて、事実同じタプルを返します。 違いは、コールバックが、 ! 演算子で直接送られてきたメッセージや、 init/1timeout のような特別なメッセージ、モニターの通知や 'EXIT' シグナルのためだけに存在する、ということです。

17.1.5. terminate

コールバック関数 terminate/2 は3つの handle_xxx 関数が {stop, Reason, NewState} または {stop, Reason, Reply, NewState} という形式のタプルを返した時にいつでも呼ばれます。 この関数は、 ReasonState という引数を2つとり、これらの引数は stop のタプルにあるものの値と同じものとなります。

また terminate/2 は、その親(自身を生成したプロセス)が死んだ時、かつ gen_server が終了を捕捉していた場合にも呼ばれます。

Note

normal, shutdown または {shutdown, Term} 以外の理由で terminate/2 が呼ばれた場合、OTPフレームワークはこれを失敗として、役に立つようあちこちにたくさんのログを取り始めます。

この関数は init/1 とは正反対であり、 terminate/2 の結果もまた init/1 で行われたことの正反対となります。 この関数はいわばあなたのサーバの守衛であり、全員が出ていったかを確認した後にドアをロックする役割を果たしている関数なのです。 もちろん、この関数はVM自身にサポートされているものであり、VMはあなたの代わりにすべてのETSテーブルを削除したり、すべてのポートを閉じたりしてくれています。 この関数が呼び出された後にコードは処理を停止してしまうので、この関数の戻り値が重要なのではないということに注意してください。

17.1.6. code_change

関数 code_change/3 はコードをアップグレードするためにあります。 この関数は code_change(PreviousVersion, State, Extra). のような形で呼び出します。 ここで、 PreviousVersion という引数は、アップグレードする場合は自身のバージョン項(バージョン項について忘れてしまった人は モジュールとは何か を読んでください)を指定し、ダウングレード(古いコードを再読み込みするだけ)の場合は {down, Version} を指定します。 State 変数は、現在のすべてのサーバの状態を保持していて、変換して使えるようになっています。

orddictを使ってすべてのデータを保存していた時のことを想像してみてください。時が経ち、orddictでは処理が遅すぎるようになってしまったので、通常のdictに変更することにしました。 次の関数呼び出しの時にプロセスがクラッシュするのを避けるために、あるデータ構造から別のデータ構造への変換はそこで、安全に、行うことができます。 新しい状態を {ok, NewState} として返すだけで良いのです。

../_images/kitty.png

Extra 変数はいまは木にする必要の無いものです。 この変数はもっと大きなOTPデプロイに使われるもので、その場合はVM上のリリース全体をアップグレードする特別なツールも存在しています。 まだ、それについて触れるのは早いので、ここでは触れません。

これで定義されたコールバックをすべてを見ることが出来ました。 もしちょっとわからなくなってしまったとしても心配しないでください。OTPフレームワークは循環的に定義されているところがあって、フレームワークのAという部分を理解するためにはBという部分を理解する必要がありのに、Bを理解するにはAを参照する必要があったりします。 こうした混乱を上手く乗り越える最善の方法は、実際に gen_server を実装してみることです。

17.2. .BEAM me up, Scotty! (スコッティ、転送を頼む)

ここからは kitty_gen_server というものを作ってみましょう。 これは kitty_server2 とほぼ同じもので、APIの最小限の変更にとどめています。 まず、新しいモジュールを次のような行から書き始めます:

-module(kitty_gen_server).
-behaviour(gen_server).

これをコンパイルしてみます。おそらく次のような出力となるでしょう:

1> c(kitty_gen_server).
./kitty_gen_server.erl:2: Warning: undefined callback function code_change/3 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_call/3 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_cast/2 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_info/2 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function init/1 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function terminate/2 (behaviour 'gen_server')
{ok,kitty_gen_server}

コンパイルは通りましたが、コールバックが見つからない旨の警告があります。 これは gen_server のビヘイビアによるものです。 ビヘイビアは通常、あるモジュールが、他のモジュールにおいて持っていると期待する関数を特定する方法です。 ビヘイビアとは、コード中の正常に動作する汎用的な部分と(あなたが書くような)特定のエラーを起こしがちな部分の間のやりとりを隠す契約です。

Note

‘behavior’ でも ‘behaviour’ でもErlangコンパイラでは受け付けてくれます

ビヘイビアを定義するのは非常に簡単です。 次のように実装される behaviour_info という関数をexportするだけです:

-module(my_behaviour).
-export([behaviour_info/1]).

%% init/1, some_fun/0 and other/3 are now expected callbacks
behaviour_info(callbacks) -> [{init,1}, {some_fun, 0}, {other, 3}];
behaviour_info(_) -> undefined.

これでビヘイビアに関してはおしまいです。 それらのビヘイビアを実装しているモジュール -behaviour(my_behaviour). を使うことができ、関数を忘れた場合にはコンパイラから警告を受け取れるようになりました。 とにかく、私達の3つめのkittyサーバの話に戻りましょう。

最初に定義していた関数は start_link/0 でした。 これは次のように変更できます:

start_link() -> gen_server:start_link(?MODULE, [], []).

最初の引数はコールバックモジュールで、2つ目は init/1 に渡す引数のリスト、3つ目はデバッグオプションで、3つ目については今は扱いません。 サーバを登録する際の名前を決める、4つ目の引数を追加することも出来ました。 前のバージョンでは関数は単純にpidを返していた一方で、今回は代わりに {ok, Pid} を返していることに注目してください。

次の関数は、このようになります:

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

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

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

これらの呼び出しはすべて1対1の変更となります。 3つ目の引数を gen_server:call/2-3 にタイムアウト値として渡すこともできる事に留意してください。 関数にタイムアウト値を設定しない(あるいは、 infinity というアトムを設定しない)のであれば、デフォルト値は5秒に設定されます。 時間切れになる前に返信を受け取れなかった場合は、呼び出しはクラッシュします。

これで、 gen_server のコールバックを追加することが出来るようになりました。 次の表は呼び出しとコールバックの対応表です:

gen_server YourModule
start/3-4 init/1
start_link/3-4 init/1
call/2-3 handle_call/3
cast/2 handle_cast/2

他にも特別な場合に関するコールバックもあります:

  • handle_info/2
  • terminate/2
  • code_change/3

では、モデルに合うようにすでに実装している init/1, handle_call/3, handle_cast/3 を変更することから始めてみましょう。

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

handle_call({order, Name, Color, Description}, _From, Cats) ->
    if Cats =:= [] ->
        {reply, make_cat(Name, Color, Description), Cats};
       Cats =/= [] ->
        {reply, hd(Cats), tl(Cats)}
    end;
handle_call(terminate, _From, Cats) ->
    {stop, normal, ok, Cats}.

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

再び、非常に小さな変更があります。 事実、賢い抽象化のおかげで、コードは短くなりました。 つぎは、新しいコールバックにとりかかりましょう。 最初は handle_info/2 です。 これがトイモジュールで、事前定義されたロギングシステムがないと考えると、想定外のメッセージを出力するだけで十分でしょう:

handle_info(Msg, Cats) ->
    io:format("Unexpected message: ~p~n",[Msg]),
    {noreply, Cats}.

次は terminate/2 コールバックです。 これはプライベート関数として実装した terminate/1 に非常に似ています:

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

最後のコールバックは code_change/3 です:

code_change(_OldVsn, State, _Extra) ->
    %% No change planned. The function is there for the behaviour,
    %% but will not be used. Only a version on the next
    {ok, State}.

make_cat/3 をプライベート関数にしておくのをお忘れなく。

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

さて、これで新しいコードを試せます:

1> c(kitty_gen_server).
{ok,kitty_gen_server}
2> rr(kitty_gen_server).
[cat]
3> {ok, Pid} = kitty_gen_server:start_link().
{ok,<0.253.0>}
4> Pid ! <<"Test handle_info">>.
Unexpected message: <<"Test handle_info">>
<<"Test handle_info">>
5> Cat = kitty_gen_server:order_cat(Pid, "Cat Stevens", white, "not actually a cat").
#cat{name = "Cat Stevens",color = white,
description = "not actually a cat"}
6> kitty_gen_server:return_cat(Pid, Cat).
ok
7> kitty_gen_server:order_cat(Pid, "Kitten Mittens", black, "look at them little paws!").
#cat{name = "Cat Stevens",color = white,
description = "not actually a cat"}
8> kitty_gen_server:order_cat(Pid, "Kitten Mittens", black, "look at them little paws!").
#cat{name = "Kitten Mittens",color = black,
description = "look at them little paws!"}
9> kitty_gen_server:return_cat(Pid, Cat).
ok
10> kitty_gen_server:close_shop(Pid).
"Cat Stevens" was set free.
ok

おお、やりました!動きました!

さて、これらの汎用化遊びで何が言えるでしょうか。 おそらく、同様の汎用化は前にも行いました。特定のコードから汎用的な部分を切り出すというのは、どのような点においても素晴らしいことです。 メンテナンスも単純になり、複雑さも減り、コードも安全になって、テストも容易になり、バグも生みにくくなります。 バグがあっても、修正しやすいです。 汎用サーバは数多ある抽象化の1つに過ぎませんが、確実に最も使われている抽象化の1つです。 次の章ではこういった抽象化やビヘイビアについてもっと見ていきます。