Elixir/GenServerの内部の動きってどうなっているだ?


はじめに
-----
GenServerの動作ってどうなっているのか、と元のErlangのコードを調べてみて、その内容と「GenServerって何だ」との自分になり理解した事を記述します。

GenServer(Generic Server)って何
----
読み込んだり、書き込んだり、また、外部に通知する機能を持ちデータを管理するサーバー(データを提供する人)との事。
そのサーバーの機能を一般化(generic)し、使い易いようにしたもので、上記3つの機能をapp側で作る必要がないようになっている。
ただ、app側から見るとcallback関数ばかりとなり、動きがよくわからず、戸惑う事が多い気がする。
実際には
```elixir
defmodule Stack do
  use GenServer

  - - -
end
```
上記`Stack`モジュールには隠れているタスク(init_it())とAPI関数の処理があり、そこからcallbackされる事により動作している。`use GenServer`の定義により、そのモジュール自体(Stack)がGenServerの機能を持つ事となる。
データの型(state)はなんでも良く、各callback関数内で更新される事となる。逆にcallback関数内で型を随時変更可能となる。データはどこかに保存されるものでなく、関数の引数として、保持されている。

GenServerの関数
-----
主な関数として以下のものがある。
* 起動:GenServer.start_link(module, init_arg, options \\ [])
　　GenServerのタスクを起動し、init()をcallbackする
* データ取得(返信あり):GenServer.call(server, request, timeout \\ 5000)
  　上記タスクにメッセージを送信し、`handle_call` が呼ばれ、応答待ち解放により戻る
* データ更新(返信なし):GenServer.cast(server, request)
    上記タスクにメッセージを送信し、`handle_cast`が呼ばれる
* 情報通知(GenServerへの情報通知) handle_info()
    上記タスクがメッセージを受けた時にcallbackを呼ぶ

GenServerの使用例
----
```elixir
defmodule Stack do
  use GenServer

  # Client ラッパー
  def start_link(state) when is_list(state) do
    GenServer.start_link(__MODULE__, state, name: :stack) # pidを:stackに名付ける
  end
  def push(element) do  # pid=:stack
    GenServer.cast(:stack, {:push, element})
  end
  def pop() do          # pid=:stack
    GenServer.call(:stack, :pop)
  end

  # Server (callbacks)
  @impl true
  def init(stack) do    # start_link()のcallback
    {:ok, stack}
  end
  @impl true
  def handle_call(:pop, _from, [head | tail]) do   # GenServer.callのcallback
    {:reply, head, tail}
  end
  @impl true
  def handle_cast({:push, element}, state) do   # GenServer.castのcallback
    {:noreply, [element | state]}
  end
end
```

ラッパー関数を準備するのが一般的
----
次のように直接GenServerを呼べば動作する。
```
iex> GenServer.start_link(Stack, [], name: :stack)
{:ok, #PID<0.153.0>}
iex> GenServer.cast(:stack, {:push, "first"})
:ok
iex> GenServer.call(:stack. :pop)
"first"
```
このように直接でも問題はないが、一般的にラッパー関数(上記例、start_link(),push(),pop())を準備する事が多い。
ちなみに、cast()、call()はpidを渡すが、ここでは起動時に`name: :stack`の設定より、pidと`:stack`が関連付けられ、`:stack`が使用できるようにしている。

個人的には違うNodeからremoteで使用する事が多いので、あまりこのラッパーを準備する事はしていない。


start_link()の動き
----
```mermaid
sequenceDiagram
    autonumber
    actor UserModule
    participant callback as callback init()
    participant genfunc as GenServer

    actor GenUserTask as GUserTsk(init_it())
    UserModule->>+genfunc : start_link() 
    Note left of callback: (module, init_arg, options \\ [])
    genfunc-->>GenUserTask : spawn()
    Note left of GenUserTask: state<init_arg>
    GenUserTask->>+callback : init()
    Note left of GenUserTask: state
    callback->>-GenUserTask : return
    Note left of genfunc: {:ok, state}
    GenUserTask-->>genfunc : send ack
    genfunc->>-UserModule : return
    Note left of genfunc: {:ok, pid<GUserTsk>}
    GenUserTask->>GenUserTask: loop()
```
1. start_link(module, init_arg, option)によりmodule内にGenServerの機能が準備される。
2. 内部のタスク`init_it()`を起動する。その時のinit_argが初期のデータ(state)となる。
3. 起動された内部のタスクからcallback関数、init()が呼ばれる。引数はstateとなり、データを渡す。
4. init()callback関数から戻る時に`{:ok, state}`とデータが戻り、データ更新される。このinit()関数は無くても、defaultとして準備されている。
5. init()が呼ばれ、内部のタスクの処理が終了した時点で受信応答待ちとなっているapiにackとしてmsg送信する。
6. start_link()のreturnとして、`{:ok, pid}`を戻す事により、内部のタスクのpidがわかる。

call()の動き
-----

```mermaid
sequenceDiagram
    autonumber
    actor UserModule
    participant callback as handle_call()
    participant genfunc as GenServer
    actor GenUserTask as GUserTsk(loop())

    UserModule->>+genfunc : GenServer.call()
    Note left of callback: (server, request, timeout \\ 5000)
    genfunc-->>GenUserTask : send $gen_call + msg
    GenUserTask->>+callback : handle_call()
    Note left of GenUserTask: (request, from, state)
    callback->>-GenUserTask : return
    Note left of genfunc: {:reply, reply, new_state}
    GenUserTask-->>genfunc : send msg
    genfunc->>-UserModule : return
    Note left of genfunc: reply<state>
```

1. `call(pid<server>, request, timeout)`によりapi関数に通知する。
2. 内部タスクに`request`のmsgを送信する。この時、`$gen_call`の情報付加し、内部タスクがcall関数である事を認識する。そして、api関数内で応答待ち(receive)となる。
3. 内部タスクからcallback関数、handle_call()を呼ぶ。引数 `(request, from, state)`、このrequest情報により、どのcallbackなのか、判定されている。
4. 戻り、`{:reply, reply, new_state}`、により応答送信、戻り値、データ更新する。
5. api関数内で応答待ちに対して、応答データ(reply)とともにmsg送信する
6. api関数はデータ(reply)を戻す。


cast()の動き
-----
```mermaid
sequenceDiagram
    autonumber
    actor UserModule
    participant callback as handle_cast()
    participant genfunc as GenServer
    actor GenUserTask as GUserTsk(loop())

    UserModule->>+genfunc : GenServer.cast() 
    Note left of callback: (server, request)
    genfunc-->>GenUserTask : send $gen_cast + msg
    genfunc->>-UserModule : return
    Note left of genfunc: :ok
    GenUserTask->>+callback : handle_cast()
    Note left of GenUserTask: (request, state)
    callback->>-GenUserTask : return
    Note left of genfunc:  {:noreply, new_state}
```
1. `cast(pid<server>, request)`により情報をapi関数に渡す
2. 内部タスク(pid)に対して、`request`に`$gen_cast`を付加したmsgを送信する。
3. castは応答待ちをせず、そのまま戻る。
4. 内部タスクはmsgを受けるとcallback関数、`handle_cast()`を`{requst, state}`で呼ぶ、`request`の情報によりどのhandle_cast()かが判定される。
5. callback関数から戻り`{:noreply,new_state}`により新しいデータを更新する。



情報通知(GenServerへの情報通知)の動き
-----
```mermaid
sequenceDiagram
    autonumber
    actor UserModule
    participant callback as handle_info()
    participant genfunc as GenServer
    actor GenUserTask as GUserTsk(loop())

    callback-->>GenUserTask : send msg
    UserModule-->>GenUserTask : send msg

    GenUserTask->>+callback : handle_info()
    Note left of GenUserTask: (msg, state)
    callback->>-GenUserTask : return
    Note left of GenUserTask:  {:noreply, new_state}
```
1. callback関数から内部タスク(pid)にmsg送信する。
2. 他の処理から内部タスク(pid)にmsg送信する。
3. 内部タスクはcallback関数、handle_info()をmsg受信により呼ぶ。`{msg, state}`
4. handle_info()からの戻り、`{:noreply, new_state}`によりデータ更新する。

`callback`関数内でのreceiveのようなwait処理は禁止
----
内部のタスク内からcallback関数が呼ばれているので、関数内でwait処理があるとGenServerの動きが壊される。そのために、callback関数は素直に戻る必要がある。
GenServer.start_link()、call()などの関数内でwaitしているものがあるので、その点注意が必要となる。

まとめ
----
これまで、正常動作の処理を記述したが、エラー処理やtimeout処理、そして、start()、start_link()の違いなどのoptionよる動作があり、複雑に絡んでいる。
erlangのコードを見ていた結果、理解するのに困難な代物で、二度と見たくないと思わされるものだった。(感想)

この資料、`GenServer`を使う上での参考になれば幸いです。