Elixir ホットコードスワッピング入門①
はじめに
Erlang/Elixir には、稼働中のプログラムのコードを更新するホットコードスワッピング(hot code swapping)という機能があります。本稿ではこの機能を解説します。
本稿執筆にあたり使用した Erlang/Elixir のバージョンは次のとおりです:
- Erlang/OTP 27
- Elixir 1.17.3
準備作業: Mix プロジェクトを作る
本稿では本番環境で稼働中の Elixir プログラムをダウンタイムなしで更新できることを具体的なコードに基づいて確かめていきます。
適当なディレクトリで mix new anemone --sup
コマンドを実行し、Anemone という名前のアプリのソースコードの骨格を生成します。スーバーバイザーの機能を使用するため --sup
オプションを付けています。
cd anemeno
コマンドで Anemone アプリのルートディレクトリに移動してください。
Counter モジュールを GenServer として定義する
lib/anemone
ディレクトリに新規ファイル counter.ex
を作成し、次のコードを書き入れてください。
defmodule Counter do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, 0, name: :counter)
end
@impl GenServer
def init(count) do
GenServer.cast(self(), :increment)
{:ok, count}
end
@impl GenServer
def handle_cast(:increment, count) do
IO.puts("count[#{@vsn}]: #{count}")
Process.sleep(1000)
GenServer.cast(self(), :increment)
{:noreply, count + 1}
end
end
GenServer モジュール Counter
を定義しています。GenServer についての解説は省略しますが、BEAM 内のプロセスに届くメッセージを処理するための関数群を持つモジュールだと考えてください。
まず、5 行目で name
オプションに :counter
を指定して GenServer.start_link/3 を呼び出している点に着目してください。これにより、Counter
サーバーをプロセス ID ではなく、アトム :counter
で参照できるようになります。
次に、コールバック handle_cast/2
の中身に着目してください。仮引数 count
の値を画面出力した後に、1000 ミリ秒スリープし、自分自身に対して :increment
メッセージを送り、count
に 1 を加えて終わります。
Counter
サーバーは 1 秒ごとに 1 ずつ増えていく整数の列をターミナルに出力し続けます。
Counter サーバーをスーパーバイザーに登録する
lib/anemone/application.ex
を次のように書き換えてください。
defmodule Anemone.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: Anemone.Worker.start_link(arg)
# {Anemone.Worker, arg}
+ Counter
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Anemone.Supervisor]
Supervisor.start_link(children, opts)
end
end
スーパーバイザーについての解説は省略します。このように書き換えることにより、Anemenne アプリの起動時に Counter サーバーが自動で起動するようになります。
Anemone アプリを起動する
動作確認をしましょう。次のコマンドを実行してください。
mix run --no-halt
オプション --no-halt
を付けないと、Anemene アプリはすぐに終了していしまいます。
ターミナル上には次のように出力されるはずです。
Compiling 1 file (.ex)
count: 0
count: 1
count: 2
count: 3
...
Ctrl+C
を二度入力して、Anemone アプリを終了してください。
リリースを作る
本番環境で Anemone アプリを動かすため、リリースを作ります。Elixir プログラムをコンパイルした時に作られる .beam
ファイルなどの集合体をリリースと呼びます。
MIX_ENV=prod mix release
上記のコマンドを実行した結果、_build/prod/rel
ディレクトリにさまざまなファイルが作られます。これがリリースです。
本番環境で Anemone アプリを起動する
次のコマンドを実行すると Anemone アプリが起動します。
_build/prod/rel/anemone/bin/anemone start
ターミナル上には次のように出力されるはずです。
count: 0
count: 1
count: 2
count: 3
...
このまま Anemone アプリを起動したままにして次に進みます。
Counter モジュールを書き換える
Counter
モジュールのソースコードを次のように書き換えてください。
def handle_cast(:increment, count) do
IO.puts("count[#{@vsn}]: #{count}")
Process.sleep(1000)
GenServer.cast(self(), :increment)
- {:noreply, count + 1}
+ {:noreply, count + 2}
end
end
この結果、Counter
サーバーが出力する整数の列の間隔が 2 になります。
リリースを更新する
次のコマンドを実行して、Anemone アプリのリリースを更新します。
MIX_ENV=prod mix release --overwrite
ls -l _build/prod/rel/anemone/lib/anemone-0.1.0/ebin/
を実行して、Elixir.Counter.beam
のタイムスタンプが最新のものになっていることを確認してください。
この時点では、Anemone アプリがターミナルに出力する整数列の間隔は 1 のままです。
起動中の Anemone アプリに接続する
次のコマンドを実行すると、起動中の Anemone アプリに IEx で接続できます。
_build/prod/rel/anemone/bin/anemone remote
はじめてのホットコードスワッピング
IEx 上で次の 2 つの式を順に評価してください。
:code.purge(Counter)
:code.load_file(Counter)
この結果、Anemone アプリが稼働している仮想マシン BEAM に新しい Counter
モジュールがロードされ、ターミナルに出力される整数列の間隔が 2 になります。どちらの式も false
と評価されますが、問題ありません。
式 :code.purge(Counter)
は現行の Counter
モジュールを削除しますが、稼働中の Counter
サーバーには影響を与えません。式 :code.load_file(Counter)
を評価すると Counter
サーバーの振る舞いが変化します。
なお、:code.purge(Counter)
を評価せずに :code.load_file(Counter)
だけを評価すると、{:error, :not_purged}
というエラーメッセージが出力され、モジュールのロードに失敗します。
モジュール属性 @vsn を導入する
ホットコードスワッピングを行う際に、Counter
サーバーが保持する状態(count
)の値を加工したい場合があります。
Counter
モジュールのソースコードを次のように書き換えてください。
defmodule Counter do
use GenServer
+ @vsn 1
def start_link(_) do
GenServer.start_link(__MODULE__, 0, name: :counter)
end
モジュール属性 @vsn
は、「version」の略です。ホットコードスワッピングの管理のために使われます。
関数 handle_cast/2
の中身をいったん元に戻します。
def handle_cast(:increment, count) do
IO.puts("count[#{@vsn}]: #{count}")
Process.sleep(1000)
GenServer.cast(self(), :increment)
- {:noreply, count + 2}
+ {:noreply, count + 1}
end
end
リリースします。
MIX_ENV=prod mix release --overwrite
Anemone アプリを再起動してください。
_build/prod/rel/anemone/bin/anemone stop
_build/prod/rel/anemone/bin/anemone start
Counter モジュールのバージョン 2 を作ってリリースする
続いて、Counter
モジュールのソースコードを次のように書き換えてください。
defmodule Counter do
use GenServer
- @vsn 1
+ @vsn 2
def start_link(_) do
GenServer.start_link(__MODULE__, 0, name: :counter)
end
def handle_cast(:increment, count) do
IO.puts("count[#{@vsn}]: #{count}")
Process.sleep(1000)
GenServer.cast(self(), :increment)
- {:noreply, count + 1}
+ {:noreply, count + 2}
end
+
+ @impl GenServer
+ def code_change(1, count, _extra), do: {:ok, count + 1000}
end
整数列の間隔が 2 となるように関数 handle_cast/2
のコードを書き換え、コールバック code_change/3
を実装しました。このコールバックの意味については後述します。
リリースしてください。
MIX_ENV=prod mix release --overwrite
GenServer.code_change/3
を利用したホットコードスワッピング
コールバック 稼働中の Anemone アプリに IEx で接続します。
_build/prod/rel/anemone/bin/anemone remote
IEx 上で式 :sys.suspend(:counter)
を評価すると Counter サーバーが一時停止します。ターミナルへの出力が止まったことを確認してください。筆者の手元では次の表示で止まっています。
...
count: 47
count: 48
count: 49
そして、IEx 上で以下の式を順に評価してください。
:code.purge(Counter)
:code.load_file(Counter)
:sys.change_code(:counter, Counter, 1, nil)
関数 :sys.change_code/4 は、BEAM 上の停止中のプロセスに対してメッセージを送ります。メッセージを受けたプロセスは GenServer モジュールのコールバック code_change/3
でメッセージを処理します。
:sys.change_code/4
の第 3 引数に指定された 1
は、現在のプロセスと結びついている GenServer モジュールの @vsn
属性の値を意味します。第 4 引数の nil
は、コールバック code_change/3
の第 3 引数に渡されますが、今回は使用していません。
Counter
モジュールのコールバック code_change/3
のコードを再掲します。
def code_change(1, count, _extra), do: {:ok, count + 1000}
IEx 上で :sys.change_code(:counter, Counter, 1, nil)
が評価されると、このコールバックが呼ばれ、Counter
サーバーが保持する整数の値に 1000 が加えられます。しかし、Counter
サーバーは停止中のため、ターミナルには何も出力されません。
Counter サーバーの稼働を再開する
IEx 上で式 :sys.resume(:counter)
を評価して Counter サーバーの稼働を再開してください。
すると、Counter サーバーが出力する数が 1001 増え、その後 2 ずつ増えていくようになります。
...
count: 47
count: 48
count: 49
count: 1050
count: 1052
count: 1054
まとめ
本稿で解説したように、関数 :code.load_file/1
を利用すると稼働中の Elixir アプリケーションの振る舞いを無停止で更新できます。これが「ホットコードスワッピング」です。
ホットコードスワッピング実施時に、GenServer プロセスが保持する状態を加工したい場合は、GenServer モジュールにコールバック code_change/3
を実装し、GenServer プロセスを一時停止して、:sys.change_code/4
関数を呼び出します。
本稿では、整数を状態として持つ比較的単純な GenServer プロセスのホットコードスワッピングを扱いました。次回は構造体を状態として持つ GenServer プロセスが稼働していて、その構造体の定義が変更された場合のホットコードスワッピングについては調べます。
Discussion