プロセス探求でレベルアップ

appup と relup のつまずき

コードのホットローディングは、Erlang では最も簡単なことの一つです。再コンパイルし、完全修飾された関数を呼び出し、楽しむだけです。しかし、正しく安全に行うのは、はるかに難しいことです。

コードのリロードを問題にする、非常に単純な課題があります。素晴らしい Erlang プログラミング脳を使い、gen_server プロセスを想像してみましょう。このプロセスには、ある種の引数を受け取る handle_cast/2 関数があります。これを別の引数を受け取るように更新し、コンパイルして、本番環境にプッシュします。すべては順調で、アプリケーションをシャットダウンしたくないので、本番環境の VM にロードして実行することにしました。

A chain of evolution/updates. First is a monkey, second is a human-like creature, both separated by an arrow with 'Update' written under it. Then appears an arrow with an explosion saying 'failed upgrade', pointing from the human-like creature to a pile of crap and a tombstone saying 'RIP, YOU'

すると、エラーレポートが大量に流れ込んできます。異なる 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(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) の呼び出しはすでに完全修飾されており、コードをリロードして update メッセージを処理するまでの間に、{call, From, Msg} という形式のメッセージを受信する可能性があります。その場合、制御されていない方法でモジュールを更新することになります。そして、クラッシュします。

正しく行うための秘密は、OTP の内部に埋もれています。時の砂を凍らせる必要があります! そうするためには、さらに秘密のメッセージが必要です。プロセスを保留にするメッセージ、コードを変更するメッセージ、そして以前の動作を再開するメッセージです。OTP の動作の奥深くには、そのような管理をすべて行うための特別なプロトコルが隠されています。これは、sys モジュールと、SASL(システムアーキテクチャサポートライブラリ)アプリケーションの一部である release_handler という 2 つ目のモジュールによって行われます。これらはすべてを処理します。

秘訣は、sys:suspend(PidOrName) を呼び出すことで OTP プロセスを中断できることです(すべてのプロセスは、監視ツリーを使用して、各監視者が持つ子を調べることで見つけることができます)。次に、sys:change_code(PidOrName, Mod, OldVsn, Extra) を使用して、プロセスを強制的に更新し、最後に sys:resume(PidOrName) を呼び出して再び動作させます。

これらの関数を常にアドホックスクリプトを記述して手動で呼び出すのは、あまり実用的ではありません。代わりに、relup がどのように行われるかを見てみましょう。

Erl の 9 番目の円

The 9 circles are: 0 (vestibule): handling syntax; 1. Records are tuples; 2. sharing nothing; 3. thinking asynchronously; 4. OTP behaviours (gen_server, gen_fsm, gen_event, supervisor); 5. OTP Applications; 6. Parse Transforms; 7. Common Test; 8. Releases; 9. Relups; and the center of erl is the distributed world with netsplits.

実行中のリリースを取得し、その 2 番目のバージョンを作成し、実行中に更新する行為は危険です。appup(個々のアプリケーションを更新する方法に関する命令を含むファイル)と relup(リリース全体を更新する命令を含むファイル)の単純なアセンブリのように見えるものは、API と文書化されていない仮定を通しての闘争にすぐになります。

私たちは、OTP の最も複雑な部分の 1 つに足を踏み入れています。理解して正しく行うのは難しく、時間がかかります。実際、この手順(これからは relup と呼ばれる)全体を避け、VM を再起動して新しいアプリケーションを起動することで、単純なローリングアップグレードを実行できるのであれば、そうすることをお勧めします。Relup は、これらの「やるか死ぬか」ツールの 1 つであるべきです。他に選択肢がほとんどない場合に使うものです。

リリースアップグレードを扱う際には、いくつかの異なるレベルがあります。

それぞれが、前のものよりも複雑になる可能性があります。ここでは、最初の 3 つのステップをどのように行うかを見てきただけです。以前のバージョンよりも長期実行アップグレードに適したアプリケーション(え、再起動なしで正規表現を実行するのは誰が気にするの?)を操作できるようにするために、素晴らしいビデオゲームを紹介します。

プログレスクエスト

Progress Quest は、革新的なロールプレイングゲームです。実際、RPG の OTP と呼べるでしょう。以前に RPG をプレイしたことがある人なら、多くのステップが似ていることに気づくでしょう。走り回る、敵を倒す、経験値を獲得する、お金を得る、レベルアップする、スキルを得る、クエストを完了する。これを永遠に繰り返します。パワープレイヤーは、マクロやボットなどのショートカットを使用して、周囲を移動し、自分に代わって入札を行います。

Progress Quest は、これらの一般的な手順をすべて取り、キャラクターがすべての作業を行うのをただ座って楽しむことができる、合理化されたゲームに変えました。

A screenshot of Progress Quest

この素晴らしいゲームの作成者である Eric Fredricksen 氏の許可を得て、私は Process Quest という、非常にシンプルな Erlang クローンを作成しました。Process Quest は、原則として Progress Quest に似ていますが、シングルプレイヤーアプリケーションではなく、多数の生のソケット接続(telnet を介して使用可能)を保持できるサーバーであり、ターミナルを使用して一時的にゲームをプレイすることができます。

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

regis-1.0.0

regis アプリケーションは、プロセスレジストリです。通常の Erlang プロセスレジストリとやや似たインターフェースを持っていますが、任意の用語を受け入れることができ、動的であることを意図しています。サーバーに入るときにすべての呼び出しがシリアル化されるため、処理が遅くなる可能性がありますが、その種の動的な作業には適していない通常のプロセスレジストリを使用するよりも優れています。このガイドが外部ライブラリで自動的に更新されることができれば(それは大変な作業です)、代わりに gproc を使用していたでしょう。これには、いくつかのモジュール、つまり regis.erlregis_server.erl、および regis_sup.erl が含まれています。最初のものは、他の 2 つのラッパー(およびアプリケーションコールバックモジュール)であり、regis_server はメインの登録 gen_server であり、regis_sup はアプリケーションの監視者です。

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_event および pq_player プロセスのペアの上にある監視者。プレイヤープロセスは孤立して役に立たなくなったり、イベントマネージャーがイベントをまったく取得できなくなったりするため、両方が連携して動作する必要があります。
pq_supersup.erl
アプリケーションのトップレベルの監視者。これは、多数の pq_sup プロセスの上にあります。これにより、必要な数のプレイヤーを生成できます。
processquest.erl
ラッパーおよびアプリケーションコールバックモジュール。プレイヤーへの基本的なインターフェースを提供します。1 つ起動し、イベントをサブスクライブします。

sockserv-1.0.0

A rainbow-colored sock

processquest アプリでのみ機能するように作成された、カスタマイズされた生のソケットサーバー。文字列をクライアントにプッシュする TCP ソケットを担当する gen_server をそれぞれ生成します。ここでも、telnet を使用して操作できます。厳密には、telnet は生のソケット接続用に作成されたものではなく、独自のプロトコルですが、ほとんどの最新クライアントは問題なく受け入れます。以下にそのモジュールを示します。

sockserv_trans.erl
これは、プレイヤーのイベントマネージャーから受信したメッセージを印刷可能な文字列に変換します。
sockserv_pq_events.erl
プレイヤーから来るすべてのイベントを受け取り、それらをソケット gen_server にキャストする単純なイベントハンドラー。
sockserv_serv.erl
接続を受け入れ、クライアントと通信し、情報をクライアントに転送する責任を負う gen_server。
sockserv_sup.erl
ソケットサーバーの束を監督します。
sockserv.erl
アプリケーション全体のアプリケーションコールバックモジュール。

リリース

すべてを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

これをベースに、リリースを構築できます。

注意: 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

機能的なリリースができたはずです。試してみましょう。`./rel/bin/erl -sockserv port 8888` (または任意のポート番号。デフォルトは8082)を実行して、VMの任意のバージョンを起動します。これにより、プロセスが起動されたというログがたくさん表示され(これが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...

OK、私にはこれで十分です。`quit` を入力してから `<Enter>` を入力して接続を閉じます。

quit
Connection closed by foreign host.

必要であれば、開いたままにして、レベルアップ、ステータスアップなどを確認できます。ゲームは基本的に機能しており、多くのクライアントで試すことができます。問題なく継続できるはずです。

素晴らしいですよね?さて...

Process Questの改善

an ant being beheaded with a tiny axe

Process Questの現在のバージョンのアプリケーションには、いくつかの問題があります。まず、倒す敵の種類が非常に少ないです。次に、テキストが少し奇妙に見えます(「Executing a Ant...」とは何でしょう)。3つ目の問題は、ゲームが少し単純すぎることです。クエストモードを追加しましょう!もう1つは、アイテムの価値が実際のゲームではレベルに直接結びついているのに対し、私たちのバージョンではそうではないことです。最後に、これはコードを読んで自分でクライアントを閉じようとしない限り分からないことですが、クライアントが接続を閉じると、サーバー上でプレーヤーのプロセスが生き残ったままになります。ええと、メモリリークです!

これを修正する必要があります!まず、修正が必要な両方のアプリケーションの新しいコピーを作成しました。これで、他のアプリケーションに加えて、`processquest-1.1.0` と `sockserv-1.0.1` があります(`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つ目の選択肢は、新しい形式を持つ新しい状態レコードを宣言することです。最終的には、次のようになります。

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

そして、モジュールの各関数節でレコードを変更する必要があります。それは面倒で、リスクを冒す価値はありません。代わりに、レコードをその基になるタプル形式に展開する方が簡単です(共通のデータ構造への短い訪問を参照してください)。

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つのリリースとの間でのみ可能なため、気にしません。より複雑なケースでは、リリース固有の情報をそこに渡したいかもしれません。

`sockserv-1.0.1` アプリケーションでは、sockserv_serv.erl のみが変更を必要としました。幸いなことに、再起動は必要なく、一致させるための新しいメッセージのみが必要でした。

2つのアプリケーションの2つのバージョンが修正されました。しかし、それだけでは安心して進むことはできません。OTPに、どのような種類の変更が異なる種類の操作を必要とするかを知らせる方法を見つける必要があります。

Appupファイル

appupファイルは、特定のアプリケーションをアップグレードするために実行する必要があるErlangコマンドのリストです。それらには、何をすべきか、いつ行うかを指示するタプルとアトムのリストが含まれています。一般的な形式は次のとおりです。

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

多くの異なるバージョンにアップグレードおよびダウングレードできるため、バージョンのリストを要求します。私たちのケースでは、`processquest-1.1.0` の場合、これは次のようになります。

{"1.1.0",
 [{"1.0.0", [Instructions]}],
 [{"1.0.0", [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_one`、`rest_for_one`など)に影響を与えたり、子仕様を変更したりできます(これは既存のプロセスには影響しません)。
{apply, {M, F, A}}
`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, [{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}]}
]}.

これは、いくつかのバージョンが変更された古いもののコピー/貼り付けです。まず、`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

クーラードを飲みすぎないでください
なぜ `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/に移動します。このディレクトリは、コードを更新する際に適切なものを探すために参照されます。ただし、リリースハンドラモジュールは、存在することを前提としているものの、必ずしも存在しない可能性のある多くのファイルに依存するという問題があります。

A cup of coffee with cookies and a spoon. Text says 'take a break'

リリースのアップグレード

よし、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クライアントを見ると、正常にアップグレードされたように見えます。問題は、sockservで接続を待機していたすべてのgen_serverが、TCP接続の受け入れがブロッキング操作であるため、メッセージをリッスンできなかったことです。したがって、サーバーは新しいバージョンのコードがロードされたときにアップグレードできず、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]}]

最初のコマンドは、接続を待機していたすべての子プロセスが既にダウンしていることを示しています。残っているプロセスは、アクティブなセッションが進行中のプロセスです。これは、コードの応答性を維持することの重要性を示しています。プロセスがメッセージを受信して​​それに応じた動作ができていれば、問題はなかったはずです。

A couch, with 'heaven' written on it

最後の2つのコマンドでは、問題を修正するためにより多くのワーカーを起動しただけです。これは機能しますが、アップグレードを実行する人が手動で操作する必要があります。いずれにしても、これは最適とは言えません。問題を解決するより良い方法は、sockserv_supが持つ子プロセスの数を監視する監視プロセスを導入するようにアプリケーションの動作を変更することです。子の数が特定のしきい値を下回った場合、監視プロセスはさらに多くの子を起動します。別の戦略は、接続の受け入れを数秒間隔でブロッキングするようにコードを変更し、メッセージを受信できる一時停止後に再試行を続けることです。これにより、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).を実行して、インストールをダウングレードしてみてください。これは正常に機能するはずですが、自身を更新しなかったプロセスがさらに強制終了するリスクがあります。

クーラードを飲みすぎないでください
何らかの理由で、この章で説明した手法を使用してリリースの最初のバージョンにロールバックしようとすると常に失敗する場合は、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.relrel/releases/NewVsn/RelName-NewVsn.relとしてコピーする
  12. rel/releases/NewVsn/RelName.bootrel/releases/NewVsn/RelName-NewVsn.bootとしてコピーする
  13. rel/releases/NewVsn/RelName.bootrel/releases/NewVsn/start.bootとしてコピーする
  14. rel/releases/OldVsn/RelName.relrel/releases/OldVsn/RelName-OldVsn.relとしてコピーする
  15. rel/releases/OldVsn/RelName.bootrel/releases/OldVsn/RelName-OldVsn.bootとしてコピーする
  16. rel/releases/OldVsn/RelName.bootrel/releases/OldVsn/start.bootとしてコピーする
  17. systools:make_relup("rel/releases/Vsn/RelName-Vsn", ["rel/releases/OldVsn/RelName-OldVsn"], ["rel/releases/DownVsn/RelName-DownVsn"]).を使用してrelupファイルを生成する
  18. relupファイルをrel/releases/Vsnに移動する
  19. systools:make_tar("rel/releases/Vsn/RelName-Vsn").を使用して新しいリリースのtarファイルを生成する
  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. すべてが正常に進んだことを確認する。そうでない場合は、古いバージョンをインストールしてロールバックする。

これらを自動化するスクリプトをいくつか作成することをお勧めします。

A podium with 3 positions: 1. you, 2. relups, 3. the author (3rd person)

繰り返しますが、relupはOTPの非常に厄介な部分であり、理解するのが難しい部分です。多くの新しいエラーが見つかる可能性があり、それらは前のエラーよりも理解するのが難しいでしょう。実行方法についてはいくつかの前提があり、リリースを作成するときに異なるツールを選択すると、実行方法が変更されます。sysモジュールの関数を使用して独自の更新コードを作成したくなるかもしれません。または、痛みを伴うステップの一部を自動化するrebar3のようなツールを使用することもできます。いずれにせよ、この章とその例は、著者自身のことを三人称で書くことを時々楽しむ人物である著者の最高の知識に基づいて書かれています。

relupを必要としない方法でアプリケーションをアップグレードできる場合は、そうすることをお勧めします。relupを使用するエリクソンの部門は、アプリケーション自体をテストするのと同じくらい多くの時間をテストに費やしていると言われています。これらは、絶対にシャットダウンできない製品を使用する場合に使用するツールです。それらを使用するのに苦労する準備ができているため、主にそれらが必要な時期がわかるでしょう(この循環論理を気に入ってください!)。必要が生じた場合、relupは完全に役立ちます。

さて、Erlangのより使いやすい機能について学んでみませんか?