すごいErlangゆかいに学んだメモ 第14章 OTPの紹介


OTPというか、gen_server の概要を掴むためのメモ


## 汎用サーバーモジュールを作る

- サーバー共通部分

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

%%% 公開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}.

%%% プライベート関数
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.
```

- アプリケーションコード


```erlang
-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}).

%%% クライアントAPI
start_link() -> my_server:start_link(?MODULE, []).

%% 同期呼び出し
order_cat(Pid, Name, Color, Description) ->
    my_server:call(Pid, {order, Name, Color, Description}).

%% 非同期呼び出し
return_cat(Pid, Cat = #cat{}) ->
    my_server:cast(Pid, {return, Cat}).

%% 同期呼び出し
close_shop(Pid) ->
    my_server:call(Pid, terminate).

%%% サーバー関数
init([]) -> []. %% ここでは何もしない

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].

%%% プライベート関数
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).
```

### my_server, kitty_server2 の概要

- my_server.erl には軽量プロセス化されるメインループ(loop関数)がある
	- my_server:call, my_server:cast はこのメインループのPidに対してsendする
	- メインループは渡された引数を元にサーバー関数である handle_cast, handle_call を呼び出す
- kitty_server2 はサーバーの機能を呼び出すための公開APIと、実際に処理されるhandle_cast, handle_call が定義されている
	- kitty_server2自体はメインループを持たない
		- メインループやサーバーの状態保存は my_serverモジュールが持つ
	- kitty_server2 はサーバーに実際に提供したい機能のみを定義すれば良い
		- サーバーとして機能は my_serverモジュールが持つ
- この my_server モジュールがOTPの gen_server の概要的モデルとなる

### 動作イメージ
- my_server, kitty_server2 は大体以下のような関係、役割分担となる。

![my_server_and_kitty_server2.png](https://qiita-image-store.s3.amazonaws.com/0/4428/3d934168-2866-359a-dc80-f4195599efcb.png "my_server_and_kitty_server2.png")

----

## gen_server ビヘイビアのコールバック関数

### init/1
サーバー初期化関数

- 返り値
    - `{ok, State}`
        - `State` がサーバーが保持する状態として、プロセスのメインループへ渡される。
    - `{ok, State, TimeOut}`
        - `TimeOut` でメッセージ受信の締切を設定可能
        - タイムアウト時に `timeout` メッセージが送信される
            - タイムアウトメッセージは `handle_info/2` で処理する
        - 普通は `erlang:start_time/3` などで手動制御するほうが良い
    - `{ok, State, hibernate}`
        - メッセージ受信待ちの間、処理能力を犠牲にして省メモリ化する
        - メッセージ受信の間隔が長くなると予想される場合に使うこともある
    - `{stop, Reason}`
        - 処理失敗時に返す
    - `ignore`

### handle_call/3
同期メッセージの処理関数

- 引数
    - `Request`
    - `From`
    - `State`
- 返り値
    - `{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}`

----

- `TimeOut`, `hibernate` の役割は `init/1` と同じ。
- `noreply` のときは自分で返信処理を実装する
    - 返信を遅延させたいときなど
    - 返信処理を忘れるとタイムアウトしてクラッシュする
    - 返信処理には `gen_server:reply/2` を使う

### handle_cast/2
非同期メッセージの処理関数

- 引数
    - `Message`
    - `State`
- 返り値
    - `{noreply, NewState}`
    - `{noreply, NewState, TimeOut}`
    - `{noreply, NewState, hibernate}`
    - `{stop, Reason, NewState}`

### handle_info/2
`handle_call/3` や `handle_cast/2` で予期していないメッセージを処理する関数
`!` 演算子で直接送られてきたメッセージや、 `init/1` の `timeout`、モニターからの通知、 `EXIT`シグナルなどを処理する

- 引数
    - `Message`
    - `State`
- 返り値
    - `{noreply, NewState}`
    - `{noreply, NewState, TimeOut}`
    - `{noreply, NewState, hibernate}`
    - `{stop, Reason, NewState}`

### termminate/2
プロセスの終了処理を実装する

- 以下のタイミングで呼ばれる
    - `handle_*` な関数が `{stop, Reason, NewState}` or `{stop, Reason, Reply, NewState}` が返したとき
    - `gen_server` が終了を補足している場合に、親プロセスが死んだとき
- 引数
    - `Reason`
    - `State`

### code_change/3
コードアップグレード処理

- 引数
    - `PreviousVersion`
    - `State`
    - `Extra`

----

## gen_server の関数

### gen_server:start_link/3-4, start/3-4
コールバックモジュールの `init/1` を呼び出す

- 引数
    1. コールバックモジュール
        - 例えば同じモジュール内にコールバック関数が定義されていたら `?MODULE` と指定すればよい
    2. `init/1` にわたすEralng項
    3. 実行中のサーバーのデバッグオプション
    4. `{local, 名前}` せサーバーの登録名を指定できる

呼び出し例

```erlang
gen_server:start_link(?MODULE, [], []).
```

### gen_server:call/2-3
コールバックモジュールの `handle_call/3` を呼び出す

- 引数
    - サーバープロセスのPid
    - `handle_call` にわたすErlang項
    - タイムアウト
        - 指定なしの場合はデフォルトで5秒に設定される
        - タイムアウト時はクラッシュする
        - `infinity` 指定でタイムアウト無し

呼び出し例

```erlang
gen_server:call(Pid, {order, Name, Color}).
```

### gen_server:cast/2
コールバックモジュールの `handle_cast/2` を呼び出す

- 引数
    - サーバープロセスのPid
    - `handle_cast` にわたすErlang項

呼び出し例

```erlang
gen_server:cast(Pid, {return, State}).
```