ElixirでHot Code Reloadingするときに気をつけること

h3poteto
oVice
Published in
17 min readDec 4, 2021

副業としてoViceで働いている h3potetoです.oViceではElixirでアプリケーションを開発しており,その中でHot Code Reloadingを使うことがあったので,その際に注意する点をまとめておきます.

この記事は Elixir Advent Calendar 2021 の5日目の記事です.

Elixir (Erlang/OTP) には,プロセスを終了させることなく新しいコードをデプロイするHot Code Reloadingという仕組みが存在する.これは,Hot Code SwapとかHot Code Deployとか呼ばれることもある.

稼働中のアプリケーションの,プロセスを止めずにコードだけを新しいものに差し替え,状態すらも継続させるという,まるで魔法のようなことが可能になるのがHot Code Reloadingだ.ただし,ちょっとだけ注意点があるので,それを紹介していく.

Erlang VMは同時に2バージョンのモジュールを動かせる

Hot Code Reloadingで何が起こっているのか?

その前に,まずHot Code Reloadingで何が行われているのかというのをちょっとだけ紹介しておく.基本的には,Erlang VM (BEAM) が relup ファイルに従って

{"1.0.1",
[{"1.0.0",[],
[{load_object_code,
{my_app,"1.0.1",
['Elixir.MyApp.Hoge']}},
point_of_no_return,
{suspend,['Elixir.MyApp.Hoge']},
{load,
{'Elixir.MyApp.Hoge',brutal_purge,
brutal_purge}},
{code_change,up,[{'Elixir.MyApp.Hoge',[]}]},
{resume,['Elixir.MyApp.Hoge']}]}],

プロセスの一時停止,新しいコードをロード,状態の更新,プロセス再スタートと順番に行っているに過ぎない.ここで出てくる code_change については後述.Erlangはここに出てきている操作を順番にやっているだけなので,ここに記述されないモジュールについては何も発生しない.そのため,もしHot Code Reloadingで思う通りに新しいモジュールがデプロイされない場合は,ここをチェックしてみると良い.

vsn

まず最初に紹介するのは vsn というキーワード.これをmoduleに指定することで,各モジュールのバージョンを明示することができる.

ただし, vsn はオプショナルである.Hot Code Reloadingをやるからと言って,必ず指定しなければいけないものではない.後述する Transform stateをやるときに便利なので,そのときに使うことをおすすめする.

例えば,

defmodule MyModule do
@vsn "2"
def init() do
end
#...
end

とすることで MyModule"2" というバージョンが指定される.これを指定しない場合,ファイルのmd5ハッシュから自動的にバージョンが算出される.ただし,先程述べたTransform stateをやる場合に code_change というメソッドを書くのだが,そこでこのバージョンを使うので,これをやる場合は指定しておくと便利である.

結局どんな場合に使えば良い?

  • gen_serverもしくは gen_statemを使う場合
  • コード変更なし(md5ハッシュが更新されない)だがモジュールをリロードしたい場合.例えばライブラリのみを更新しているが,モジュールでそのライブラリを使っている場合, vsn が更新されないとモジュールがリロードされないため,更新されたライブラリが利用されない場合がある.

Transform state

前述した code_change を使う話がこれ.通常,ErlangはHot Code Reloadingの際にプロセスの一時停止・再開は行うが,Stateを変更したりはしてくれない.そのため,例えばモジュールのstructを変更するようなものは,Hot Code Reloadingできないということになる.

しかし,Erlangのspecial processと呼ばれるもの(gen_serverとかgen_statem)は,Hot Code Reloading時にStateを変更する方法を提供している.これが code_change メソッドである.このメソッドは,Hot Code Reloadingの際に呼ばれ,このメソッド内部でStateを古いバージョンから新しいバージョンに変換することができる.

基本

defmodule MyApp.Hoge do
@vsn "1"
use GenServer
defstruct [:hoge]
def init(state) do
{:ok, state}
end
def handle_call(_, _from, state) do
## Some codes
end
end

MyApp.Hoge のstructを以下のように変更するとする.

defmodule MyApp.Hoge do
@vsn "2"
use GenServer
defstruct [:hoge, :fuga]
def init(state) do
{:ok, state}
end
def handle_call(_, _from, state) do
## Some codes
end
def code_change("1" = vsn, state, _extra) do
{:ok, %{ state | fuga: "fuga" }}
end
end

vsnを更新し, code_change メソッドを定義し,内部で新しいStateを返す.こうすることで,Hot Code Reloadingされたあとの MyApp.Hoge はStateに fuga というメンバを持ち,値も入った状態でプロセスが再開することになる.

code_changeが実行される条件

便利な code_change だが,すべてのプロセスで,どんな状況でも呼ばれるわけではない.

  1. プロセスはアプリケーションのSupervisorによって起動されているか,もしくはそのSupervison treeによって起動されている必要がある³
  2. プロセスはErlangのspecial processである必要がある

1は非常に重要な点で,先程の例であれば MyApp.Hogeapplication.ex で以下のように起動される必要がある.

defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Hoge
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end

もしくはSupervision treeの下で,

defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.MySupervisor
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule MyApp.Supervisor do
use Supervisor
def start_link(init_args) do
Supervisor.start_link(__MODULE__, init_args, name: __MODULE__)
end
@impl Supervisor
def init(_) do
children = [
MyApp.Hoge
]
Supervisor.init(children, strategy: :one_for_one)
end
end

のように起動される必要がある.

code_changeが呼ばれない例

special processではない

defmodule MyApp.Websocket do
@vsn "2"
@behaviour :cowboy_websocket
defstruct [:username]
# Some methods
# Will not be called
def code_change("1" = vsn, %{username: username} = state, _extra) do
{:ok, %{state | username: username <> "-user"}}
end
end

gen_serverがApplicationのSupervisorで起動されていない

defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.MyServer
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule MyApp.MyServer do
@vsn "1"
use GenServer
defstruct [:hoge]
def init(_state) do
{:ok, pid} = GenServer.start_link(MyApp.Hoge, %MyApp.Hoge{})
{:ok, %{hoge: pid}}
end
# Some methods
end
defmodule MyApp.Hoge do
@vsn "2"
use GenServer
defstruct [:hoge, :fuga]
# Some methods
# Will not be called
def code_change("1" = vsn, state, _extra) do
{:ok, %{ state | fuga: "fuga" }}
end
end

この例では, MyApp.MyServer はApplicationのSupervisorによって起動されているが, MyApp.Hoge はそうではないし,Supervision treeにも所属していない.そのため code_change メソッドは呼び出されない.

モジュールの名前変更

Hot Code ReloadingにおいてRenameのイベントは検知されない.そのためモジュールの名前を変えるのは十分に気をつける必要がある.

何が起こるのか

例えば, Hoge モジュールを Fuga に変更する場合,

  • 古いプロセスは Hoge を利用し続けているが,新しいプロセスには Hoge が存在しない.そのため,古いプロセスで Hoge を呼び出した時点でクラッシュする.もし,このプロセスがSupervisorで管理されている場合,再起動される.
  • HogeFuga がgen_serverの場合はもう少し複雑なので以下を参照

アプリケーションのSupervisorで起動されたgen_serverの場合

defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Hoge
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end

このような場合,Supervisorは Myapp.Hoge を再起動しない.そのため,もし

children = [
MyApp.Fuga
]

のような書き換えをしたとしても, Hot Code Reloadingによって Fuga は起動しない.つまり,仮に新しいバージョンで Fuga が起動されることを期待したコードを書けば,クラッシュする.

defmodule MyApp.SomeModule do
def init() do
MyApp.Fuga.fugafuga() #=> (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
end
end

そして,放置したところで Fuga は起動しないので,クラッシュし続けることになる.

つまり,このケースにおいて Hot Code Reloadingを使うことはできない.Erlang VMを再起動するしかない.

gen_serverが他のプロセスによって起動された場合

defmodule MyApp.SomeModule do
use GenServer
def init(state) do
{:ok, pid} = MyApp.Hoge.start_link()
{:ok, %{hoge: pid}}
end
def handle_info(_, state) do
MyApp.Hoge.hogehoge()
end
end

このようにして起動した HogeFuga に変更するとする.

defmodule MyApp.SomeModule do
use GenServer
def init(state) do
{:ok, pid} = MyApp.Fuga.start_link()
{:ok, %{hoge: pid}}
end
def handle_info(_, state) do
MyApp.Fuga.hogehoge() #=> (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
end
end

Hot Code Reloading後,MyApp.Fuga.hogehoge() の呼び出しはクラッシュする.なぜなら Hot Code Reloadingにおいて, MyApp.SomeModuleinitは誰からも呼ばれず, MyApp.Fuga は起動していないため.しかし, MyApp.SomeModule をどこかのSupervisorで

children = [
MyApp.SomeModule
]

として起動していた場合,クラッシュしたプロセスがSupervisorにより再起動される.その際には init を通過するため, Fuga が起動するので,これ以降 MyApp.Fuga.hogehoge() の呼び出しは成功するようになる.

つまり,このケースにおいては,1回のクラッシュを許容するのであれば Hot Code Reloading可能である.もちろん, MyApp.SomeModule をSupervisorで管理していない場合,これはうまくいかない.

メソッドの呼び出し方

Local call vs Full qualified call

Local call

defmodule MyModule do
def hoge() do
end
def fuga() do
hoge() # local call
end
end

Full qualified call

defmodule MyModule do
def hoge() do
end
def fuga() do
MyModule.hoge() # full qualified call
end
end

Full qualified callの場合,常に最新バージョンのモジュールメソッドを呼び出す.対してLocal callの場合,同じバージョンのモジュールメソッドを呼び出す.詳しくは以下のスライドを参照.

https://www.slideshare.net/Elixir-Meetup/hot-code-replacement-alexei-sholik/19

どちらを使うかはケースバイケースだが,自分がどちらを使っているかは意識しておいたほうが良い.

Configの変更

Elixirでは通常, Mix.ConfigConfigconfig/${mix_env}.exs 内で利用していると思う.ここの設定値を増やしたり,変更した場合,Hot Code Reloadingでは設定の再読込は行われない.そのため,新しい設定値を利用することはできない.もちろん,これは rel/config.exsrel/vm.args も同じである.

この場合も,Hot Code Reloadingを使うことはできないということになる.

心配しなくていいこと

  • メソッドの引数や戻り値を変更する場合
  • ファイル名を変更する場合
  • ライブラリの更新

これらにおいては,ほぼ問題が発生しないので特に心配する必要はない.

何が嬉しいの?

ここまで注意点を書いてきたが,ここまでしてHot Code Reloadingする旨味はなんだろう?

  1. プロセスが停止・再起動することがないので,サーバはリクエストを受け続けられる
  2. WebSocketのプロセスも停止・再起動することがないので,デプロイ時にコネクションが切れない
  3. Stateが保持し続けられる

これらはoViceにとってはかなり嬉しい.oViceはもちろんWebSocketを使っており,WebRTCも使っている.これらがデプロイ時に途切れないというのは,Webアプリケーション開発をしていく上で重要だ(これがないと,例えばユーザが少ない深夜帯にしかデプロイできない,というような状況になる).

もちろん,フロントエンドのJavaScriptでWebSocketやWebRTCの再接続処理は行われる.しかし,デプロイのたびに切断され再接続するのと,デプロイによって切断されないとでは,大きな違いだ.ビデオ通話しているときに切断されるのは嫌であろう…….

まとめ

Hot Code Reloadingするアプリケーションを開発する上での注意点をいくつか紹介しました.これらを検証するために,テスト用のアプリケーションをひとつ作りました.

Hot Code Reloadingが提供する利点はかなり大きいので(特にWebSocketを使うアプリケーションでは),必要な場面では恐れずに使っていきたいですね.

--

--

h3poteto
oVice
Writer for

I am a software engineer working in Japan. Sometimes I use Elixir, Golang, Ruby, TypeScript, Python, Swift and others.