25. プロセスの冒険でレベルを上げる

25.1. appupとrelupのしゃっくり

コードのホットローディングは、Erlangの中でも最も簡単な事の1つです。 再コンパイルして、完全修飾関数呼出し(fully-qualified function call)をして、それで完了です。 しかしながら、これを正しく安全に行うのはもっとずっと難しいのです。

コードをリロードすることを難しくしている、非常に単純な課題が1つあります。 素晴らしいErlangプログラマの脳みそを使って、gen_serverのプロセスを考えてみましょう。 このプロセスは、1種類の引数だけ受け取る handle_cast/2 関数を持っています。 この関数を異なる種類の引数を取るように更新して、コンパイルして、プロダクション環境にプッシュします。 すべて順調に動いていますが、停止させたくないアプリケーションがあるので、プロダクション環境のVMにロードして実行させます。

../_images/evolve.png

そして、大量のエラーレポートが出され始めました。 異なる handle_cast 関数が互換性がないということが分かりました。 したがって、2回目に呼ばれたときに、一致する節がないのです。 顧客は怒り狂い、あなたの上司も怒り狂います。 運用者も、おかしくなった場所を探してコードをロールバックし、火消しなどをしなければならないので、起こっています。 運が良ければ、あなたが運用者かもしれません。 遅くまで残って、管理人の当直を台なしにすることになります。(彼は音楽に合わせて鼻歌を歌うのが好きで、ときに踊ったりもするんですが、あなたがいることで恥ずかしくてそれができないのです。) 遅くに帰宅すると、家族や友達やWoWの奇襲部隊や子供が腹を立てて、怒鳴り、叫び、ドアをバタンと閉めて、あなたは一人きりとなります。 あなたが、なにもおかしくなることはない、ダウンタイムはないと約束したのです。 あなたはどちらにせよErlangを使っているんですよね。 しかし、そうななりませんでした。 あなたは一人きりにされ、キッチンの隅で膝を抱えて丸くなり、凍った冷凍焼きおにぎりを食べているのです。

もちろん、いつもこんなに悪い状況になるわけではないですが、要点は得ています。 公開しているモジュールのインターフェースを変更する場合は、プロダクションシステムでライブコードアップグレードを行うことは非常に危険です。つまり、内部的なデータ構造や、関数名、レコードの修正(思い出して下さい、これらはタプルです!)といった操作が危ないのです。 これらはすべてクラッシュを引き起こす原因となります。

コードのリロードを最初に試した際に、完全修飾呼出しをするさいに隠れたメッセージを持ったプロセスがありました。 コードを思い出してみると、プロセスはこのようなものでした:

loop(N) ->
    receive
        some_standard_message -> N+1;
        other_message -> N-1;
        {get_count, Pid} ->
            Pid ! N,
            loop(N);
        update -> ?MODULE:loop(N);
    end.

しかし、この方法では loop/1 の引数を変更しようとすると問題を解決できません。これを次のように拡張する必要があります:

loop(N) ->
    receive
        some_standard_message -> N+1;
        other_message -> N-1;
        {get_count, Pid} ->
            Pid ! N,
            loop(N);
        update -> ?MODULE:code_change(N);
    end.

それから code_change/1 は新しいバージョンの loop を呼び出す部分の管理をします。 しかしこの方法では汎用的なループでは対応できません。 次の例を見てください:

loop(Mod, State) ->
    receive
        {call, From, Msg} ->
        {reply, Reply, NewState} = Mod:handle_call(Msg, State),
            From ! Reply,
            loop(Mod, NewState);
        update ->
            {ok, NewState} = Mod:code_change(State),
            loop(Mod, NewState)
    end.

問題点がわかりましたか。 Mod を更新して、新しいバージョンをロードしたい場合、この実装ではそれを安全に行う方法がありません。 Mod:handle_call(Msg, State) はすでに完全修飾で、 {call, From, Msg} という形式のメッセージをコードのリロードと更新メッセージの処理の間に受け取る事は十分ありえます。 この場合、モジュールの更新は制御不能な状態となり、クラッシュします。

これを正しく動かすコツは、OTPの内部に埋まっています。 私たちは時の流れを止めなければなりません! そのためには、もっと秘密のメッセージが必要となります。たとえば、プロセスを保留するメッセージ、コードを変更するメッセージ、前に処理していた作業を再開するメッセージなどです。 OTPビヘイビアの深層部では、あらゆる種類の管理をしている特別なプロトコルがあります。 これは、 sys モジュールと呼ばれるものと、 release_handler と呼ばれるものを通じて行われいて、後者は SASL(System Architecture Support Libraries)アプリケーションの一部になっています。 これらがすべての面倒をみてくれるのです。

ここで使っている技は、OTPプロセスを sys:suspend(PidOrName) を呼んでサスペンドしている部分です。(監視ツリーを使ったり各スーパバイザが持っている子プロセスを見ることですべてのプロセスを見つけることができます) それから sys:change_code(PidOrName, Mod, OldVsn, Extra) を使って、プロセス自身を強制的に更新し、最終的に sys:resume(PidOrName) を呼び出して再稼働させる事ができます。

これらの関数をいつもアドホックなスクリプトを書いて手動で呼び出すのはあまり実用的ではありません。 代わりに、relupが行われたかを見ていきましょう。

25.2. Erlang地獄の最下層

../_images/9-circles-of-erl.png

稼働しているリリースを取ってきて、第2版を作り、それそ稼働中に更新するのは非常に危険です。 appup (個々のアプリケーションの更新方法についての指示を含んだファイル)を単純に集めたものや relup (リリース全体の更新方法についての指示を含んだファイル)のようなものが、すぐにAPI経由で苦闘し、ドキュメント化されずに先の苦労を肩代わりするものとなりました。

これからOTPの中で最も複雑で、理解しがたく、正しく動作させることが難しく、時間もかかる部分の内の1つについて話そうとしています。 事実、すべての手順(以下relupと呼びます)を避け、単純にVMを再起動し新しいアプリケーションを起動することで更新ができるなら、そちらを選ぶことをおすすめします。 relupは「殺るか殺られるか」のツールの1つです。 選択肢がほとんど無いときに使うものです。

リリースの更新を扱う上で、たくさんの階層があります:

  • OTPアプリケーションを書く
  • それらをリリースにする
  • 1つ以上のOTPアプリケーションのバージョンを更新する
  • そのアプリケーションの古いバージョンから新しいバージョンへの遷移を行うために、何を変更すべきかを説明した appup ファイルを作成する
  • 新しいアプリケーションで新しいリリースを作る
  • appup ファイルをこれらのリリースから生成する
  • 新しいアプリケーションを稼働しているErlangシェルにインストールする

下に行くにしたがってどんどん複雑になります。 ここでは最初の3つの手順だけ見て来ました。 前に触れた3つの手順よりも長期間におよぶ更新により適合したアプリケーションを扱えるように(とはいえ再起動無しに走る正規表現に誰が興味があるでしょうか)、最高のTVゲームを紹介します。

25.3. Progress Quest

Progress Questは革命的なロールプレイングゲームです。 私は事実これをRPGのOTPと呼んでいます。 もしあなたがいままでRPGをしたことがあれば、多くの手順が似ていることに気がつくでしょう。歩きまわって、敵を殺して、経験値を稼いで、お金を稼いで、レベルアップして、新しい能力を得て、冒険をクリアするのです。 延々と極めていくのです。 やり込む人は、楽をするためにマクロや歩きまわるためのボットといったものさえ作って、キャラクタに指示を出します。

Progress Questではこれらの汎用的な手順をすべて踏んでいって、あなたがすべきことが座って、あなたのキャラクタがすべての仕事をするのを楽しむように、シナリオを1つのゲームに仕立て上げていきます:

../_images/progressquest.jpg

この素敵なゲームの作者、Eric Fredricksenの許可のもと、Process Questという名前の最小限のErlangクローンを作りました。 Process QuestはProgress Questと似た原則で行うゲームですが、1人プレイのアプリケーションではなく、たくさんのRAWソケットコネクション(telnetに使えます)を保持できるサーバで、誰かがターミナルを使って、一時的に遊べるゲームです。

ゲームは次の部品で構成されています:

25.3.1. regis-1.0.0

regisアプリケーションはプロセスレジストリです。 これは通常のErlangプロセスレジストリと幾分似たインターフェースを持っていますが、これはどんな項でも受け入れることができ、動的であるように作られています。 この特性は、すべての呼び出しがサーバに入るときにシリアライズされるので動作を遅くしますが、通常のプロセスレジストリはこのような動的な処理向きには作られていないので、これを使うよりは良いでしょう。 このガイドが自動的に外部ライブラリを使って自分自身を更新できるなら(非常に骨の折れる作業です)、私は gproc を代わりに使います。 これは2、3のモジュールを持っていて、それぞれ regis.erlregis_server.erlregis_sup.erl という名前です。 regis は他の2つのラッパ(とアプリケーションコールバックモジュール)で、 regis_server は主要なレジストリのgen_server、 regis_sup はアプリケーションのスーパバイザです。

25.3.2. processquest-1.0.0

これはアプリケーションの核となる部分です。 ここにゲームのすべてのロジックが含まれています。 敵、お店、戦場、統計などすべてです。 プレーヤー自身は動作し続けるために自分自身にメッセージを送るgen_fsmです。 ここには regis よりも多くのモジュールがあります:

pq_enemy.erl

このモジュールはランダムに戦う敵を選びます。形式は {<<"Name">>, [{drop, {<<"DropName">>, Value}}, {experience, ExpPoints}]} というタプルです。 このモジュールでプレーヤーが敵と戦うことができます。

pq_market.erl

このモジュールでは、お店を実装していて、アイテムの値段と強さの一覧で探すことができます。 アイテムはすべて {<<"Name">>, Modifier, Strength, Value} の形式です。 武器や防具、盾、兜などを取得する関数もあります。

pq_stats.erl

これはあなたのキャラクターの状態を簡単に表示するためのモジュールです。

pq_events.erl

gen_eventイベントマネージャのラッパです。 このモジュールは、サブスクライバが各プレーヤーからのイベントを受け取るために、各自のハンドラを使って接続する汎用的なハブの役割を果たします。 またゲームがその場限りのものにならないようにプレーヤーの動作を一定時間待つような管理も行います。

pq_player.erl

中心となるモジュールです。 これは、戦場にいる、お店に行く、を延々と繰り返すgen_fsmです。 このモジュールでは上記のモジュールにある関数をすべて使います。

pq_sup.erl

pq_eventpq_player プロセスの上にあるスーパバイザです。 この2つのモジュールは一緒に動作している必要があり、そうでなければ、プレーヤープロセスが役に立たなくなるか孤立し、あるいはイベントマネージャが以後まったくイベントを受信しなくなります。

pq_supersup.erl

アプリケーションの最上位にあるスーパバイザです。 これは大量の pq_sup プロセスの上位に存在します。 このモジュールによって好きなだけプレーヤーを生成できます。

processquest.erl

ラッパとアプリケーションコールバックモジュールです。 このモジュールはプレーヤーに基本的なインターフェースを提供します。プレーヤーを起動して、イベントをサブスクライブします。

25.3.3. sockserv-1.0.0

../_images/sock.png

これはカスタマイズしたRAWソケットサーバで、 processquest アプリケーションとだけやり取りできるように実装されています。 これはあるクライアントに文字列を送信するTCPソケットを管理するgen_serverを生成します。 確認になりますが、接続にはtelnetを使うことでしょう。 telnetは技術的にはRAWソケット接続のために作られたわけではなく、独自のプロトコルを実装しているのですが、たいていのモダンなクライアントであれば問題な受信できます。 以下モジュールを挙げていきます:

sockserv_trans.erl

これはプレーヤーのイベントマネージャから受け取ったメッセージを表示可能な文字列に変換します。

sockserv_pq_events.erl

プレーヤーから来たどんなイベントも受け取りソケットのgen_serverに投入する、簡素なイベントハンドラです。

sockserv_serv.erl

接続を許可し、クライアントと通信し、情報をクライアントに転送するgen_serverです。

sockserv_sup.erl

大量のソケットサーバを監視します。

sockserv.erl

アプリケーション全体のアプリケーションコールバックモジュールです。

25.3.4. リリース

processquest という名前のディレクトリの配下に次のような構造ですべてのファイルを配置しました:

apps/
- processquest-1.0.0
  - ebin/
  - src/
  - ...
- regis-1.0.0
  - ...
- sockserv-1.0.0
  - ...
rel/
  (will hold releases)
processquest-1.0.0.config

これに基づいてリリースをビルドします。

Note

processquest-1.0.0.config の中身を見てみると、 cryptosasl といったアプリケーションがあるのがわかると思います。 Cryptoは擬似乱数生成器をきちんと初期化するために必要で、SASLはシステムでappupを有効にするために必須です。 リリース内にSASLを入れ忘れた場合は、システムを更新することはできません。

設定ファイル内に {excl_archive_filters, [".*"]} という新しいフィルタも導入されました。 このフィルタは、 .ez ファイルが生成されないようにし、通常のファイルとディレクトリだけが生成されるようにします。 この設定はこれから使うツールが必要なものを見つけるのに .ez ファイルの中を見に行くことができないためです。

また debug_info を取り除くような指示も見当たらないことに気付くでしょう。 debug_info 無しでは、なんらかの理由でappupが失敗してしまいます。

前の章の指示に従うと、すべてのアプリケーションにおいて erl -make を呼び出すことから始めます。 それが完了したら、 processquest ディレクトリでErlangシェルを起動して、次のように入力します:

1> {ok, Conf} = file:consult("processquest-1.0.0.config"), {ok, Spec} = reltool:get_target_spec(Conf), reltool:eval_target_spec(Spec, code:root_dir(), "rel").
ok

機能的リリースを行うべきでしょう。 やってみましょう。 まず、どんなバージョンでもいいので、次のコマンドでVMを起動します。 ./rel/bin/erl -sockserv port 8888 (ポート番号は好きな番号で構いません。デフォルトは8082です。) これでプロセスが起動する際の大量のログが表示されて(これはSASLの機能の1つです)から、通常のErlangシェルが起動します。 localhost上で好きなクライアントを使ってtelnetセッションを開始します:

$ telnet localhost 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
What's your character's name?
hakvroot
Stats for your character:
 Charisma: 7
 Constitution: 12
 Dexterity: 9
 Intelligence: 8
 Strength: 5
 Wisdom: 16

Do you agree to these? y/n

私にしては知恵とカリスマ性が高すぎるような気がします。 n を入力して <Enter> を押します:

n
Stats for your character:
 Charisma: 6
 Constitution: 12
 Dexterity: 12
 Intelligence: 4
 Strength: 6
 Wisdom: 10

Do you agree to these? y/n

おお、これはいい、醜くて、バカで、弱い。 まさに、私を元にしたヒーローに求めていたものです:

y
Executing a Wildcat...
Obtained Pelt.
Executing a Pig...
Obtained Bacon.
Executing a Wildcat...
Obtained Pelt.
Executing a Robot...
Obtained Chunks of Metal.
...
Executing a Ant...
Obtained Ant Egg.
Heading to the marketplace to sell loot...
Selling Ant Egg
Got 1 bucks.
Selling Goblin hair
Got 1 bucks.
...
Negotiating purchase of better equipment...
Bought a plastic knife
Heading to the killing fields...
Executing a Pig...
Obtained Bacon.
Executing a Ant...

いいでしょう、これで十分です。 quit と入力して <Enter> を押して接続を閉じます:

quit
Connection closed by foreign host.

続けたければ、そのままにして、レベルが上がったり、スターを集めたりといったのを見てもいいでしょう。 このゲームは基本的な動作はしています。多くのクライアントで試してもいいでしょう。 問題なく動作すると思います。

素晴らしくないですか。 ではそろそろ次に...

25.4. Process Questを改良する

../_images/ant.png

Process Questアプリケーションの現在のバージョンではいくつか問題があります。 まずはじめに、倒すべき敵の種類が非常に少ないです。 次に、表示される文章が少しおかしいです。(「アリを実行する(Executing a Ant)」ってどういうこと...) 3つ目の問題は、ゲームが少々単純すぎるということです。 ということで、冒険にモードを追加してみましょう! 他の問題は、通常のゲームであればアイテムの値段はレベルに直接比例していますが、私たちのゲームではそうではありません。 最後に、この問題はソースコードを読んで自分の側でクライアントを閉じてみないとわからないのですが、クライアントが接続を閉じてしまうと、プレーヤープロセスはサーバ側で生きたままになってしまいます。 なんてことったい、これはメモリリークになります!

これを直さなければいけません! まず最初に、修正が必要な両方のアプリケーションのコピーを作ることから始めました。 これまであったものに processquest-1.1.0sockserv-1.0. が加わりました。(バージョンの付け方は MajorVersion.Enhancements.BugFixes です。) それから必定な変更をすべて実装しました。 詳細はこの章で見ていくには多すぎるので、すべては見ていきません。この章ではアプリケーションの更新に関して見ていくのであって、その細かく込み入った話を知るためではありません。 これらの複雑な話をすべて理解したい方のために、理解する必要がある情報を探せるように、コードの中に丁寧にコメントを書きました。 ではまず、 processquest-1.1.0 に対する変更について見てみましょう。 全体として、 pq_enemy.erlpq_events.erlpq_player.erl に変更を加えて、 pq_quest.erl というファイルを追加しました。これはプレーヤーが敵を倒した数に応じて冒険を変化させるものです。 いま挙げたファイルの中で pq_player.erl に加えた変更前と非互換な変更だけが、時間を取って見る必要がある変更です。 私が行った変更は、レコードの修正です。変更前が次のもので:

-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1,
                equip=[], money=0, loot=[], bought=[], time=0}).

これを次のように変更しました:

-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1,
                equip=[], money=0, loot=[], bought=[],
                time=0, quest}).

ここで、 quest フィールドは pq_quest:fetch/0 から与えられる値を保持します。 この変更によって、バージョン1.1.0の code_change/4 関数を修正する必要があります。 事実、これを2度修正する必要があります。まず1回はアップグレードする場合(1.0.0から1.1.0への変更)で、もう1回はダウングレードする場合(1.1.0から1.0.0への変更)です。 幸いにも、OTPはそれぞれの場合について異なる引数を渡してくれます。 アップグレードする場合には、、モジュールのバージョン番号を取得します。 ここではアップグレードに関してはとりあえず置いといて、無視しておきましょう。 ダウングレードするときは、 {down, Version} を取得します。 これで各操作を容易にパターンマッチできます:

code_change({down, _}, StateName, State, _Extra) ->
...;
code_change(_OldVsn, StateName, State, _Extra) ->
....

しかし、ちょっと待って下さい! いつものように、状態を盲目的に取ることはできません。 ここを更新しなければいけません。 問題は、次のようなことができないということです:

code_change(_OldVsn, StateName, S = #state{}, _Extra) ->
....

選択肢が2つあります。 1つ目は、新しい形式の新しいstateレコードを宣言する。 たとえばこのようにできるでしょう:

-record(state, {...}).
-record(new_state, {...}).

それから、モジュール内の各関数節の中にあるレコードを変更しなければいけません。 これは非常に面倒で、リスクを取る価値がありません。 かわりにもう1つの方法として、レコードを展開して、その元となっているタプル形式にするほうが簡潔でしょう。(一般的なデータ構造への小さな旅 を思い出してください。)

code_change({down, _},
            StateName,
            #state{name=N, stats=S, exp=E, lvlexp=LE, lvl=L, equip=Eq,
                   money=M, loot=Lo, bought=B, time=T},
            _Extra) ->
    Old = {state, N, S, E, LE, L, Eq, M, Lo, B, T},
    {ok, StateName, Old};
code_change(_OldVsn,
            StateName,
            {state, Name, Stats, Exp, LvlExp, Lvl, Equip, Money, Loot,
             Bought, Time},
             _Extra) ->
    State = #state{
        name=Name, stats=Stats, exp=Exp, lvlexp=LvlExp, lvl=Lvl, equip=Equip,
        money=Money, loot=Loot, bought=Bought, time=Time, quest=pq_quest:fetch()
    },
    {ok, StateName, State}.

そして、 code_change/4 関数があります! これが行なっていることは、両方のタプルの形式の変換です。 新しいバージョンのために、新しい冒険を追加する部分の面倒を見ています。既存プレーヤーを使えない冒険を追加するのは退屈でしょう。 ここでまだ _Extra 変数を無視していることにお気づきでしょう。 この変数はappupファイル(すぐに説明します)から渡されて、その値を選択することになるでしょう。 いまは、この点に関しては無視します。なぜなら1つのリリースから、または1つのリリースへのアップグレードまたはダウングレードしか出来ないからです。 より複雑な事例では、リリース固有の情報を渡したくなるでしょう。

sockserv-1.0.1 アプリケーションでは、 sockserv_serv.erl だけに変更が必要でした。 幸いにも、これは再起動が必要なく、パターンマッチする新しいメッセージを追加しただけでした。

2つのアプリケーションの2つのバージョンが修正されました。 けれども、まだお祝いするには十分ではありません。 OTPにどのような変更異なった動作を必要とするかを知らせる方法を見つけなければいけません。

25.5. Appupファイル

appupファイルは、あるアプリケーションをアップグレードするために行われる必要があるErlangコマンドのリストです。 このファイルにはあるタプルとアトムのリストが書かれていて、各タプルやアトムはどのような場合になにをするべきかが書いてあります。 一般的な形式は次のようなものです:

{NewVersion,
 [{VersionUpgradingFrom, [Instructions]}]
 [{VersionDownGradingTo, [Instructions]}]}.

アップグレードやダウングレードを多くの異なるバージョンにたいして行えるようにするため、このファイルではバージョンのリストが要求されます。 私たちの例では、 processquest-1.1.0 には次のようなタプルが必要になります:

{"1.1.0",
 [{"1.0.0", [Instructions]}],
 [{"1.0.0", [Instructions]}]}.

Instructions には、高レベルなコマンドも低レベルなコマンドも両方含まれます。 しかし、通常は、高レベルなコマンドだけを書けば間に合います:

{add_module, Mod}

モジュール Mod が初めてロードされます。

{load_module, Mod}

モジュール Mod はすでにVM上にロードされていて、変更されました。

{delete_module, Mod}

モジュール Mod はVMから削除されました。

{update, Mod, {advanced, Extra}}

これは Mod を動かしているすべてのプロセスをサスペンドし、モジュール内の code_change 関数を最後の引数となっている Extra を引数として呼び、 Mod を動かしているすべてのプロセスを復帰させます。 Extra は、アップグレードに引数が必要になったときに、 code_change 関数に任意のデータを渡すときに使え、

{update, Mod, supervisor}

これを呼ぶことによって、スーパバイザの init 関数を再定義して、再起動戦略( one_for_onerest_for_one など)に影響を与えたり、子プロセスの仕様を変更します。(既存プロセスには影響しません)

{apply, {M, F, A}}

apply(M,F,A) を呼びます Will call apply(M,F,A).

モジュールの依存関係

{load_module, Mod, [ModDependencies]}{update, Mod, {advanced, Extra}, [ModDeps]} を使って、確実に、ある他のモジュールが事前に処理されたあとにだけコマンドが実行されるようにできます。 これは特に Mod とその依存モジュールが同じアプリケーション内にない場合に便利です。 悲しいことに、 delete_module の指示では同様の依存関係を指定する方法がありません。

アプリケーションを追加または削除する

relupを生成するときには、アプリケーションを削除または追加する上で特別な指示は必要ありません。 relupファイル(リリースをアップグレードするファイル)を生成する関数は、私たちのかわりにこれを検出してくれます。

これらの指示を使って、私たちのアプリケーション用に次の2つのappupファイルを書くことができます。 ファイル名は NameOfYourApp.appup として、アプリケーションの ebin/ ディレクトリに置かなければいけません。 processquest-1.1.0 のappupファイルを以下に示します:

{"1.1.0",
 [{"1.0.0", [{add_module, pq_quest},
             {load_module, pq_enemy},
             {load_module, pq_events},
             {update, pq_player, {advanced, []}, [pq_quest, pq_events]}]}],
 [{"1.0.0", [{update, pq_player, {advanced, []}},
             {delete_module, pq_quest},
             {load_module, pq_enemy},
             {load_module, pq_events}]}]}.

新しいモジュールを追加して、サスペンドが必要ない2つのモジュールを読み込んで、 pq_player を安全にアップグレードする、という手順がわかると思います。 コードをダウングレードするときは、まったく同様の事をしますが、順番は逆です。 面白いのは、ある場合では {load_module, Mod} は新しいバージョンをロードして、別の場合ではこれが古いバージョンをロードするということです。 これはすべてアップグレードかダウングレードかの文脈に依存しています。

sockserv-1.0.1 は変更するモジュールが1つしかない上に、サスペンドが必要ないので、appupファイルはこれだけです:

{"1.0.1",
 [{"1.0.0", [{load_module, sockserv_serv}]}],
 [{"1.0.0", [{load_module, sockserv_serv}]}]}.

おお!次の手順は新しいモジュールを使って新しいリリースを作ることです。 processquest-1.1.0.config がこれです:

{sys, [
    {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]},
    {erts, [{mod_cond, derived},
    {app_file, strip}]},
    {rel, "processquest", "1.1.0",
    [kernel, stdlib, sasl, crypto, regis, processquest, sockserv]},
    {boot_rel, "processquest"},
    {relocatable, true},
    {profile, embedded},
    {app_file, strip},
    {incl_cond, exclude},
    {excl_app_filters, ["_tests.beam"]},
    {excl_archive_filters, [".*"]},
    {app, stdlib, [{mod_cond, derived}, {incl_cond, include}]},
    {app, kernel, [{incl_cond, include}]},
    {app, sasl, [{incl_cond, include}]},
    {app, crypto, [{incl_cond, include}]},
    {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]},
    {app, sockserv, [{vsn, "1.0.1"}, {incl_cond, include}]},
    {app, processquest, [{vsn, "1.1.0"}, {incl_cond, include}]}
]}.

これは古いバージョンのものをコピー&ペーストして、いくつか変更したものです。 まず、2つの新しいアプリケーションを erl -make でコンパイルします。 既にzipファイルをダウンロードしていた場合は、もうコンパイルされているでしょう。 それから新しいリリースを生成できます。 最初に、2つの新しいアプリケーションをコンパイルし、次のように入力します:

$ erl -env ERL_LIBS apps/
1> {ok, Conf} = file:consult("processquest-1.1.0.config"), {ok, Spec} = reltool:get_target_spec(Conf), reltool:eval_target_spec(Spec, code:root_dir(), "rel").
ok

Don’t Drink Too Much Kool-Aid:

なぜsystoolsだけを使わないのでしょうか。 systoolsには問題があるのです。 まず始めに、記載されるバージョンがおかしく、きちんと動作しないappupファイルを生成します。 また、ほとんどドキュメントがなく、reltoolが使うものといくらか似ている程度のディレクトリ構造を想定しています。 しかしながら、最大の問題は、ルートディレクトリにデフォルトのErlangがインストールされている場所を使うということです。これによって、展開するときに多くのパーミッションに関する問題が発生します。

どのツールを使うにしても簡単な方法はなく、多くの手作業を要します。 したがって、両モジュールを使う、かなり複雑な一連のコマンドを並べているのです。なぜなら、このほうが幾分か作業が減るからです。

しかし、待って下さい。もっと手作業が必要になります!

  1. rel/releases/1.1.0/processquest.rel をコピーして rel/releases/1.1.0/processquest-1.1.0.rel とする
  2. rel/releases/1.1.0/processquest.boot をコピーして rel/releases/1.1.0/processquest-1.1.0.boot とする
  3. rel/releases/1.1.0/processquest.boot をコピーして rel/releases/1.1.0/start.boot とする
  4. rel/releases/1.0.0/processquest.rel をコピーして rel/releases/1.0.0/processquest-1.0.0.rel とする
  5. rel/releases/1.0.0/processquest.boot をコピーして rel/releases/1.0.0/processquest-1.0.0.boot とする
  6. rel/releases/1.0.0/processquest.boot をコピーして rel/releases/1.0.0/start.boot とする

これで、relupファイルを生成できます。 Erlangシェルを起動し、次のコマンドを打って、実際に生成してみましょう:

$ erl -env ERL_LIBS apps/ -pa apps/processquest-1.0.0/ebin/ -pa apps/sockserv-1.0.0/ebin/
1> systools:make_relup("./rel/releases/1.1.0/processquest-1.1.0", ["rel/releases/1.0.0/processquest-1.0.0"], ["rel/releases/1.0.0/processquest-1.0.0"]).
ok

環境変数 ERL_LIBS は最新バージョンのアプリケーションしか探しに行かないため、systoolsのrelupジェネレータが必要な物をすべて見つけられるように -pa <古いアプリケーションへのパス> を追加する必要があります。 これが終わったら、relupファイルを rel/releases/1.1.0 に移します。 このディレクトリはコードをアップグレードする際に正しいファイルを探すために確認されます。 しかし問題が1つあって、リリースハンドラモジュールが、そのディレクトリに存在していると想定される多くのファイルに依存しているのですが、必ずしもそこにある必要はありません。

../_images/take-a-break2.png

25.6. リリースをアップグレードする

いいですね、これでrelupファイルが手に入りました。 しかし、これを使えるようにするにはまだやることがたくさんあります。 次の手順は、リリースの新しいバージョンをすべてtarファイルにすることです。

2> systools:make_tar("rel/releases/1.1.0/processquest-1.1.0").
ok

ファイルは rel/releases/1.1.0 に格納されます。 これを手動で rel/releases に移動し、名前を変更して、バージョン番号を追加する必要があります。 よりハードコードジャンキーですね!これらの手作業を私たちはこのコマンドで行います。 $ mv rel/releases/1.1.0/processquest-1.1.0.tar.gz rel/releases/

さて、今度は本当のプロダクションアプリケーションを起動する前に必ず行いたい作業です。 この手順は、relupの後に最初のバージョンにロールバックできるように、アプリケーションを起動する前に行われる必要があります。 この手順を行わないと、はじめにあったバージョンではなく、それよりも新しいリリースのものにしかダウングレードできなくなります!

それではシェルを開いて、実行してみます:

1> release_handler:create_RELEASES("rel", "rel/releases", "rel/releases/1.0.0/processquest-1.0.0.rel", [{kernel,"2.14.4", "rel/lib"}, {stdlib,"1.17.4","rel/lib"}, {crypto,"2.0.3","rel/lib"},{regis,"1.0.0", "rel/lib"}, {processquest,"1.0.0","rel/lib"},{sockserv,"1.0.0", "rel/lib"}, {sasl,"2.1.9.4", "rel/lib"}]).

この関数の一般的な書式は release_handler:create_RELEASES(RootDir, ReleasesDir, Relfile, [{AppName, Vsn, LibDir}]) です。 この関数は rel/releases というディレクトリ(あるいは他の ReleasesDir )に RELEASES という名前のファイルを作成します。このファイルは、relupがリロードするファイルやモジュールを探す際に必要なリリースに関する基本情報を持っています。

これで、古いバージョンのコードの起動を開始することができます。 rel/bin/erl を起動すると、デフォルトでは1.1.0のリリースを起動します。 これは、VMを起動する前に新しいリリースを作成したことによるものです。 いまからデモでは、リリースを ./rel/bin/erl -boot rel/releases/1.0.0/processquest というコマンドで起動する必要があります。 ライブアップグレードが行われている様子を確認できるように、telnetクライアントを立ち上げて、私たちのソケットサーバに接続してみましょう。

アップグレードの準備が万端だと思ったら、ProcessQuestを実行しているErlangシェルにいって、次の関数を呼び出してみてください:

1> release_handler:unpack_release("processquest-1.1.0").
{ok,"1.1.0"}
2> release_handler:which_releases().
[{"processquest","1.1.0",
  ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3",
   "regis-1.0.0","processquest-1.1.0","sockserv-1.0.1",
   "sasl-2.1.9.4"],
  unpacked},
 {"processquest","1.0.0",
  ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3",
   "regis-1.0.0","processquest-1.0.0","sockserv-1.0.0",
   "sasl-2.1.9.4"],
  permanent}]

2番目のプロンプトが、リリースをアップグレードする準備が出来ているけれど、まだインストールされていない、あるいは恒久的にはなっていないことを表しています。 これをインストールするには、次のコマンドを実行します:

3> release_handler:install_release("1.1.0").
{ok,"1.0.0",[]}
4> release_handler:which_releases().
[{"processquest","1.1.0",
  ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3",
   "regis-1.0.0","processquest-1.1.0","sockserv-1.0.1",
   "sasl-2.1.9.4"],
  current},
 {"processquest","1.0.0",
  ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3",
   "regis-1.0.0","processquest-1.0.0","sockserv-1.0.0",
   "sasl-2.1.9.4"],
   permanent}]

いま、リリース 1.1.0が走っていますが、まだそこに永続的にいるわけではありません。 まだ、アプリケーションをそのままで残しておくこともできます。 新しいリリースを永続化するために次の関数を呼出します:

5> release_handler:make_permanent("1.1.0").
ok.

ちくしょう。 大量のプロセスが死んでいます。(上の例ではエラー出力は取り除きました。) telnetクライアントを見なければ、順調にアップグレードされたように見えます。 問題は、TCPコネクションを受け入れることはブロックする操作なので、socksrv内で接続を待っていたすべてのgen_serverがメッセージを待ち受けることができなかったということです。 したがって、新しいバージョンのコードがロードされて、VMに殺された時、サーバはアップグレードできませんでした。 これを確認してみましょう:

6> supervisor:which_children(sockserv_sup).
[{undefined,<0.51.0>,worker,[sockserv_serv]}]
7> [sockserv_sup:start_socket() || _ <- lists:seq(1,20)].
[{ok,<0.99.0>},
 {ok,<0.100.0>},
 ...
 {ok,<0.117.0>},
 {ok,<0.118.0>}]
8> supervisor:which_children(sockserv_sup).
[{undefined,<0.112.0>,worker,[sockserv_serv]},
 {undefined,<0.113.0>,worker,[sockserv_serv]},
 ...
 {undefined,<0.109.0>,worker,[sockserv_serv]},
 {undefined,<0.110.0>,worker,[sockserv_serv]},
 {undefined,<0.111.0>,worker,[sockserv_serv]}]

最初のコマンドでは、接続を待っていたすべての子プロセスはすでに死んでいたことを示しています。 残ったプロセスは生きたセッションが走っているプロセスです。 これを見ると、コードを反応の良い物にしておくことの大切さが分かります。 プロセスをメッセージを受け取って、それに対する処理を実行できるようにしておいたいので、上手く動作しています。

../_images/couch.png

前の2つのコマンドでは、問題を直すため、より多くのワーカを起動しました。 これは上手くいきますが、アップグレードを実施した人の手作業が必要となります。 どのような場合においても、これは理想的な方法ではありません。 この問題を解決するより良い方法としては、アプリケーションワーカが動作する方法を変更して、 sockserv_sup に子プロセスがいくつあるかを見る監視プロセスを持たせるようにすることです。 子プロセスの数がある閾値以下になったら、監視プロセスはそれより多くの子プロセスを起動します。 他の戦略は、接続の受け入れを1度に数秒間のブロックを行うことで完了し、ブロックによる中断の後に、その間に行われたメッセージ送信のリトライを続けるという方法です。 この方法はgen_serverに自身がアップグレードするのに必要な時間を与えます。この時間は、リリースをインストールしてから、永続化するまでの時間を想定しています。 私は怠け者なので、今挙げた戦略のどちらか、あるいは両方を実装するのは読者のみなさんの演習にします。 この例で見たようなクラッシュを見ることで、実際のシステムの更新を行う前にコードをテストする良いきっかけとなるでしょう。

とにかく、私たちは問題を解決したので、アップグレードがどのようになったか見てみたいですよね:

9> release_handler:which_releases().
[{"processquest","1.1.0",
  ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3",
   "regis-1.0.0","processquest-1.1.0","sockserv-1.0.1",
   "sasl-2.1.9.4"],
  permanent},
 {"processquest","1.0.0",
  ["kernel-2.14.4","stdlib-1.17.4","crypto-2.0.3",
   "regis-1.0.0","processquest-1.0.0","sockserv-1.0.0",
   "sasl-2.1.9.4"],
  old}]

ガッツポーツです。 release_handler:install(OldVersion). を実行することでダウングレードを試して見ることができます。 これは、自身をアップグレードしていないプロセスをたくさん殺してしまうリスクがありますが、うまくいくでしょう。

Don’t Drink Too Much Kool-Aid:

何らかの理由で、この章で紹介した方法で最初のバージョンのリリースへのロールバックが常に失敗する場合、おそらく RELEASES ファイルの作成を忘れている可能性があります。 これは release_handler:which_releases() を呼んだときに、 {YourRelease,Version,[],Status} の中に空のリストがあるかどうかで確認できます。 ここに入るリストは、読み込みやリロードをするモジュールを探す場所のリストで、VMを起動して RELEASES ファイルを読み込んだり、あるいは新しいリリースを展開するときに最初に作られるものです。

次のリストが、機能的なrelupを得るために取らなければいけないアクションのすべてです:

  1. まず、最初のイテレーションとしてOTPアプリケーションを書きます
  2. コンパイルします
  3. Reltoolを使ってリリース(1.0.0)を作成します。これはデバッグ情報付きで、 .ez アーカイブは作成しないようにします。
  4. プロダクションアプリケーションを起動する前に確実に RELEASES ファイルを作成します。これは release_handler:create_RELEASES(RootDir, ReleasesDir, Relfile, [{AppName, Vsn, LibDir}]) で行えます。
  5. リリースを実行します!
  6. バグを直します
  7. アプリケーションの新しいバージョンのバグを直します
  8. アプリケーションごとにappupファイルを書きます
  9. 新しいアプリケーションをコンパイルします
  10. 新しいリリース(私たちの例では1.1.0)を生成します。その際デバッグ情報は込みで、 .ez アーカイブは作成しません。
  11. rel/releases/NewVsn/RelName.rel をコピーして rel/releases/NewVsn/RelName-NewVsn.rel とします
  12. rel/releases/NewVsn/RelName.boot をコピーして rel/releases/NewVsn/RelName-NewVsn.boot とします
  13. rel/releases/NewVsn/RelName.boot をコピーして rel/releases/NewVsn/start.boot とします
  14. rel/releases/OldVsn/RelName.rel をコピーして rel/releases/OldVsn/RelName-OldVsn.rel とします
  15. rel/releases/OldVsn/RelName.boot をコピーして rel/releases/OldVsn/RelName-OldVsn.boot とします
  16. rel/releases/OldVsn/RelName.boot をコピーして rel/releases/OldVsn/start.boot とします
  17. relupファイルを次のコマンドで生成します。 systools:make_relup("rel/releases/Vsn/RelName-Vsn", ["rel/releases/OldVsn/RelName-OldVsn"], ["rel/releases/DownVsn/RelName-DownVsn"]).
  18. relupファイルを rel/releases/Vsn に移動します
  19. 新しいリリースのtarファイルを systools:make_tar("rel/releases/Vsn/RelName-Vsn") で生成します
  20. tarファイルを rel/releases/ に移します
  21. 最初のバージョンのリリースをまだ実行させているシェルを開きます
  22. release_handler:unpack_release("NameOfRel-Vsn") を呼び出します
  23. release_handler:install_release(Vsn) を呼び出します
  24. release_handler:make_permanent(Vsn) を呼び出します
  25. ちゃんと動作していることを確認します。もしうまく動いていなかったら、古いバージョンをインストールすることでロールバックします。

これを自動化するためにスクリプトを書きたくことなるでしょう。

../_images/podium.png

再度になりますが、relupはOTPの中でも非常に煩雑な場所で、理解が難しい箇所です。 たくさんの新しいエラーを見つけるでしょうし、そのエラーはどれも、今まで見てきたものと比較しても理解し難いものだと思います。 諸々動作させるにはいくつか想定できますし、リリースを作成するときにどのツールを選ぶかで何をすべきかは代わります。 sys モジュールの関数を使って独自の更新用のコードを書きたくなる衝動にすら駆られるでしょう! あるいは、いくつかの苦痛になる手順を自動化してくれる rebar のようなツールを使うかもしれません。 いずれにせよ、この章の内容や、そこで挙げた例は、客観性を自称する筆者が持てる知識の中で最も有用なものです。

relupなしにアプリケーションをアップグレードすることができるなら、私はその方法を推奨します。 relupを使っているEricssonの部署が彼らのアプリケーション自体のテストをするときに、それと同じくらいrelupのテストに時間を割いたと言われています。 relupは決して終了させることができないような製品を扱うときに使われるツールです。 たいていrelupを使う中での面倒なこと経験して、これらの必要性が分かるようになることでしょう。(循環論法ですね!) その必要が出てきた時には、relupはまったくもって便利です。

そろそろ、Erlangのより扱いやすい機能について学んでみましょうか。