この章は、実行時のアップグレード、ダウングレードを行う、よくある様々なケースでの .appup ファイルのサンプルを紹介します。
新しい関数を追加した、バグの修正をしたなど、機能性モジュールの変更を行った場合は、シンプルコード交換で十分です。
サンプル:
{"2",
[{"1", [{load_module, m}]}],
[{"1", [{load_module, m}]}]
}.
OTP設計原則に従ったシステム実装ををすると、システムプロセスと特殊なプロセスを除く、全てのプロセスは supervisor 、 gen_server 、 gen_fsm 、 gen_event などのビヘイビア側に置かれることになります。これらは STDLIB アプリケーションに属し、通常の場合、アップグレードやダウングレードはエミュレータの再起動が必要となります。
OTPは、特殊なプロセスを除いて、レジデンスモジュールの変更をサポートしています。
コールバックモジュールは機能性モジュールであるため、コードの拡張にあたっては、シンプルコード交換で十分です。
サンプル: リリース・ハンドリング の例で説明した ch3 モジュールに関数を一つ追加した場合、 ch_app.appup は次のようになります。
{"2",
[{"1", [{load_module, ch3}]}],
[{"1", [{load_module, ch3}]}]
}.
OTPはビヘイビアプロセスの内部ステートの変更についてもサポートしています。詳しくは 内部ステートの変更 を参照してください。
このケースでは、シンプルコード交換では十分ではありません。新しいバージョンのコールバックモジュールに切り替える前に、プロセスは明示的にコールバック関数の code_change を使用して、内部ステートを変更する必要があります。同期コード交換が利用されます。
サンプル: gen_serverビヘイビア の章の ch3 gen_serverについて考えていきます。内部ステートの Chs という項は、利用可能なチャンネルを表しています。これに、今まで割り当ててきた数をトラッキングし続ける、カウンター N を追加しようとしたとします。これはつまり、内部ステートのフォーマットを {Chs,N} にしなければならない、ということを意味しています。
.appup ファイルは次のようになります。
{"2",
[{"1", [{update, ch3, {advanced, []}}]}],
[{"1", [{update, ch3, {advanced, []}}]}]
}.
update 命令の三番目の項目は、 {advanced, Extra} というタプルになっています。これは、適応されるプロセスは、新しいバージョンのモジュールをロードする前に内部ステートを変更しなければならない、ということを表しています。この変更作業は、プロセスが code_change コールバック関数( gen_server(3) 参照)を呼び出すことで行われます。このサンプルのケースでは、 Extra の項は [] となっています。この項は、そのままの形で関数に渡されます。
-module(ch3).
...
-export([code_change/3]).
...
code_change({down, _Vsn}, {Chs, N}, _Extra) ->
{ok, Chs};
code_change(_Vsn, Chs, _Extra) ->
{ok, {Chs, 0}}.
最初の引数の {down,Vsn} はダウングレード時の、 Vsn はアップグレード時、という意味になります。 Vsn という項はモジュールの元のバージョンから取得されます。これはアップグレード元、あるいはダウングレード先のバージョンになります。
バージョンは、もしあればモジュール属性の vsn で定義されます。この ch3 の場合には見あたらないため、とても巨大な数値ですが、BEAMファイルのチェックサムとなります。このプログラムでは特に必要ではないため無視しています。
(ここでは表示されていませんが、 ch3 の他のコールバック変数も同じように変更されていたり、新しいインタフェース関数が追加されている必要があります。)
リリース・ハンドリング のサンプルで示したように、新しいインタフェース関数を追加してモジュールを拡張したとします。ここでは、 available/0 という関数を ch3 モジュールに追加したものとして説明を行います。
もし、 m1 と呼ばれるモジュールからこの関数への呼び出しを追加した場合、新しいバージョンの ch3 がロードされる前に、新しいバージョンの m1 がロードされて、 ch3:available/0 を呼び出したとすると、リリースのアップグレード時にランタイムエラーが発生してしまいます。
そのため、このようなアップグレード場合や、逆にダウングレードを行う場合は、 ch3 は m1 よりも先にロードされなければなりません。私たちはこのようなケースを「 m1 は ch3 に 依存している 」 と呼んでいます。リリースハンドリング命令の中では、 DepMods という要素を使ってこれを表現します。
{load_module, Module, DepMods}
{update, Module, {advanced, Extra}, DepMods}
DepMods は、対象のモジュールが依存しているモジュール群のリストになります。
サンプル: myapp アプリケーション内の m1 モジュールは1から2へのアップグレード、および、2から1へのダウングレード時には、 ch3 に依存します。
myapp.appup:
{"2",
[{"1", [{load_module, m1, [ch3]}]}],
[{"1", [{load_module, m1, [ch3]}]}]
}.
ch_app.appup:
{"2",
[{"1", [{load_module, ch3}]}],
[{"1", [{load_module, ch3}]}]
}.
m1 と ch3 が同じアプリケーションに属しているのであれば、 .appup ファイルは次のような見た目になります。
{"2",
[{"1",
[{load_module, ch3},
{load_module, m1, [ch3]}]}],
[{"1",
[{load_module, ch3},
{load_module, m1, [ch3]}]}]
}.
ダウングレードの際にも、 ch3 は m1 に依存していることに注意してください。 systools はアップグレードとダウングレードの違いについても知っていて、アップグレード時には m1 よりも先に ch3 を読み込み、ダウングレード時には ch3 よりも先に m1 を読み込むという、正しい relup を生成します。
この場合、シンプルコード交換では不十分です。特別なプロセスのための、新しいバージョンのレジデンスモジュールがロードされると、プロセスのループ関数呼び出しは、新しいコード上のループ関数呼び出しに切り替える必要があります。これには、同期コード交換が必要となります。
Note
リリースハンドラがプロセスを見つけるためには、ユーザ定義のレジデンスモジュールの名前が、特別なプロセスの 子プロセスの仕様の設定 の中の Modules リストに設定されている必要があります。
サンプル: sysとproc_lib の章で説明した ch4 モジュールについて考えていきます。これはスーパバイザから起動され、 :ref:child_spec は次のようになっています。
{ch4, {ch4, start_link, []},
permanent, brutal_kill, worker, [ch4]}
もし ch4 が sp_app アプリケーションの一部であり、このアプリケーションを1から2にアップグレードする際に、新しいバージョンのモジュールをロードする必要があるとします。この sa_app.appup は次のようになります。
{"2",
[{"1", [{update, ch4, {advanced, []}}]}],
[{"1", [{update, ch4, {advanced, []}}]}]
}.
この update 命令には {advanced, Extra} タプルを含めなければなりません。この命令は特別なプロセスに対して、 system_code_change/4 というコールバック関数を呼ぶようにさせます。ユーザはこの関数を実装しなければなりません。このサンプルの場合は、 Extra の項は [] ですが、これは system_code_change/4 にそのまま渡されます。
-module(ch4).
...
-export([system_code_change/4]).
...
system_code_change(Chs, _Module, _OldVsn, _Extra) ->
{ok, Chs}.
最初の引数は、システムメッセージを受信したときに、特別なプロセスから呼ばれた、 sys:handle_system_msg(Request, From, Parent, Module, Deb, State) 関数に渡された、内部ステートの State です。 ch4 の中では、利用可能なチャンネルのリストの Chs が内部ステートとして設定されます。
2つ目の引数はモジュールの名前(ch4)になります。
3つ目の引数は gen_server:code_change/3 の所で説明した通り、 Vsn か {down,Vsn} となります。
この場合、最初の引数以外のすべての引数を無視して、内部ステートをそのまま返しています。単なるコード拡張であれば、これで十分です。内部ステートを変更したい場合には(内部ステートの変更 のサンプルと同じように)この関数の中で行って、 {ok,Chs2} を返せば行えます。
スーパバイザ・ビヘイビア は、既存の 子プロセスの仕様の設定 の変更と同じように、 再起動戦略 や 再起動頻度の最大値 などの内部ステートの変更をサポートしています。
子プロセスの追加と削除も行えますが、これは自動では行えません。 .appup ファイルに命令を追加する必要があります。
スーパバイザの内部ステートを変更しなければならない時は、同期コード交換が必要となります。しかし、特別な update 命令を使用する必要があります。
この場合は、アップグレード、ダウングレードの両方の場合で、コールバックモジュールの新しいバージョンが最初にロードされる必要があります。 init/1 の新しい返り値がチェックでき、それにしたがって内部ステートが変更されます。
スーパバイザに対しては、次のような update 命令が使用されます。
{update, Module, supervisor}
サンプル: スーパバイザ・ビヘイビア の章の ch_sup の再起動戦略を one_for_one から one_for_all に変更したいとします。 ch_sup.erl の init/1 のコールバック関数を次のように変更します。
-module(ch_sup).
...
init(_Args) ->
{ok, {{one_for_all, 1, 60}, ...}}.
ch_app.appup は次のようになります。
{"2",
[{"1", [{update, ch_sup, supervisor}]}],
[{"1", [{update, ch_sup, supervisor}]}]
}.
子プロセスの仕様の変更を行うのも、 .appup ファイルに対して、上記で説明したプロパティの変更と同じように変更することで行うことができます。
{"2",
[{"1", [{update, ch_sup, supervisor}]}],
[{"1", [{update, ch_sup, supervisor}]}]
}.
この変更は既存の子プロセスには影響を与えません。例えば、起動関数の変更は、子プロセスの再起動時にのみ適用されます。
子プロセスのidは変更できないことに注意してください。
子プロセスの仕様の Modules フィールドの変更は、リリースハンドリングプロセスそのものに影響を与えます。このフィールドは、同期コード交換を行うときに、どのプロセスが影響を受けるかを特定するのに使用されます。
上記で説明した通り、子プロセスの仕様の変更は、既存の子プロセスには影響を与えません。新しい子プロセスの仕様は自動的に追加されますが、削除はされません。そのため、子プロセスは自動的には起動したり、停止したりすることはありません。これを行うには、明示的に命令を使用しなければなりません。
サンプル: ch_sup を1から2にアップグレードするときに、新しい子プロセス m1 を ch_sup に追加したいと想定して話を進めます。これは、2から1にダウングレードするときには、この m1 を削除しなければならない、ということを意味します。
{"2",
[{"1",
[{update, ch_sup, supervisor},
{apply, {supervisor, restart_child, [ch_sup, m1]}}
]}],
[{"1",
[{apply, {supervisor, terminate_child, [ch_sup, m1]}},
{apply, {supervisor, delete_child, [ch_sup, m1]}},
{update, ch_sup, supervisor}
]}]
}.
この命令の順番が大切です。
また、スクリプトを動作させるためには、スーパバイザを ch_sup として登録しなければなりません。もしスーパバイザが登録されていないと、スクリプトからは直接アクセスすることができません。スーパバイザのpidを見つけて、 supervisor:restart_child などを呼び出すような補助関数を書く代わりに、 apply 命令を使って、スクリプトからこの関数を呼び出すようにします。
モジュール m1 が ch_app のバージョン2で導入されるのであれば、アップグレード時に追加されたり、ダウングレード時に削除されるようにしなければなりません。
{"2",
[{"1",
[{add_module, m1},
{update, ch_sup, supervisor},
{apply, {supervisor, restart_child, [ch_sup, m1]}}
]}],
[{"1",
[{apply, {supervisor, terminate_child, [ch_sup, m1]}},
{apply, {supervisor, delete_child, [ch_sup, m1]}},
{update, ch_sup, supervisor},
{delete_module, m1}
]}]
}.
ここでも命令の順番が台説です。アップグレード時には m1 がロードされて、新しい子プロセスが起動される前に、スーパバイザの子プロセスの仕様が変更されます。ダウングレード時には子プロセスの仕様が変更される前に子プロセスが停止され、モジュールが削除されなければなりません。
サンプル: 新しい機能性モジュール m が ch_app に追加されました。
{"2",
[{"1", [{add_module, m}]}],
[{"1", [{delete_module, m}]}]
OTP設計原則に従ったシステム構成の中では、あらゆるプロセスが、スーパバイザに所属する子プロセスになります。 子プロセスの追加と削除 を参照してください。
アプリケーションの追加と削除時は .appup ファイルは不要です。 relup が生成されるときに、 .rel ファイルの比較が行われ、 add_application と remove_application 命令が自動的に追加されます。
アプリケーションの再起動は、スーパバイザの階層構造の構成変更など、変更が複雑すぎて、プロセスの再起動なしに更新ができない場合に有用です。
サンプル: 新しい子プロセスの m1 を ch_sup に追加するときは、スーパバイザのアップデートの代わりに、アプリケーション全体を再起動することもできます。
{"2",
[{"1", [{restart_application, ch_app}]}],
[{"1", [{restart_application, ch_app}]}]
}.
リリースをインストールするときは、アプリケーションの仕様は relup スクリプトの評価前に自動的に更新されます。そのため、 .appup ファイルの中には何も命令を含める必要はありません。
{"2",
[{"1", []}],
[{"1", []}]
}.
.app ファイルの env キーを更新してアプリケーションの構成を変更する場合には、上記の アプリケーション仕様の変更 を参照してください。
これ以外には、アプリケーション構成パラメータを sys.config の中で追加したり更新することもできます。
アプリケーションの追加、削除、再起動を行うリリース・ハンドリング命令はプライマリ・アプリケーションにだけに適用できます。インクルードされたアプリケーションに対応した命令はありません。しかし、インクルードされたアプリケーションは最上位のスーパバイザを伴う、監視ツリーであるのでり、インクルードされたアプリケーションが、スーパバイザの子プロセスとして起動されているのであれば、 relup ファイルを手動で作成することができます。
サンプル: 監視ツリー内に、 prim_sup というスーパバイザを持つ、 prim_app というアプリケーションを含むリリースを行おうとしていたとします。
新しいバージョンのリリースには、 prim_app のの中に、 ch_app というサンプルのアプリケーションをふくめないといけません。この場合、 ch_sup の最上位のスーパバイザは、 prim_sup の子プロセスとして起動されなければなりません。
prim_sup のコードの編集
init(...) ->
{ok, {...supervisor flags...,
[...,
{ch_sup, {ch_sup,start_link,[]},
permanent,infinity,supervisor,[ch_sup]},
...]}}.
prim_app の .app ファイルの編集
{application, prim_app,
[...,
{vsn, "2"},
...,
{included_applications, [ch_app]},
...
]}.
ch_app を含む、新しい .rel ファイルの作成
{release,
...,
[...,
{prim_app, "2"},
{ch_app, "1"}]}.
しかし、これを行って relup ファイルを生成するには、削除や追加などの prim_app の再起動のための命令を含めるだけではなく、 ch_app の起動(ダウングレード時は停止も)の命令も含めなければなりません。これは、 ch_app が新しい .rel ファイルに含まれているからではなく、古いファイルに含まれていないためです。
その代わりに、正しい relup ファイルを手動で作成することができます。方法としては、スクラッチから作成する方法と、生成されたものを編集する方法があります。 ch_app の起動/停止の命令は、アプリケーションのロード/アンロードの命令と置き換えます。
{"B",
[{"A",
[],
[{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
{load_object_code,{prim_app,"2",[prim_app,prim_sup]}},
point_of_no_return,
{apply,{application,stop,[prim_app]}},
{remove,{prim_app,brutal_purge,brutal_purge}},
{remove,{prim_sup,brutal_purge,brutal_purge}},
{purge,[prim_app,prim_sup]},
{load,{prim_app,brutal_purge,brutal_purge}},
{load,{prim_sup,brutal_purge,brutal_purge}},
{load,{ch_sup,brutal_purge,brutal_purge}},
{load,{ch3,brutal_purge,brutal_purge}},
{apply,{application,load,[ch_app]}},
{apply,{application,start,[prim_app,permanent]}}]}],
[{"A",
[],
[{load_object_code,{prim_app,"1",[prim_app,prim_sup]}},
point_of_no_return,
{apply,{application,stop,[prim_app]}},
{apply,{application,unload,[ch_app]}},
{remove,{ch_sup,brutal_purge,brutal_purge}},
{remove,{ch3,brutal_purge,brutal_purge}},
{purge,[ch_sup,ch3]},
{remove,{prim_app,brutal_purge,brutal_purge}},
{remove,{prim_sup,brutal_purge,brutal_purge}},
{purge,[prim_app,prim_sup]},
{load,{prim_app,brutal_purge,brutal_purge}},
{load,{prim_sup,brutal_purge,brutal_purge}},
{apply,{application,start,[prim_app,permanent]}}]}]
}.
繰り返しになりますが、 relup ファイルは手動で作成する必要があります。スクラッチで書く方法と、生成されたファイルを編集する方法があります。 prim_sup が更新され、そのアプリケーション仕様がアンロードされる前に、 ch_app に関するすべてのコードを最初にロードし、アプリケーション仕様もロードします。
{"B",
[{"A",
[],
[{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
{load_object_code,{prim_app,"2",[prim_sup]}},
point_of_no_return,
{load,{ch_sup,brutal_purge,brutal_purge}},
{load,{ch3,brutal_purge,brutal_purge}},
{apply,{application,load,[ch_app]}},
{suspend,[prim_sup]},
{load,{prim_sup,brutal_purge,brutal_purge}},
{code_change,up,[{prim_sup,[]}]},
{resume,[prim_sup]},
{apply,{supervisor,restart_child,[prim_sup,ch_sup]}}]}],
[{"A",
[],
[{load_object_code,{prim_app,"1",[prim_sup]}},
point_of_no_return,
{apply,{supervisor,terminate_child,[prim_sup,ch_sup]}},
{apply,{supervisor,delete_child,[prim_sup,ch_sup]}},
{suspend,[prim_sup]},
{load,{prim_sup,brutal_purge,brutal_purge}},
{code_change,down,[{prim_sup,[]}]},
{resume,[prim_sup]},
{remove,{ch_sup,brutal_purge,brutal_purge}},
{remove,{ch3,brutal_purge,brutal_purge}},
{purge,[ch_sup,ch3]},
{apply,{application,unload,[ch_app]}}]}]
}.
ポートプログラムなどErlang以外のプログラミング言語で書かれたプログラムの変更は、アプリケーションに非常に依存します。OTPではこれに対する特別なサポートを提供していません。
ポートプログラムへの変更の例: Erlangプロセスが、 gen_serverの portc というポートの制御をしているという想定の下に話を進めます。このポートはコールバック関数の init/1 の中でオープンされています。
init(...) ->
...,
PortPrg = filename:join(code:priv_dir(App), "portc"),
Port = open_port({spawn,PortPrg}, [...]),
...,
{ok, #state{port=Port, ...}}.
もしポートプログラムの更新が必要な場合、gen_serverの code_change 関数を拡張し、古いポートを閉じて、新しいポートを開くことができます。必要であれば、gen_server古いポートプログラムに保存すべきデータを最初に要求し、新しいポートにこのデータを渡すこともできます。
code_change(_OldVsn, State, port) ->
State#state.port ! close,
receive
{Port,close} ->
true
end,
PortPrg = filename:join(code:priv_dir(App), "portc"),
Port = open_port({spawn,PortPrg}, [...]),
{ok, #state{port=Port, ...}}.
.app ファイル内のアプリケーションのバージョン番号更新を行い、 .appup ファイルを書きます。
["2",
[{"1", [{update, portc, {advanced,port}}]}],
[{"1", [{update, portc, {advanced,port}}]}]
].
Cのプログラムが置かれている priv ディレクトリの情報を、新しいリリースパッケージに含めるようにします。
1> systools:make_tar("my_release", [{dirs,[priv]}]).
...
もしエミュレータの再起動が行える、もしくは行わなければならない場合には、次のような .relup ファイルを作るだけで簡単に行えます。
{"B",
[{"A",
[],
[restart_new_emulator]}],
[{"A",
[],
[restart_new_emulator]}]
}.
この方法を使うと、特別な .appup ファイルを使わないでも、リリースハンドラフレームワークは自動的にリリースパッケージをパックしたり、展開したり、パスを自動的に更新したりできます。
もし、データベースの内容の変更など、永続されたデータの変更が必要な場合は、新しいリリースバージョンをインストールする前に、同じようにしてこのための命令を .relup ファイルに追加することができます。
Copyright (c) 1991-2009 Ericsson AB