そもそもOTPとは
一言で言えば、Erlangで用意された便利なライブラリなどの集合体で便利ツールをまとめたものという認識をしている
OTPとはopen telecom platoformの略で当初は堅牢性が重要な電話交換機を開発するために使用されていた
今になってはElixirでもOTPを使用して気軽に様々なアプリを作成することが可能
OTPには behaviour(ビヘイビア: 振る舞い、動作)というルール(規約)、役割のようなものが数多く用意されている
ドラクエでいう職業のようなもので、それぞれのビヘイビアによって実行する内容や役割が異なる
今回、扱うのは「GenServer」と言われるビヘイビアでサーバーを実装するためのものだ
このGenServerを使用することでstateを保持するサーバーを簡単に実装することが可能
関数型言語でどのように値を保持しているかという内容を補足的に解説しておくと、以前から扱っているアキュムレーターがまさにそれである
内部的には再帰関数によってアキュムレーターによる値の保存を行なっている
しかし、それを毎度毎度、記述するのはどうなのよということでGenServerの出番というわけだ
アキュムレーターについて解説した記事もあるので宜しければ、ご覧ください
www.okb-shelf.work
また合わせて「Supervisor」と言われるプロセスの死活管理のためのビヘイビアについても軽く扱う
戦士(GenServer)と僧侶(Supervisor)の二人組のパーティを作っていくと思ってもらえれば良い
戦士(GenServer)が死亡した際に、僧侶(Supervisor)が面倒をみてくれる
Supervisorは優秀で彼が使う蘇生呪文は「ザオリク」であり、100%プロセスを復活させる(例え話ね)
そのためOTPを使ったアプリケーションは高い保守性と堅牢性とトラッキング(ログ、行動管理など)を実現することが出来る
GenServerについて
先ほども説明した通り、用意されたビヘイビアの1つで値を保持するサーバーを実装するために今回は用いる
GenServerにはクライアントのリクエストを捌くための2つの関数が用意されている
- handle_call -> 同期関数で戻り値を指定可能
- handle_cast -> 非同期関数で戻り値を指定不可
自身では戻り値があるやつとないやつ程度のレベルで理解している
このhandle_callとhandle_castがクライアントからのリクエストを受け付ける関数で第1引数のアトムでパターンマッチをして対象の関数を呼び分ける
第2引数はpid(Process Identifier(プロセス認識子))が渡り、第3引数にはサーバーで保持しているstateが渡る
今回は簡略化のためmoduleでwrapしていないが、実際にはそうではない。詳しくはまた後ほど記述するだろう
handle_call
def handle_call(:one, , ), do: {:reply, 1, 1}
def handle_call(:two, , ), do: {:reply, 2, 2}
def handle_call(:three, , ), do: {:reply, 3, 3}
handle_cast
def handle_cast(:reset, ), do: {:noreply, 0)
こんな感じでcallもcastも同名の関数でパターンマッチを使用して処理を分岐させてるんだなーという理解をしてもらえれば100点だ
他にも
- init()
- handle_info()
- terminate()
- code_change()
- form_status()
という関数も用意されているがそれぞれについて詳しくは触れない
いまさらながらスタックについて
有名なデータ構造の1つでいわゆる「後入れ先出し」という動きをとる
スタックにはpush(追加)とpop(取得&除去)という2つの動き方がある
リストを使って説明をすると以下のようになる
なお、先頭要素は1で末尾要素は5なので1.皿の上に2.皿が。2.皿の上に3.皿が....というイメージをしてみてほしい
init_stack_values = [1,2,3,4,5]
stack_values = [1,2,3,4,5,6]
stack_values = [1,2,3,4,5,6,99]
popped_value = 99
stack_values = [1,2,3,4,5,6]
popped_value = 6
stack_values = [1,2,3,4,5]
今回はこのpushとpopに加えて、現在のstackの状態を知らせるinfoという関数も実装する
プロジェクトの作成
一通り説明したところでさっそくスタックサーバーを作成する
まずはプロジェクトを新規作成する。その際に「--sup」というオプションを付与しておく
これは後にsupervisorを実装する際に必要となるので指定しておく
$ mix new --sup stack
コマンドの実行後、stackというディレクトリが生成され、中には様々なファイルが生成される
./ilb/stack にserver.exを新規作成して以下の記述をする
.lib/stack/server.ex
defmodule Stack.Server do
use GenServer
def start_link(init_lst) do
GenServer.start_link(__MODULE__, init_lst, name: __MODULE__)
end
def handle_call(:pop, , state) do
[head | tail] = Enum.reverse(state)
{:reply, head, Enum.reverse(tail)}
end
def handle_cast({:push, value}, state) do
{:noreply, state ++ [value]}
end
def handle_call(:info, , state) do
{:reply, state, state}
end
end
さっそく動作を確認してみる
GenServerのプロセスを立ち上げて、スタックの動きを確認する
$ iex -S mix
iexのプロンプトが立ち上がる
iex> Stack.Server.start_link([1,2,3])
{:ok,
iex> GenServer.call(Stack.Server, :pop)
3
iex> GenServer.cast(Stack.Server, {:push, 99})
:ok
iex> GenServer.call(Stack.Server, :info)
[1, 2, 99]
どうやら一連の動きは問題なく稼働しているようだ
しかしながら、例外を無理矢理、発生されると当然ながらエラーになる(stackを空にしてpop)
iex> GenServer.call(Stack.Server, :pop)
99
iex> GenServer.call(Stack.Server, :pop)
2
iex> GenServer.call(Stack.Server, :pop)
1
iex> GenServer.call(Stack.Server, :info)
[]
iex> GenServer.call(Stack.Server, :pop)
22:11:06.670 [error] GenServer Stack.Server terminating
** (MatchError) no match of right hand side value: []
:
一度、プロセスがcrashするともうGenServerの関数を呼び出しても反応がなくなる
iex> GenServer.call(Stack.Server, :info)
** (exit) exited in: GenServer.call(Stack.Server, :info, 5000)
:
再度、GenServer(プロセス)を立ち上げてやれば問題はない
iex> Stack.Server.start_link([11,22,33])
{:ok,
iex> GenServer.call(Stack.Server, :info)
[11, 22, 33]
しかしながら面倒だ
Elixir、Erlangでは死んだプロセスは破棄して新たにプロセスを立ち上げてやれば良いという方針で
こんな時にSupervisorが役に立つ。死んだプロセスを破棄して新たなプロセスを立ち上げてくれる
supervisorを設定する
今回はシンプルなsupervisorを設定する
iexプロンプトの立ち上げ時にGenServer(プロセス)の立ち上げとプロセスがcrashした際に新規にプロセスを立ち上げるようにしてもらう
--supをプロジェクト生成時に渡しているため、以下のファイルが生成されているはずだ
./lib/stack/application.ex
defmodule Stack.Application do
@moduledoc false
use Application
def start(, ) do
children = [
]
opts = [strategy: :one_for_one, name: Stack.Supervisor]
Supervisor.start_link(children, opts)
end
end
初期の状態は上記のようになっているので、これを以下のように変更する
defmodule Stack.Application do
@moduledoc false
use Application
def start(, ) do
import Supervisor.Spec, warn: false
children = [
worker(Stack.Server, [[1,2,3]])
]
opts = [strategy: :one_for_one, name: Stack.Supervisor]
Supervisor.start_link(children, opts)
end
プロンプトを一度、終了して再度立ち上げる
$ iex -S mix
すでにGenServer(プロセス)が立ち上がっているのが分かる
iex> GenServer.call(Stack.Server, :info)
[1, 2, 3]
そしてわざとcrashさせてみる。先ほどは再度、GenServer(プロセス)を立ち上げたが今回はどうか
iex> GenServer.call(Stack.Server, :pop)
3
iex> GenServer.call(Stack.Server, :pop)
2
iex> GenServer.call(Stack.Server, :pop)
1
iex> GenServer.call(Stack.Server, :pop)
22:31:05.161 [error] GenServer Stack.Server terminating
** (MatchError) no match of right hand side value: []
:
iex> GenServer.call(Stack.Server, :info)
[1, 2, 3]
supervisorによってGenServer(プロセス)が新たに作成されているようだ
初期値は先程、applicationの初期値として指定したものが与えられている
crash前の値を保持できないの?と当然思うが、別のプロセスを用意してそちらへ保管させることで可能である
そのやり方についてはプログラミングElixirにも記述があるが別の記事にまとようと思う
例外処理と関数の整理
最後にGenServerの利便性を上げるためにちょっとした修正をする
毎回、GenServer.call(Stack.Server, :info)とやるのは面倒だし、よりシンプルにスタックを利用したいため、関数を以下の様に整備した
またスタックが空になった時に、errorになってしまう問題を解決するために再帰関数でお馴染みのパターンマッチを用意する
defmodule Stack.Server do
use GenServer
def start_link(init_lst) do
GenServer.start_link(__MODULE__, init_lst, name: __MODULE__)
end
def pop(), do: GenServer.call(__MODULE__, :pop)
def push(val), do: GenServer.cast(__MODULE__, {:push, val})
def info(), do: GenServer.call(__MODULE__, :info)
def handle_call(:pop, , []), do: {:reply, [], []}
def handle_call(:pop, , state) do
[head | tail] = Enum.reverse(state)
{:reply, head, Enum.reverse(tail)}
end
def handle_cast({:push, value}, state) do
{:noreply, state ++ [value]}
end
def handle_call(:info, , state) do
{:reply, state, state}
end
end
こうしてやることでよりシンプルで利便性よくGenServerを利用することが可能だ
iex> Stack.Server.pop()
3
iex> Stack.Server.pop()
2
iex> Stack.Server.push(99)
:ok
iex> Stack.Server.info()
[1, 99]
iex> Stack.Server.push(999)
:ok
iex> Stack.Server.info()
[1, 99, 999]
iex> Stack.Server.pop()
[]
呼び出しがシンプルになり、例外のerrorが無事に発生しなくなった
いいね
感想というか語らせて
以前からOTPというものの存在を理解はしていたが、どちらかというとElixirのsyntaxや仕様ばかりに着目していた
今回OTPを自身でまとめてみたのには理由がある
知人の方からElixirでリモートワークを募集しているページを見せて頂いた
求人の要項には
- OTPへの理解がある方
- phoenixへの理解がある方
- 新しいものへの挑戦心
という項目が多かった。3つ目はばっちりだとしても上記の2つはよく分かっていない
特にOTPはそうで、どこで使うだとか、全くイメージできていないレベル感で「こりゃあかんな」と思った
今まで趣味レベルでのElixirへのアプローチをとってきたが少し、仕事を意識してしまったというのが今回の記事が生まれた経緯となる
というか絶対に落ちないサーバーってめちゃくちゃかっこいいやん(joe曰く99.9999%落ちない)
なんでスルーしてたんだろうと反省
ようやく入門できたかなと自己満足して終了
参考文献
プログラミングElixir
What is OTP?
Elixir初心者がOTPって結局なんなのか調べてみた
[翻訳] ElixirにおけるOTPの紹介
スタック