はじめに
elixirでシステムを組む醍醐味と言えば、プロセス指向プログラミングです。
その根幹を支えるのがerlang由来のOTPです。オブジェクト指向におけるメッセージ(いわゆるドット)に相当する便利ツールが、プロセス指向ではerlangが提供するプロセス間メッセージング及び、これをOTPとしてラップしたgen_serverやgen_eventなどになります。
オブジェクト指向ではクラスをひな形として作成したインスタンスがシステムを構成しておりました。プロセス指向においては、モジュールをひな形として作成されたプロセスがシステムを構成します。
例えばデータを一時的に納めておく場所が欲しい時、オブジェクト指向な人はデータストアクラスが欲しいなと考えます。一方でプロセス指向な人はこれを、データストアプロセスが欲しいな、と考えます。
都合、プロセスに対するアクセス手段としてコールバックやインタフェース関数が求められるところ、elixirはerlangのOTPを用いて、その実装の手間を解消しています。elixirの元となる言語がerlangではなくerlang/OTPと表記される由来の一つです。
erlangにとって言語として無くてはならないのがOTPです。
ブサメンにとっての二次元のようなものですね。
ということで、phoenixアプリケーションもまた、elixirで書かれたアプリケーションでありますから、同様にOTPを用いてプロセス指向することができます。それではphoenixアプリケーションにオレオレなプロセスを追加してみましょう。
尚、以降の作業に関しては、以下の記事でevmとkiex、erlang/OTPとexlir、Phoenixをインストールしていることが前提となります。動作の確認はcentos6で行っておりますが、centos7でも動くと思います。デビアンマンは適宜読み替えて下さい。
実装
erlang/OTPのお作法として、仕事をするプロセス(worker)は監視用のプロセス(supervisor)に監視されていることが前提です。そこで最初はsupervisorを追加します。その上でsupervisorの管理下にworkerを追加することで、アプリケーションを構成します。
phoenixのエントリポイントはlib/xxx.exです。xxxは最初にプロジェクトを作成した際のプロジェクト名となります。ここに記載されているsupervisorが最上位に位置するsupervisorとなり、その子としてsupervisorやworkerを追加することになります。
## PhoenixのSupervisorに子(Workers.Supervisor)を登録
$ vi lib/hello_world.ex
children = [
# Start the endpoint when the application starts
supervisor(HelloWorld.Endpoint, []),
# Start the Ecto repository
worker(HelloWorld.Repo, []),
# Here you could define other workers and supervisors as children
# worker(HelloWorld.Worker, [arg1, arg2, arg3]),
supervisor(HelloWorld.WorkersSupervisor, []) # ADD
]
ちなみにworker(HelloWorld.Repo)というのが、Phoenixのcontrollerでモデルの参照に利用されているRepoプロセスの起動項です。これを引っこ抜くとPhoenixはモデルに対するアクセス方法を失ってしまいます。
PhoenixのSupervisorに登録した子(HelloWorld.WorkersSupervisor)の実装
$ vi lib/hello_world/worker_supervisor.ex
defmodule HelloWorld.WorkersSupervisor do
use Supervisor
require Logger
alias HelloWorld.WorkersSupervisor
alias HelloWorld.Worker
def start_link do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def init(args) do
children = [
worker(Worker, [], restart: :transient)
]
options = [
strategy: :simple_one_for_one
]
supervise(children, options)
end
def start_worker(args \\ []) do
Supervisor.start_child(WorkersSupervisor, [args])
end
def count_workers do
Supervisor.count_children(WorkersSupervisor)
end
end
HelloWorld.WorkersSupervisorの子としてHelloWorld.Workerの実装
$ vi lib/hello_world/worker.ex
defmodule HelloWorld.Worker do
use GenServer
require Logger
def start_link(args) do
GenServer.start_link(__MODULE__, [])
end
def init(state) do
{:ok, state}
end
def handle_call(msg, from, state) do
Logger.info(msg)
{:reply, msg, state}
end
def hello(worker_pid) do
GenServer.call(worker_pid, "hello")
end
end
これでサーバ起動時にHelloWorld.WorkersSupervisorのプロセスがphoenixアプリケーションを構成するプロセスの一つとして立ち上がり、その監視下でHelloWorld.Workerを立ち上げることができるようになります。
確認
では実際にHelloWorld.WorkerをHelloWorld.WorkersSupervisorの下でHelloWorld.Workerを立ち上げてみましょう。
$ iex -S mix phoenix.server
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
[info] MQTT connection established
[info] Running HelloWorld.Endpoint with Cowboy on http://localhost:4000
Interactive Elixir (1.0.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> HelloWorld.WorkersSupervisor.count_workers
%{active: 0, specs: 1, supervisors: 0, workers: 0}
iex(2)> HelloWorld.WorkersSupervisor.start_worker
{:ok, #PID<0.304.0>}
iex(3)> HelloWorld.WorkersSupervisor.count_workers
%{active: 1, specs: 1, supervisors: 0, workers: 1}
iex(4)> HelloWorld.WorkersSupervisor.start_worker
{:ok, #PID<0.307.0>}
iex(5)> HelloWorld.WorkersSupervisor.count_workers
%{active: 2, specs: 1, supervisors: 0, workers: 2}
ポコポコとHelloWorld.Workerプロセスが立ち上がることが確認できます。
ちなみにerlangはプロセスに対してマシンリソースを均等に割り当てるよう動作します。そして、同時に起動できるプロセス数の上限は、デフォルトで32768、最大で268435456です(実際に起動可能なプロセス数はメモリ依存)。つまり相当数のプロセスを捌くことが前提となっております。これがerlangにプロセス指向プログラミングを可能とさせています。
では、こうして立ち上げたHelloWorld.Workerプロセスに対して、ちゃんと仕事(ログ出力)をしているか確認する為、メッセージを送ってみましょう。
iex(6)> {:ok, pid} = HelloWorld.WorkersSupervisor.start_worker
{:ok, #PID<0.365.0>}
iex(7)> HelloWorld.Worker.hello(pid)
[info] hello
"hello"
コンソールにHelloWorld.Workerプロセスの出力したログが確認できます。
このようにelixirは一つのアプリケーション中で幾つものプロセスを立ち上げて、その連携によりアプリケーション全体を構成します。並行だの並列だの言われるが所以は、これらプロセス指向プログラミングを行う為の手立てとも言えますね。