檢視原始碼 Appup 食譜

此章節包含在運行時進行升級/降級的典型案例的 .appup 檔案範例。

變更功能模組

當功能模組已變更時,例如,如果已新增一個新函數或已修正錯誤,則簡單的程式碼替換就足夠了,例如

{"2",
 [{"1", [{load_module, m}]}],
 [{"1", [{load_module, m}]}]
}.

變更駐留模組

在根據 OTP 設計原則實作的系統中,除了系統程序和特殊程序之外,所有程序都駐留在 supervisorgen_servergen_statemgen_eventgen_fsm 的其中一種行為中。這些屬於 STDLIB 應用程式,升級/降級通常需要重新啟動運行時系統。

因此,OTP 不提供變更駐留模組的支援,除非是 特殊程序 的情況。

變更回呼模組

回呼模組是一種功能模組,而對於程式碼擴充,簡單的程式碼替換就足夠了。

範例

當將函數新增至 ch3 時,如發行處理 中的範例所述,ch_app.appup 如下所示

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.

OTP 也支援變更行為程序的內部狀態;請參閱 變更內部狀態

變更內部狀態

在這種情況下,簡單的程式碼替換是不夠的。在切換到新版本的回呼模組之前,程序必須使用回呼函數 code_change/3 明確轉換其狀態。因此,使用同步程式碼替換。

範例

考慮來自 gen_server 行為ch3 模組。內部狀態是一個代表可用通道的詞彙 Chs。假設您想要新增計數器 N,以追蹤目前為止的 alloc 要求次數。這表示格式必須變更為 {Chs,N}

.appup 檔案可以如下所示

{"2",
 [{"1", [{update, ch3, {advanced, []}}]}],
 [{"1", [{update, ch3, {advanced, []}}]}]
}.

update 指令的第三個元素是一個元組 {advanced,Extra},表示受影響的程序要在載入新版本的模組之前執行狀態轉換。這是由程序呼叫回呼函數 code_change/3 來完成的(請參閱 STDLIB 中的 gen_server)。詞彙 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 是模組清單,Module 相依於這些模組。

範例

當從 "1" 升級到 "2" 或從 "2" 降級到 "1" 時,應用程式 myapp 中的模組 m1 相依於 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}]}]
}.

如果 m1ch3 屬於同一個應用程式,則 .appup 檔案可以如下所示

{"2",
 [{"1",
   [{load_module, ch3},
    {load_module, m1, [ch3]}]}],
 [{"1",
   [{load_module, ch3},
    {load_module, m1, [ch3]}]}]
}.

降級時,m1 也相依於 ch3systools 知道升級和降級之間的差異,並產生正確的 relup,其中在升級時,ch3 會在 m1 之前載入,但在降級時,m1 會在 ch3 之前載入。

變更特殊程序的程式碼

在這種情況下,簡單的程式碼替換是不夠的。當載入特殊程序的駐留模組新版本時,程序必須對其迴圈函數進行完全合格的呼叫,才能切換到新程式碼。因此,必須使用同步程式碼替換。

注意

使用者定義的駐留模組名稱必須列在特殊程序的子規格的 Modules 部分中。否則,發行處理常式找不到程序。

範例

考慮 sys 和 proc_lib 中的範例 ch4。當由監督程式啟動時,子規格可以如下所示

{ch4, {ch4, start_link, []},
 permanent, brutal_kill, worker, [ch4]}

如果 ch4 是應用程式 sp_app 的一部分,且當從此應用程式的版本 "1" 升級到版本 "2" 時要載入新版本的模組,則 sp_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}.

在此例中,除了第一個以外的所有引數都會被忽略,且函數只會再次傳回內部狀態。如果程式碼僅擴充,則這就足夠了。如果內部狀態反而是變更的(類似於 變更內部狀態中的範例),則在此函數中完成,並傳回 {ok,Chs2}

變更監督程式

監督程式行為支援變更內部狀態,也就是變更重新啟動策略和最大重新啟動頻率屬性,以及變更現有的子規格。

可以新增或刪除子程序,但不會自動處理。必須在 .appup 檔案中提供指令。

變更屬性

由於監督程式要變更其內部狀態,因此需要同步程式碼替換。但是,必須使用特殊的 update 指令。

首先,必須載入新版本的回呼模組,無論是升級還是降級。然後可以檢查 init/1 的新傳回值,並相應地變更內部狀態。

下列 upgrade 指令用於監督程式

{update, Module, supervisor}

範例

若要將 ch_sup 的重新啟動策略(來自 監督程式行為)從 one_for_one 變更為 one_for_all,請變更 ch_sup.erl 中的回呼函數 init/1

-module(ch_sup).
...

init(_Args) ->
    {ok, {#{strategy => one_for_all, ...}, ...}}.

檔案 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 欄位可能會影響發行處理程序本身,因為此欄位用於識別在執行同步程式碼替換時受影響的程序。

新增與刪除子程序

如先前所述,變更子程序規格不會影響現有的子程序。新的子程序規格會自動新增,但不會自動刪除。子程序不會自動啟動或終止,必須使用 apply 指令來完成。

範例

假設在將 ch_app 從 "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_applicationremove_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_app,其監管樹中有一個監督者 prim_sup

在新的發行版本中,要將應用程式 ch_app 包含在 prim_app 中。也就是說,其最上層監督者 ch_sup 將作為 prim_sup 的子程序啟動。

工作流程如下

步驟 1) 編輯 prim_sup 的程式碼

init(...) ->
    {ok, {...supervisor flags...,
          [...,
           {ch_sup, {ch_sup,start_link,[]},
            permanent,infinity,supervisor,[ch_sup]},
           ...]}}.

步驟 2) 編輯 prim_app.app 檔案

{application, prim_app,
 [...,
  {vsn, "2"},
  ...,
  {included_applications, [ch_app]},
  ...
 ]}.

步驟 3) 建立新的 .rel 檔案,包含 ch_app

{release,
 ...,
 [...,
  {prim_app, "2"},
  {ch_app, "1"}]}.

可以透過兩種方式啟動包含的應用程式。這將在接下來的兩節中說明。

應用程式重新啟動

步驟 4a) 啟動包含的應用程式的一種方法是重新啟動整個 prim_app 應用程式。通常會使用 prim_app.appup 檔案中的 restart_application 指令。

但是,如果這樣做並產生 .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]}}]}]
}.

監督者變更

步驟 4b) 啟動包含的應用程式(或在降級的情況下停止它)的另一種方法是,將新增和移除 prim_sup 的子程序的指令,與載入/卸載所有 ch_app 程式碼及其應用程式規格的指令結合。

同樣地,.relup 檔案是手動建立的,可以從頭開始或透過編輯產生的版本來建立。先載入 ch_app 的所有程式碼,並載入應用程式規格,然後再更新 prim_sup。降級時,應先更新 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 程式碼

變更以 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, ...}}.

如果要更新連接埠程式,可以使用 code_change/3 函式來擴充 gen_server 的程式碼,該函式會關閉舊連接埠並開啟新連接埠。(如有必要,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]}]).
...

執行階段系統重新啟動與升級

兩個升級指令會重新啟動執行階段系統

  • restart_new_emulator

    適用於升級 ERTS、Kernel、STDLIB 或 SASL 時。當 systools:make_relup/3,4 產生 relup 檔案時,會自動新增此指令。它會在所有其他升級指令之前執行。如需此指令的詳細資訊,請參閱 發行版本處理指令中的 restart_new_emulator (Low-Level)。

  • restart_emulator

    當所有其他升級指令執行後,需要重新啟動執行階段系統時使用。如需此指令的詳細資訊,請參閱 發行版本處理指令中的 restart_emulator (Low-Level)。

如果需要重新啟動執行階段系統,但不需要任何升級指令,也就是說,如果重新啟動本身足以讓升級的應用程式開始執行新版本,則可以手動建立簡單的 .relup 檔案

{"B",
 [{"A",
   [],
   [restart_emulator]}],
 [{"A",
   [],
   [restart_emulator]}]
}.

在這種情況下,可以使用具有自動封裝和解封發行版本套件、自動路徑更新等的發行版本處理架構,而無需指定 .appup 檔案。