?
http://erlang.org/doc/design_principles/des_princ.html
圖和代碼皆源自以上鏈接中Erlang官方文檔,翻譯時的版本為20.1。
這個設計原則,其實是說用戶在設計系統的時候應遵循的標準和規范。閱讀前我一直以為寫的是作者在設計 Erlang/OTP 框架時的一些原則。
閑話少敘。Let's go!
1.概述
OTP設計原則規定了如何使用進程、模塊和目錄來組織 Erlang 代碼。
1.1 監控樹
Erlang/OTP的一個基本概念就是監控樹。它是基于 workers(工人)和 supervisors(監工、監程)的進程組織模型。
- Workers 是實際執行運算的進程。
- Supervisors 是負責監控 workers 的進程。如果 worker 發生異常,supervisor 可以重啟這個 worker。
- 監控樹就是由 supervisors 和 workers 組成的層次結構,讓我們可以設計和編寫容錯的軟件。
下圖中方塊表示 supervisor,圓圈表示 worker(圖源Erlang官方文檔):
圖1.1:? 監控樹
?
1.2 Behaviours(行為模式)
在監控樹中,很多進程擁有一樣的結構,遵循一樣的行為模式。例如,supervisors 結構上都是一樣的,唯一的不同就是他們監控的子進程不同。而很多 wokers 都是以 server/client、finite-state machines(有限狀態自動機)或是 error logger(錯誤記錄器)之類的事件處理器的行為模式運行。
Behaviour就是把這些通用行為模式形式化。也就是說,把進程的代碼分成通用的部分(behaviour 模塊)和專有的部分(callback module 回調模塊).
Behaviour 是 Erlang/OTP 框架中的一部分。用戶如果要實現一個進程(例如一個 supervisor),只需要實現回調模塊,然后導出預先定義的函數集(回調函數)就行了。
下面的例子表明了怎么把代碼分成通用部分和專有部分。我們把下面的代碼當作是一個簡單的服務器(用普通Erlang編寫),用來記錄 channel 集合。其他進程可以各自通過調用函數 alloc/0 和 fee/1 來分配和釋放 channel。
-module(ch1). -export([start/0]). -export([alloc/0, free/1]). -export([init/0]).start() ->spawn(ch1, init, []).alloc() ->ch1 ! {self(), alloc},receive{ch1, Res} ->Resend.free(Ch) ->ch1 ! {free, Ch},ok.init() ->register(ch1, self()),Chs = channels(),loop(Chs).loop(Chs) ->receive{From, alloc} ->{Ch, Chs2} = alloc(Chs),From ! {ch1, Ch},loop(Chs2);{free, Ch} ->Chs2 = free(Ch, Chs),loop(Chs2)end.
?這個服務器可以重寫成一個通用部分 server.erl :
-module(server). -export([start/1]). -export([call/2, cast/2]). -export([init/1]).start(Mod) ->spawn(server, init, [Mod]).call(Name, Req) ->Name ! {call, self(), Req},receive{Name, Res} ->Resend.cast(Name, Req) ->Name ! {cast, Req},ok.init(Mod) ->register(Mod, self()),State = Mod:init(),loop(Mod, State).loop(Mod, State) ->receive{call, From, Req} ->{Res, State2} = Mod:handle_call(Req, State),From ! {Mod, Res},loop(Mod, State2);{cast, Req} ->State2 = Mod:handle_cast(Req, State),loop(Mod, State2)end.
和一個回調模塊 ch2.erl :
-module(ch2). -export([start/0]). -export([alloc/0, free/1]). -export([init/0, handle_call/2, handle_cast/2]).start() ->server:start(ch2).alloc() ->server:call(ch2, alloc).free(Ch) ->server:cast(ch2, {free, Ch}).init() ->channels().handle_call(alloc, Chs) ->alloc(Chs). % => {Ch,Chs2}handle_cast({free, Ch}, Chs) ->free(Ch, Chs). % => Chs2
注意以下幾點:
- server 的代碼可以重用來構建不同的服務器。
- server 名字(在這個例子中是 ch2)對用戶函數來說是透明的。即,修改名字不會影響函數調用。
- 協議(server 發送和接受到的消息)也是透明的。這是一個好的編碼慣例,修改協議不會影響到調用接口函數的代碼。
- 擴展 server 的功能不需要改變 ch2 或其他回調模塊。
上面的 ch1.erl 和 ch2.erl 中,channels/0, alloc/1 和 free/2 的實現被刻意遺漏,因為與本例無關。完整性起見,下面給出這些函數的一種實現方式。這只是個示例,現實中還必須能夠處理諸如 channel 用完無法分配等情況。
channels() ->{_Allocated = [], _Free = lists:seq(1,100)}.alloc({Allocated, [H|T] = _Free}) ->{H, {[H|Allocated], T}}.free(Ch, {Alloc, Free} = Channels) ->case lists:member(Ch, Alloc) oftrue ->{lists:delete(Ch, Alloc), [Ch|Free]};false ->Channelsend.
沒有使用 behaviour 的代碼可能效率更高,但是通用性差。將系統中的所有 applications 組織成一致的行為模式很重要。
而且使用 behaviour 能讓代碼易讀易懂。簡易的程序結構可能會更有效率,但是比較難理解。
上面的 server 模塊其實就是一個簡化的 Erlang/OTP behaviour - gen_server。
Erlang/OTP的標配 behaviour 有:
- gen_server? 實現 client/server 模式的服務器
- gen_statem? 實現狀態機(譯者補充:舊版本中為 gen_fsm)
- gen_event? 實現事件處理器
- supervisor? 實現監控樹中的監控者
編譯器能識別模塊屬性 -behaviour(Behaviour) ,會對未實現的回調函數發出編譯警告,例如:
-module(chs3). -behaviour(gen_server). ...3> c(chs3). ./chs3.erl:10: Warning: undefined call-back function handle_call/3 {ok,chs3}
?
1.3 Applications
Erlang/OTP 自帶一些組件,每個組件實現了特定的功能。這些組件用 Erlang/OTP 術語叫做 application(應用)。例如 Mnesia 就是一個Erlang/OTP 應用,它包含了所有數據庫服務所需的功能,還有 Debugger,用來 debug Erlang 代碼。基于 Erlang/OTP 的系統,至少必須包含下面兩個 application:
- Kernel - 運行 Erlang 時必須的功能
- STDLIB - Erlang 標準庫
應用的概念適用于程序結構(進程)和目錄結構(模塊)。
最簡單的應用由一組功能模塊組成,不包含任何進程,這種叫 library application(庫應用)。STDLIB 就屬于這類。
有進程的應用可以使用標準 behaviour 很容易地實現一個監控樹。
如何編寫應用詳見后文 Applications。
?
1.4 Releases(發布版本)
一個 release 是一個完整的系統,包含 Erlang/OTP 應用的子集和一系列用戶定義的 application。
詳見后文?Releases。
怎么在目標環境中部署 release 在系統原則的文檔中有講到。
?
1.5 Release Handling(管理發布)
管理 release 即在一個 release 的不同版本之間升級或降級,怎么在一個運行中的系統操作這些,詳見后文 Release Handling。
?
2?gen_server Behaviour
這部分可與 stdblib 中的 gen_server(3) 教程(包含了 gen_server 所有接口函數和回調函數)一起閱讀。
2.1 Client-Server 原則
?C/S模型就是一個服務器對應任意多個客戶端。C/S模型是用來進行資源管理,多個客戶端想分享一個公共資源。而服務器則用來管理這個資源。

圖 2.1: ? Client-Server Model
?
2.2 例子
前文有用普通 erlang 寫的簡單的服務器的例子。使用 gen_server 重寫,結果如下:
-module(ch3). -behaviour(gen_server).-export([start_link/0]). -export([alloc/0, free/1]). -export([init/1, handle_call/3, handle_cast/2]).start_link() ->gen_server:start_link({local, ch3}, ch3, [], []).alloc() ->gen_server:call(ch3, alloc).free(Ch) ->gen_server:cast(ch3, {free, Ch}).init(_Args) ->{ok, channels()}.handle_call(alloc, _From, Chs) ->{Ch, Chs2} = alloc(Chs),{reply, Ch, Chs2}.handle_cast({free, Ch}, Chs) ->Chs2 = free(Ch, Chs),{noreply, Chs2}.
下一小節將解釋這段代碼。
?
2.3 啟動一個 gen_server
在上一小節的示例中,gen_server 通過調用 ch3:start_link() 啟動:
start_link() ->gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}
start_link 調用了函數 gen_server:start_link/4 ,這個函數產生并連接了一個新進程(一個 gen_server)。
- 第一個參數,{local, ch3},指定了進程名,gen_server 會在本地注冊為 ch3。
????????? 如果名字被省略,gen_server 不會被注冊,此時一定要用它的 pid。名字還可以用 {global, Name},這樣的話 gen_server 會調用 global:register_name/2 來注冊。
- 第二個參數,ch3,是回調模塊的名字,即回調函數所在的模塊名。
????????? 接口函數 (start_link, alloc 和 free) 和回調函數 (init, handle_call 和 handle_cast) 放在同一個模塊中。這是一個好的編程慣例,把與一個進程相關的代碼放在同一個模塊中。
- 第三個參數,[],是用來傳遞給回調函數 init 的參數。此例中 init 不需要輸入,所以忽視了這個參數。
- 第四個參數,[],是一個選項list。查看 gen_server(3) 可獲悉可用選項。
如果名字注冊成功,這個新的 gen_server 進程會調用回調函數 ch3:init([]) 。init 函數應該返回 {ok, State},其中 State 是 gen_server 的內部狀態,在此例中,內部狀態指的是 channel 集合。
init(_Args) ->{ok, channels()}.
gen_server:start_link 是同步調用,在 gen_server 初始化成功可接收請求之前它不會返回。
如果 gen_server 是一個監控樹的一部分,supervisor 啟動 gen_server 時一定要使用 gen_server:start_link。還有一個函數是 gen_server:start ,這個函數會啟動一個獨立的 gen_server,也就是說它不會成為監控樹的一部分。
?
2.4 同步消息請求 - Call
同步的請求 alloc() 是用 gen_server:call/2 來實現的:
alloc() ->gen_server:call(ch3, alloc).
ch3 是 gen_server 的名字,要與進程名字相符合才能使用。alloc 是實際的請求。
這個請求會被轉化成一個消息,發送給 gen_server。收到消息后,gen_server 調用 handle_call(Request, From, State) 來處理消息,正常會返回 {reply, Reply, State1}。Reply 是會發回給客戶端的回復內容,State1 是 gen_server 新的內部狀態。
handle_call(alloc, _From, Chs) ->{Ch, Chs2} = alloc(Chs),{reply, Ch, Chs2}.
此例中,回復內容就是分配給它的 channel Ch,而新的內部狀態是剩余的 channel 集合 Chs2。
就這樣,ch3:alloc() 返回了分配給它的 channel Ch,gen_server 則保存剩余的 channel 集合,繼續等待新的請求。
?
2.5 異步消息請求 - Cast
異步的請求 free(Ch) 是用 gen_server:cast/2 來實現的:
free(Ch) ->gen_server:cast(ch3, {free, Ch}).
ch3 是 gen_server 的名字,{free, Ch} 是實際的請求。
這個請求會被轉化成一個消息,發送給 gen_server。發送后直接返回 ok。
收到消息后,gen_server 調用?handle_cast(Request, State) 來處理消息,正常會返回 {noreply,State1}。State1 是 gen_server 新的內部狀態。
handle_cast({free, Ch}, Chs) ->Chs2 = free(Ch, Chs),{noreply, Chs2}.
此例中,新的內部狀態是新的剩余的 channel集合 Chs2。然后 gen_server 繼續等待新的請求。
?
2.6 終止
在監控樹中
如果 gen_server 是監控樹的一部分,則不需要終止函數。gen_server 會自動被它的監控者終止,具體怎么終止通過 終止策略 來決定。
如果要在終止前進行一些操作,終止策略必須有一個 time-out 值,且 gen_server 必須在 init 函數中被設置為捕捉 exit 信號。當被要求終止時,gen_server 會調用回調函數 terminate(shutdown, State):
init(Args) ->...,process_flag(trap_exit, true),...,{ok, State}....terminate(shutdown, State) ->..code for cleaning up here..ok.
獨立的 gen_server
如果 gen_server 不是監控樹的一部分,可以寫一個 stop 函數,例如:
... export([stop/0]). ...stop() ->gen_server:cast(ch3, stop). ...handle_cast(stop, State) ->{stop, normal, State}; handle_cast({free, Ch}, State) ->.......terminate(normal, State) ->ok.
處理 stop 消息的回調函數返回 {stop, normal, State1},normal 意味著這是一次自然死亡,而 State1 是一個新的 gen_server 內部狀態。這會導致 gen_server 調用 terminate(normal, State1) 然后優雅地……掛掉。
?
2.7 處理其他消息
如果 gen_server 會在除了請求之外接收其他消息,需要實現回調函數 handle_info(Info, State) 來進行處理。其他消息可能是 exit 消息,如果 gen_server 與其他進程連接起來(不是 supervisor),并且被設置為捕捉 exit 信號。
handle_info({'EXIT', Pid, Reason}, State) ->..code to handle exits here..{noreply, State1}.
一定要實現 code_change 函數。(譯者補充:在代碼熱更新時會用到)
code_change(OldVsn, State, Extra) ->..code to convert state (and more) during code change{ok, NewState}.
?
3?gen_statem Behavior
此章可結合 gen_statem(3) (包含全部接口函數和回調函數的詳述)教程一起看。
注意:這是 Erlang/OTP 19.0 引入的新 behavior。它已經經過了完整的 review,穩定使用在至少兩個大型 OTP 應用中并被保留下來。基于用戶反饋,我們覺得有必要在 Erlang/OTP 20.0 對它進行小調整(不向后兼容)。
3.1 事件驅動的狀態機
現在的自動機理論沒有具體描述狀態變遷是如何觸發的,而是假定輸出是一個以輸入和當前狀態為參數的函數,它們是某種類型的值。
對一個事件驅動的狀態機來說,輸入就是一個觸發狀態變遷的事件,輸出是狀態遷移過程中執行的動作。用類似有限狀態自動機的數學模型來描述,它是一系列如下形式的關系:
State(S) x Event(E) -> Actions(A), State(S')
這些關系可以這么理解:如果我們現在處于 S 狀態,事件 E 發生了,我們就要執行動作 A 并且轉移狀態為 S' 。注意: S’ 可能與 S 相同。
由于 A 和 S' 只取決于 S 和 E,這種狀態機被稱為 Mealy 機(可參見維基百科的描述)。
跟大多數 gen_ 開頭的 behavior 一樣, gen_statem 保存了 server 的數據和狀態。而且狀態數是沒有限制的(假設虛擬機內存足夠),輸入事件類型數也是沒有限制的,因此用這個 behavior 實現的狀態機實際上是圖靈完備的。不過感覺上它更像一個事件驅動的 Mealy 機。
?
3.2 回調模式
gen_statem 支持兩種回調模式:
- state_functions 方式,狀態遷移規則以 erlang 函數的形式編寫,寫法如下:
StateName(EventType, EventContent, Data) ->... code for actions here ...{next_state, NewStateName, NewData}.
???????? 在示例部分用的最多的就是這種格式。
- handle_event_function 方式,只用一個 erlang 函數來裝載所有的狀態遷移規則:
handle_event(EventType, EventContent, State, Data) ->... code for actions here ...{next_state, NewState, NewData}
??????? 示例可見單個事件處理器這一小節。
這兩種函數都支持其他的返回值,具體可見 gen_statem 的教程頁面的?Module:StateName/3。其他的返回元組可以停止狀態機、在狀態機引擎中執行轉移動作、發送回復等等。
選擇何種回調方式
這兩種回調方式有不同的功能和限制,但是目標都一樣:要處理所有可能的事件和狀態的組合。
你可以同時只關心一種狀態,確保每個狀態都處理了所有事件。或者只關心一個事件,確保它在所有狀態下都被處理。你也可以結合兩種策略。
state_functions 方式中,狀態只能用 atom 表示,gen_statem 引擎通過狀態名來分發處理。它提倡回調模塊把一個狀態下的所有事件和動作放在代碼的同一個地方,以此同時只關注一個狀態。
當你的狀態圖確定時,這種模式非常好。就像本小節舉的例子,狀態對應的事件和動作都放在一起,每個狀態有自己獨一無二的名字。
而通過 handle_event_function 方式,可以結合兩種策略,因為所有的事件和狀態都在同一個回調函數中。
無論是想以狀態還是事件為中心,這種方式都能滿足。不過沒有分發到輔助函數的話,Module:handle_event/4 會迅速增長到無法管理。
?
3.3 狀態enter回調
不論回調模式是哪種,gen_statem 都會在狀態改變的時候(譯者補充:進入狀態的時候調用)自動調用回調函數(call the state callback),所以你可以在狀態的轉移規則附近寫狀態入口回調。通常長這樣:
StateName(enter, _OldState, Data) ->... code for state entry actions here ...{keep_state, NewData}; StateName(EventType, EventContent, Data) ->... code for actions here ...{next_state, NewStateName, NewData}.
這可能會在特定情況下很有幫助,不過它要求你在所有狀態中都處理入口回調。詳見 State Entry Actions。
?
3.4 動作(Actions)
在第一小節事件驅動的狀態機中,動作(action)作為通用狀態機模型的一部分被提及。一般的動作會在 gen_statem 處理事件的回調中執行(返回到 gen_statem 引擎之前)。
還有一些特殊的狀態遷移動作,在回調函數返回后指定 gen_statem 引擎去執行。回調函數可以在返回的元組中指定一個動作列表。這些動作影響 gen_statem 引擎本身,可以做下列事情:
- 延緩(postpone)當前事件, 詳見延緩事件
- 掛起(hibernate)狀態機,詳見掛起
- 狀態超時(state time-out),詳見狀態超時
- 一般超時(generic time-out),詳見一般超時
- 事件超時(event time-out),詳見事件超時
- 回復調用者,詳見全狀態事件
- 生成下一個要處理的事件,詳見自生成事件
詳見 gen_statem(3) 。你可以回復很多調用者、生成多個后續事件、設置相對時間或絕對時間的超時等等。
?
3.5 事件類型
事件分成不同的類型(event types)。同狀態下的不同類型的事件都在同一個回調函數中處理,回調函數以 EventType 和 EventContent 作為參數。
下面列出事件類型和來源的完整列表:
cast
?????? 由 gen_statem:cast 生成。
{call, From}
?????? 由 gen_statem:call 生成,狀態遷移動作返回 {reply, From, Msg} 或調用 gen_statem:reply 時,會用到 From 作為回復地址。
info
????? 發送給 gen_statem 進程的常規進程消息。
state_timeout
??? ? 狀態遷移動作 {state_timeout,Time,EventContent} 生成。
- {timeout,Name}
??? ? 狀態遷移動作 {{timeout,Name},Time,EventContent} 生成。
timeout
??? ? 狀態遷移動作 {timeout,Time,EventContent}(或簡寫為 Time)生成。
internal
??? ? 狀態遷移動作 {next_event,internal,EventContent} 生成。
上述所有事件類型都可以用 {next_event,EventType,EventContent} 來生成。
?????
3.6 示例
密碼鎖的門可以用一個自動機來表述。初始狀態,門是鎖住的。當有人按一個按鈕,即觸發一個事件。結合此前按下的按鈕,結果可能是正確、不完整或者錯誤。如果正確,門鎖會開啟10秒鐘(10,000毫秒)。如果不完整,則等待下一個按鈕被按下。如果錯了,一切從頭再來,等待新一輪按鈕。
圖3.1: 密碼鎖狀態圖
密碼鎖狀態機用 gen_statem 實現,回調模塊如下:
-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock).-export([start_link/1]). -export([button/1]). -export([init/1,callback_mode/0,terminate/3,code_change/4]). -export([locked/3,open/3]).start_link(Code) ->gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).button(Digit) ->gen_statem:cast(?NAME, {button,Digit}).init(Code) ->do_lock(),Data = #{code => Code, remaining => Code},{ok, locked, Data}.callback_mode() ->state_functions.locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] ->do_unlock(),{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};[Digit|Rest] -> % Incomplete{next_state, locked, Data#{remaining := Rest}};_Wrong ->{next_state, locked, Data#{remaining := Code}}end.open(state_timeout, lock, Data) ->do_lock(),{next_state, locked, Data}; open(cast, {button,_}, Data) ->{next_state, open, Data}.do_lock() ->io:format("Lock~n", []). do_unlock() ->io:format("Unlock~n", []).terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok. code_change(_Vsn, State, Data, _Extra) ->{ok, State, Data}.
下一小節解釋代碼。
?
3.7 啟動狀態機
前例中,可調用 code_lock:start_link(Code) 來啟動 gen_statem:
start_link(Code) ->gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
start_link 函數調用 gen_statem:start_link/4,生成并連接了一個新進程(gen_statem)。
- 第一個參數,{local, ?NAME} 指定了名字。在此例中 gen_statem 在本地注冊為 code_lock(?NAME)。如果名字被省略,gen_statem 不會被注冊,此時必須用它的 pid。名字還可以用{global, Name},這樣的話 gen_server 會調用 global:register_name/2 來注冊。
- 第二個參數,?MODULE,這個參數就是回調模塊的名字,此例的回調模塊就是當前模塊。接口函數(start_link/1 和 button/1)與回調函數(init/1, locked/3, 和 open/3)放在同一個模塊中。這是一個好的編程慣例,把 client 和 server 的代碼放在同一個模塊中。
- 第三個參數,Code,是一串數字,存儲了正確的門鎖密碼,將被傳遞給 init/1 函數。
- 第四個參數,[],是一個選項list。查看 gen_statem:start_link/3 可獲悉可用選項。
如果名字注冊成功,這個新的 gen_statem 進程會調用 init 回調 code_lock:init(Code)。init 函數應該返回 {ok, State, Data},其中 State 是初始狀態(此例中是鎖住狀態,假設門一開始是鎖住的)。Data 是 gen_statem 的內部數據。此例中 Data 是一個map,其中 code 對應的是正確的密碼,remaining 對應的是按鈕按對后剩余的密碼(初始與 code 一致)。
init(Code) ->do_lock(),Data = #{code => Code, remaining => Code},{ok,locked,Data}.
gen_statem:start_link 是同步調用,在 gen_statem 初始化成功可接收請求之前它不會返回。
如果 gen_statem 是一個監控樹的一部分,supervisor 啟動 gen_statem 時一定要使用 gen_statem:start_link。還有一個函數是 gen_statem:start ,這個函數會啟動一個獨立的 gen_statem,也就是說它不會成為監控樹的一部分。
callback_mode() ->state_functions.
函數 Module:callback_mode/0 規定了回調模塊的回調模式,此例中是 state_functions 模式,每個狀態有自己的處理函數。
?
3.8 事件處理
通知 code_lock 按鈕事件的函數是用?gen_statem:cast/2 實現的:
button(Digit) ->gen_statem:cast(?NAME, {button,Digit}).
第一個參數是 gen_statem 的名字,要與進程名字相同,所以我們用了同樣的宏 ?NAME。{button, Digit} 是事件的內容。
這個事件會被轉化成一個消息,發送給 gen_statem。當收到事件時, gen_statem 調用 StateName(cast, Event, Data),一般會返回一個元組 {next_state, NewStateName, NewData}。StateName 是當前狀態名,NewStateName是下一個狀態。NewData 是 gen_statem 的新的內部數據,Actions 是 gen_statem 引擎要執行的動作列表。
locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] -> % Completedo_unlock(),{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};[Digit|Rest] -> % Incomplete{next_state, locked, Data#{remaining := Rest}};[_|_] -> % Wrong{next_state, locked, Data#{remaining := Code}}end.open(state_timeout, lock, Data) ->do_lock(),{next_state, locked, Data}; open(cast, {button,_}, Data) ->{next_state, open, Data}.
如果門是鎖著的,按鈕被按下,比較輸入按鈕和正確的按鈕。根據比較的結果,如果鎖開了,gen_statem 變為 open 狀態,否則繼續保持 locked 狀態。
如果按鈕是錯的,數據又變為初始的密碼列表。
狀態為 open 時,按鈕事件會被忽略,狀態維持不變。還可以返回 {keep_state, Data} 表示狀態不變或者返回 keep_state_and_data 表示狀態和數據都不變。
?
3.9 狀態超時
當給出正確的密碼,門鎖開啟,locked/2 返回如下元組:
{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};
10,000 是以毫秒為單位的超時時長。10秒后,會觸發一個超時,然后 StateName(state_timeout, lock, Data) 被調用,此后門重新鎖住:
open(state_timeout, lock, Data) ->do_lock(),{next_state, locked, Data};
狀態超時會在狀態改變的時候自動取消。重新設置一個狀態超時相當于重啟,舊的定時器被取消,新的定時器被啟動。也就是說可以通過重啟一個時間為 infinite 的超時來取消狀態超時。
?
3.10 全狀態事件
有些事件可能在任何狀態下到達 gen_statem。可以在一個公共的函數處理這些事件,所有的狀態函數都調用它來處理通用的事件。
假定一個 code_length/0 函數返回正確密碼的長度(不敏感的信息)。我們把所有與狀態無關的事件分發到公共函數 handle_event/3:
... -export([button/1,code_length/0]). ...code_length() ->gen_statem:call(?NAME, code_length).... locked(...) -> ... ; locked(EventType, EventContent, Data) ->handle_event(EventType, EventContent, Data).... open(...) -> ... ; open(EventType, EventContent, Data) ->handle_event(EventType, EventContent, Data).handle_event({call,From}, code_length, #{code := Code} = Data) ->{keep_state, Data, [{reply,From,length(Code)}]}.
此例使用 gen_statem:call/2,調用者會等待 server 的回復。{reply,From,Reply} 元組表示回復,{keep_state, ...} 用來保持狀態不變。這個返回格式在你想保持狀態不變(不管狀態是什么)的時候非常方便。
?
3.11 單個事件處理器
如果使用 handle_event_function 模式,所有的事件都會在 Module:handle_event/4 被處理,我們可以(也可以不)在第一層以事件為中心進行分組,然后再判斷狀態:
... -export([handle_event/4]).... callback_mode() ->handle_event_function.handle_event(cast, {button,Digit}, State, #{code := Code} = Data) ->case State oflocked ->case maps:get(remaining, Data) of[Digit] -> % Completedo_unlock(),{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};[Digit|Rest] -> % Incomplete{keep_state, Data#{remaining := Rest}};[_|_] -> % Wrong{keep_state, Data#{remaining := Code}}end;open ->keep_state_and_dataend; handle_event(state_timeout, lock, open, Data) ->do_lock(),{next_state, locked, Data}....
?
3.12 終止
在監控樹中
如果 gen_statem 是監控樹的一部分,則不需要終止函數。gen_statem 自動的被它的監控者終止,具體怎么終止通過 終止策略 來決定。
如果需要在終止前進行一些操作,那么終止策略必須有一個 time-out 值,且 gen_statem 必須在 init 函數中被設置為捕捉 exit 信號,調用 process_flag(trap_exit, true):
init(Args) ->process_flag(trap_exit, true),do_lock(),...
當被要求終止時,gen_statem 會調用回調函數 terminate(shutdown, State, Data):
terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok.
獨立的 gen_statem
如果 gen_statem 不是監控樹的一部分,可以寫一個 stop 函數(使用 gen_statem:stop)。建議增加一個 API :
...
-export([start_link/1,stop/0])....
stop() -> gen_statem:stop(?NAME).
這會導致 gen_statem 調用 terminate/3(像監控樹中的服務器被終止一樣),等待進程終止。
?
3.13 事件超時
事件超時功能繼承自 gen_statem 的前輩 gen_fsm ,事件超時的定時器在有事件達到的時候就會被取消。你可以接收到一個事件或者一個超時,但不會兩個都收到。
事件超時由狀態遷移動作 {timeout,Time,EventContent} 指定,或者僅僅是 Time, 或者僅僅一個 Timer 而不是動作列表(繼承自 gen_fsm)。
不活躍情況下想做點什么時,可以用此類超時。如果30秒內沒人按鈕,重置密碼列表:
...locked(timeout, _, #{code := Code, remaining := Remaining} = Data) ->{next_state, locked, Data#{remaining := Code}}; locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) -> ...[Digit|Rest] -> % Incomplete{next_state, locked, Data#{remaining := Rest}, 30000}; ...
接收到任意按鈕事件時,啟動一個30秒超時,如果接收到超時事件就重置密碼列表。
接收到其他事件時,事件超時會被取消,所以要么接收到其他事件要么接受到超時事件。所以不能也不必要重啟一個事件超時。因為你處理的任何事件都會取消事件超時。
?
3.14 一般超時
前面說的狀態超時只在狀態不改變時有效。而事件超時只在不被其他事件打斷的時候生效。
你可能想要在某個狀態下開啟一個定時器,而在另一個狀態下做處理,想要不改變狀態就取消一個定時器,或者希望同時存在多個定時器。這些都可以用過 generic time-outs 一般超時來實現。它們看起來有點像事件超時,但是它們有名字,不同名字的可以同時存在多個,并且不會被自動取消。
下面是用一般超時實現來替代狀態超時的例子,定時器名字是 open_tm :
... locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] ->do_unlock(),{next_state, open, Data#{remaining := Code},[{{timeout,open_tm},10000,lock}]}; ...open({timeout,open_tm}, lock, Data) ->do_lock(),{next_state,locked,Data}; open(cast, {button,_}, Data) ->{keep_state,Data}; ...
和狀態超時一樣,可以通過給特定的名字設置新的定時器或設置為infinite來取消定時器。
也可以不取消失效的定時器,而是在它到來的時候忽略它(確定已無用時)。
?
3.15 Erlang 定時器
最全面的處理超時的方式就是使用 erlang 的定時器,詳見 erlang:start_timer3,4。大部分的超時任務可以通過 gen_statem 的超時功能來完成,但有時候你可能想獲取 erlang:cancel_timer(Tref) 的返回值(剩余時間)。
下面是用 erlang 定時器替代前文狀態超時的實現:
... locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] ->do_unlock(),Tref = erlang:start_timer(10000, self(), lock),{next_state, open, Data#{remaining := Code, timer => Tref}}; ...open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->do_lock(),{next_state,locked,maps:remove(timer, Data)}; open(cast, {button,_}, Data) ->{keep_state,Data}; ...
?當狀態遷移到 locked 時,我們可以不從 Data 中清除 timer 的值,因為每次進入 open 狀態都是一個新的 timer 值。不過最好不要在 Data 中保留過期的值。
當其他事件觸發,你想清除一個 timer 時,可以使用 erlang:cancel_timer(Tref) 。如果沒有延緩(下一小節會講到),超時消息被 cancel 后就不會再被收到,所以要確認是否一不小心延緩了這類消息。要注意的是,超時消息可能在你 cancel 它之前就到達,所以要根據 erlang:cancel_timer(Tref) 的返回值,把這消息從進程郵箱里讀出來。
另一種處理方式是,不要 cancel 掉一個 timer,而是在它到達之后忽略它。
?
3.16 延緩事件
如果你想在當前狀態忽略某個事件,在后續的某個狀態中再處理,你可以延緩這個事件。延緩的事件會在狀態變化后重新觸發,即:OldState =/= NewState 。
延緩是通過狀態遷移動作 postpone 來指定的。
此例中,我們可以延緩在 open 狀態下的按鈕事件(而不是忽略它),這些事件會進入等待隊列,等到 locked 狀態時再處理:
... open(cast, {button,_}, Data) ->{keep_state,Data,[postpone]}; ...
延緩的事件只會在狀態改變時重新觸發,因此要考慮怎么保存內部數據。內部數據可以在數據 Data 或者狀態 State 中保存,比如用兩個幾乎一樣的狀態來表示布爾值,或者使用一個復合狀態(回調模塊的 handle_event_function)。如果某個值的變化會改變事件處理,那需要把這個值保存在狀態 State 里。因為 Data 的變化不會觸發延緩的事件。
如果你沒有用延緩的話,這個不重要。但是如果你決定使用延緩功能,沒有用不同的狀態做區分,可能會產生很難發現的 bug。
模糊的狀態圖
狀態圖很可能沒有給特定的狀態指定事件處理方式。可能在相關的上下文中有提及。
可能模糊的動作(譯者補充:在狀態圖中沒有給出處理方式,可能對應的動作):忽略(丟棄或者僅僅 log)事件、延緩事件至其他狀態處理。
選擇性 receive
Erlang 的選擇性 receive 語句經常被用來寫簡單的狀態機(不用 gen_statem 的普通 erlang 代碼)。下面是可能的實現方式之一:
-module(code_lock). -define(NAME, code_lock_1). -export([start_link/1,button/1]).start_link(Code) ->spawn(fun () ->true = register(?NAME, self()),do_lock(),locked(Code, Code)end).button(Digit) ->?NAME ! {button,Digit}.locked(Code, [Digit|Remaining]) ->receive{button,Digit} when Remaining =:= [] ->do_unlock(),open(Code);{button,Digit} ->locked(Code, Remaining);{button,_} ->locked(Code, Code)end.open(Code) ->receiveafter 10000 ->do_lock(),locked(Code, Code)end.do_lock() ->io:format("Locked~n", []). do_unlock() ->io:format("Open~n", []).
此例中選擇性 receive 隱含了把 open 狀態接收到的所有事件延緩到 locked 狀態的邏輯。
選擇性 receive 語句不能用在 gen_statem 或者任何 gen_* 中,因為 receive 語句已經在 gen_* 引擎中包含了。為了兼容?sys ,behavior 進程必須對系統消息作出反應,并把非系統的消息傳遞給回調模塊,因此把 receive 集成在引擎層的 loop 里。
動作 postpone(延緩)是被設計來模擬選擇性 receive 的。選擇性 receive 隱式地延緩所有不被接受的事件,而 postpone 動作則是顯示地延緩一個收到的事件。
兩種機制邏輯復雜度和時間復雜度是一樣的,而選擇性 receive 語法的常因子更少。
?
3.17 entry動作
假設你有一張狀態圖,圖中使用了狀態 entry 動作。只有一兩個狀態有 entry 動作時你可以用自生成事件(詳見下一部分),但是使用內置的狀態enter回調是更好的選擇。
在 callback_mode/0 函數的返回列表中加入 state_enter,會在每次狀態改變的時候傳入參數 (enter, OldState, ...) 調用一次回調函數。你只需像事件一樣處理這些請求即可:
... init(Code) ->process_flag(trap_exit, true),Data = #{code => Code},{ok, locked, Data}.callback_mode() ->[state_functions,state_enter].locked(enter, _OldState, Data) ->do_lock(),{keep_state,Data#{remaining => Code}}; locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] ->{next_state, open, Data}; ...open(enter, _OldState, _Data) ->do_unlock(),{keep_state_and_data, [{state_timeout,10000,lock}]}; open(state_timeout, lock, Data) ->{next_state, locked, Data}; ...
你可以返回 {repeat_state, ...} 、{repeat_state_and_data,_} 或 repeat_state_and_data 來重復執行 entry 代碼,這些詞其他含義跟 keep_state 家族一樣(保持狀態、數據不變等等)。詳見 state_callback_result() 。
?
3.18 自生成事件
有時候可能需要在狀態機中生成事件,可以用狀態遷移動作 {next_event,EventType,EventContent} 來實現。
你可以生成所有類型(type)的事件。其中 internal 類型只能通過 next_event 來生成,不會由外部產生,你可以確定一個 internal 事件是來自狀態機自身。
你可以用自生成事件來預處理輸入數據,例如解碼、用換行分隔數據。有強迫癥的人可能會說,應該分出另一個狀態機來發送預處理好的數據給主狀態機。為了降低消耗,這個預處理狀態機可以通過一般的狀態事件處理來實現。
下面的例子為一個輸入模型,通過 put_chars(Chars) 輸入,enter() 來結束輸入:
... -export(put_chars/1, enter/0). ... put_chars(Chars) when is_binary(Chars) ->gen_statem:call(?NAME, {chars,Chars}).enter() ->gen_statem:call(?NAME, enter)....locked(enter, _OldState, Data) ->do_lock(),{keep_state,Data#{remaining => Code, buf => []}}; ...handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) ->{keep_state, Data#{buf := [Chars|Buf],[{reply,From,ok}]}; handle_event({call,From}, enter, #{buf := Buf} = Data) ->Chars = unicode:characters_to_binary(lists:reverse(Buf)),try binary_to_integer(Chars) ofDigit ->{keep_state, Data#{buf := []},[{reply,From,ok},{next_event,internal,{button,Chars}}]}catcherror:badarg ->{keep_state, Data#{buf := []},[{reply,From,{error,not_an_integer}}]}end; ...
用 code_lock:start([17]) 啟動程序,然后就能通過 code_lock:put_chars(<<"001">>), code_lock:put_chars(<<"7">>), code_lock:enter() 這一系列動作開鎖了。
?
3.19 重寫例子
這一小節包含了之前提到的大部分修改,用到了狀態 enter 回調,用一個新的狀態圖來表述:
圖 3.2:重寫密碼鎖狀態圖
注意,圖中沒有說明 open 狀態如何處理按鈕事件。需要從其他地方找,因為沒標明的事件不是被去掉了,而是在其他狀態中進行處理了。圖中也沒有說明 code_length/0 需要在所有狀態中處理。
回調模式:state_functions
使用 state functions:
-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_2).-export([start_link/1,stop/0]). -export([button/1,code_length/0]). -export([init/1,callback_mode/0,terminate/3,code_change/4]). -export([locked/3,open/3]).start_link(Code) ->gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). stop() ->gen_statem:stop(?NAME).button(Digit) ->gen_statem:cast(?NAME, {button,Digit}). code_length() ->gen_statem:call(?NAME, code_length).init(Code) ->process_flag(trap_exit, true),Data = #{code => Code},{ok, locked, Data}.callback_mode() ->[state_functions,state_enter].locked(enter, _OldState, #{code := Code} = Data) ->do_lock(),{keep_state, Data#{remaining => Code}}; locked(timeout, _, #{code := Code, remaining := Remaining} = Data) ->{keep_state, Data#{remaining := Code}}; locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] -> % Complete{next_state, open, Data};[Digit|Rest] -> % Incomplete{keep_state, Data#{remaining := Rest}, 30000};[_|_] -> % Wrong{keep_state, Data#{remaining := Code}}end; locked(EventType, EventContent, Data) ->handle_event(EventType, EventContent, Data).open(enter, _OldState, _Data) ->do_unlock(),{keep_state_and_data, [{state_timeout,10000,lock}]}; open(state_timeout, lock, Data) ->{next_state, locked, Data}; open(cast, {button,_}, _) ->{keep_state_and_data, [postpone]}; open(EventType, EventContent, Data) ->handle_event(EventType, EventContent, Data).handle_event({call,From}, code_length, #{code := Code}) ->{keep_state_and_data, [{reply,From,length(Code)}]}.do_lock() ->io:format("Locked~n", []). do_unlock() ->io:format("Open~n", []).terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok. code_change(_Vsn, State, Data, _Extra) ->{ok,State,Data}.
回調模式:handle_event_function
這部分描述了如何使用一個 handle_event/4? 函數來替換上面的例子。前文提到的在第一層以事件作區分的方式在此例中不太合適,因為有狀態 enter 調用,所以用第一層以狀態作區分的方式:
... -export([handle_event/4]).... callback_mode() ->[handle_event_function,state_enter].%% State: locked handle_event(enter, _OldState, locked,#{code := Code} = Data) ->do_lock(),{keep_state, Data#{remaining => Code}}; handle_event(timeout, _, locked,#{code := Code, remaining := Remaining} = Data) ->{keep_state, Data#{remaining := Code}}; handle_event(cast, {button,Digit}, locked,#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] -> % Complete{next_state, open, Data};[Digit|Rest] -> % Incomplete{keep_state, Data#{remaining := Rest}, 30000};[_|_] -> % Wrong{keep_state, Data#{remaining := Code}}end; %% %% State: open handle_event(enter, _OldState, open, _Data) ->do_unlock(),{keep_state_and_data, [{state_timeout,10000,lock}]}; handle_event(state_timeout, lock, open, Data) ->{next_state, locked, Data}; handle_event(cast, {button,_}, open, _) ->{keep_state_and_data,[postpone]}; %% %% Any state handle_event({call,From}, code_length, _State, #{code := Code}) ->{keep_state_and_data, [{reply,From,length(Code)}]}....
真正的密碼鎖中把按鈕事件從 locked 狀態延遲到 open 狀態感覺會很奇怪,它只是用來舉例說明事件延緩。
?
3.20 過濾狀態
目前實現的服務器,會在終止時的錯誤日志中輸出所有的內部狀態。包含了門鎖密碼和剩下需要按的按鈕。
這個信息屬于敏感信息,你可能不想因為一些不可預料的事情在錯誤日志中輸出這些。
還有可能內部狀態數據太多,在錯誤日志中包含了太多沒用的數據,所以需要進行篩選。
你可以通過實現函數?Module:format_status/2 來格式化錯誤日志中通過?sys:get_status/1,2 獲得的內部狀態,例如:
... -export([init/1,terminate/3,code_change/4,format_status/2]). ...format_status(Opt, [_PDict,State,Data]) ->StateData ={State,maps:filter(fun (code, _) -> false;(remaining, _) -> false;(_, _) -> trueend,Data)},case Opt ofterminate ->StateData;normal ->[{data,[{"State",StateData}]}]end.
實現 Module:format_status/2 并不是強制的。如果不實現,默認的實現方式就類似上面這個例子,除了默認不會篩選 Data(即 StateData = {State,Data}),例子中因為有敏感信息必須進行篩選。
?
3.21 復合狀態
回調模式?handle_event_function 支持使用非 atom 的狀態(詳見回調模式),比如一個復合狀態可能是一個 tuple。
你可能想在狀態變化的時候取消狀態超時,或者和延緩事件配合使用控制事件處理,這時候就要用到復合狀態。我們引入可配置的鎖門按鈕來完善前面的例子(這就是此問題中的狀態),這個按鈕可以在 open 狀態立馬鎖門,且可以通過 set_lock_button/1 這個接口來設置鎖門按鈕。
假設我們在開門的狀態調用 set_lock_button,并且此前已經延緩了一個按鈕事件(不是舊的鎖門按鈕,譯者補充:是新的鎖門按鈕)。說這個按鈕按得太早不算是鎖門按鈕,合理。然而門鎖狀態變為 locked 時,你就會驚奇地發現一個鎖門按鈕事件觸發了。
我們用?gen_statem:call 來實現 button/1 函數,仍在 open 狀態延緩它所有的按鈕事件。在 open 狀態調用 button/1,狀態變為 locked 之前它不會返回,因為 locked 狀態時事件才會被處理并且回復。
如果另一個進程在 button/1 掛起,有人調用 set_lock_button/1 來改變鎖門按鈕,被掛起的 button 調用會立刻生效,門被鎖住。因此,我們把當前的門鎖按鈕作為狀態的一部分,這樣當我們改變門鎖按鈕時,狀態會改變,所有的延緩事件會重新觸發。
我們定義狀態為 {StateName,LockButton},其中 StateName 和之前一樣,而 LockButton 則表示當前的鎖門按鈕:
-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_3).-export([start_link/2,stop/0]). -export([button/1,code_length/0,set_lock_button/1]). -export([init/1,callback_mode/0,terminate/3,code_change/4,format_status/2]). -export([handle_event/4]).start_link(Code, LockButton) ->gen_statem:start_link({local,?NAME}, ?MODULE, {Code,LockButton}, []). stop() ->gen_statem:stop(?NAME).button(Digit) ->gen_statem:call(?NAME, {button,Digit}). code_length() ->gen_statem:call(?NAME, code_length). set_lock_button(LockButton) ->gen_statem:call(?NAME, {set_lock_button,LockButton}).init({Code,LockButton}) ->process_flag(trap_exit, true),Data = #{code => Code, remaining => undefined},{ok, {locked,LockButton}, Data}.callback_mode() ->[handle_event_function,state_enter].handle_event({call,From}, {set_lock_button,NewLockButton},{StateName,OldLockButton}, Data) ->{next_state, {StateName,NewLockButton}, Data,[{reply,From,OldLockButton}]}; handle_event({call,From}, code_length,{_StateName,_LockButton}, #{code := Code}) ->{keep_state_and_data,[{reply,From,length(Code)}]}; %% %% State: locked handle_event(EventType, EventContent,{locked,LockButton}, #{code := Code, remaining := Remaining} = Data) ->case {EventType, EventContent} of{enter, _OldState} ->do_lock(),{keep_state, Data#{remaining := Code}};{timeout, _} ->{keep_state, Data#{remaining := Code}};{{call,From}, {button,Digit}} ->case Remaining of[Digit] -> % Complete{next_state, {open,LockButton}, Data,[{reply,From,ok}]};[Digit|Rest] -> % Incomplete{keep_state, Data#{remaining := Rest, 30000},[{reply,From,ok}]};[_|_] -> % Wrong{keep_state, Data#{remaining := Code},[{reply,From,ok}]}endend; %% %% State: open handle_event(EventType, EventContent,{open,LockButton}, Data) ->case {EventType, EventContent} of{enter, _OldState} ->do_unlock(),{keep_state_and_data, [{state_timeout,10000,lock}]};{state_timeout, lock} ->{next_state, {locked,LockButton}, Data};{{call,From}, {button,Digit}} ->ifDigit =:= LockButton ->{next_state, {locked,LockButton}, Data,[{reply,From,locked}]};true ->{keep_state_and_data,[postpone]}endend.do_lock() ->io:format("Locked~n", []). do_unlock() ->io:format("Open~n", []).terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok. code_change(_Vsn, State, Data, _Extra) ->{ok,State,Data}. format_status(Opt, [_PDict,State,Data]) ->StateData ={State,maps:filter(fun (code, _) -> false;(remaining, _) -> false;(_, _) -> trueend,Data)},case Opt ofterminate ->StateData;normal ->[{data,[{"State",StateData}]}]end.
對現實中的鎖來說,button/1 在狀態變為 locked 前被掛起不合理。但是作為一個 API,還好。
?
3.22 掛起
(譯者補充:此掛起跟前文的掛起不同,前文的掛起僅意味著 receive 阻塞。)
如果一個節點中有很多個 server,并且他們在生命周期中某些時候會空閑,那么這些 server 的堆內存會造成浪費,通過?proc_lib:hibernate/3 來掛起 server 會把它的內存占用降到最低。
注意:掛起一個進程代價很高,詳見 erlang:hibernate/3 。不要在每個事件之后都掛起它。
此例中我們可以在 {open,_} 狀態掛起,因為正常來說只有在一段時間后它才會收到狀態超時,遷移至 locked 狀態:
... %% State: open handle_event(EventType, EventContent,{open,LockButton}, Data) ->case {EventType, EventContent} of{enter, _OldState} ->do_unlock(),{keep_state_and_data,[{state_timeout,10000,lock},hibernate]}; ...
最后一行的動作列表中 hibernate 是唯一的修改。如果任何事件在 {open,_} 狀態到達,我們不用再重新掛起,接收事件后 server 會一直處于活躍狀態。
如果要重新掛起,我們需要在更多的地方插入 hibernate 來改變。例如,跟狀態無關的 set_lock_button 和 code_length 操作,在 {open,_} 狀態可以讓他 hibernate,但是這樣會讓代碼很亂。
另一個不常用的方法是使用事件超時,在一段時間的不活躍后觸發掛起。
本例可能不值得使用掛起來降低堆內存。只有在運行中產生了垃圾的 server 才會從掛起中受益,從這個層面說,上面的是個不好的例子。
?
4?gen_event Behaviour
此章可結合 gen_event(3)(包含全部接口函數和回調函數的詳述)教程一起看。
4.1 事件處理原則
在 OTP 中,一個事件管理器(event manager)是一個可以接收事件的指定的對象。事件(event)可能是要記錄日志的錯誤、警告、信息等等。
事件管理器中可以安裝(install)0個、1個或更多的事件處理器(event handler)。當事件管理器收到一個事件通知,這個事件被所有安裝好的事件處理器處理。例如,一個處理錯誤的事件管理器可能內置一個默認的處理器,把錯誤寫到終端。如果某段時間需要把錯誤信息寫到文件,用戶可以添加另一個處理器來處理。不需要再寫入文件時,則可以刪除這個處理器。
事件管理器是一個進程,而事件處理器則是一個回調模塊。
事件管理器本質上就是維護一個 {Module, State} 列表,其中 Module 是一個事件處理器,State 則是處理器的內部狀態。
?
4.2 例子
將錯誤信息寫到終端的事件處理器的回調模塊可能長這樣:
-module(terminal_logger). -behaviour(gen_event).-export([init/1, handle_event/2, terminate/2]).init(_Args) ->{ok, []}.handle_event(ErrorMsg, State) ->io:format("***Error*** ~p~n", [ErrorMsg]),{ok, State}.terminate(_Args, _State) ->ok.
將錯誤信息寫到文件的事件處理器的回調模塊可能長這樣:
-module(file_logger). -behaviour(gen_event).-export([init/1, handle_event/2, terminate/2]).init(File) ->{ok, Fd} = file:open(File, read),{ok, Fd}.handle_event(ErrorMsg, Fd) ->io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),{ok, Fd}.terminate(_Args, Fd) ->file:close(Fd).
下一小節分析這些代碼。
?
4.3 開啟一個事件管理器
調用下面的函數來開啟一個前例中說的處理錯誤的事件管理器:
gen_event:start_link({local, error_man})
這個函數創建并連接一個新進程(事件管理器 event manager)。
參數 {local, error_man} 指定了事件管理器的名字,事件管理器在本地注冊為 error_man。
如果名字參數被忽略,事件管理器不會被注冊,則必須用到它的進程 pid。名字還可以用{global, Name},這樣的話會調用 global:register_name/2 來注冊事件管理器。
如果 gen_event 是一個監控樹的一部分,supervisor 啟動 gen_event 時一定要使用 gen_event:start_link。還有一個函數是 gen_event:start ,這個函數會啟動一個獨立的 gen_event,也就是說它不會成為監控樹的一部分。
?
4.4 添加一個事件處理器
下例表明了在 shell 中,如何開啟一個事件管理器,并為它添加一個事件處理器:
1> gen_event:start({local, error_man}). {ok,<0.31.0>} 2> gen_event:add_handler(error_man, terminal_logger, []). ok
這個函數會發送一個消息給事件處理器 error_man,告訴它需要添加一個事件處理器 terminal_logger。事件管理器會調用函數 terminal_logger:init([]) (init 的參數 [] 是 add_handler 的第三個參數)。正常的話 init 會返回 {ok, State},State就是事件處理器的內部狀態。
init(_Args) ->{ok, []}.
此例中 init 不需要任何輸入,因此忽略了它的參數。terminal_logger 中不需要用到內部狀態,file_logger 可以用內部狀態來保存文件描述符。
init(File) ->{ok, Fd} = file:open(File, read),{ok, Fd}.
?
4.5 事件通知
3> gen_event:notify(error_man, no_reply). ***Error*** no_reply ok
其中 error_man 是事件處理器的注冊名,no_reply 是事件。
這個事件會以消息的形式發送給事件處理器。接收事件時,事件管理器會按照安裝的順序,依次調用每個事件處理器的 handle_event(Event, State)。handle_event 正常會返回元組 {ok,State1},其中 State1 是事件處理器的新的內部狀態。
terminal_logger 中:
handle_event(ErrorMsg, State) ->io:format("***Error*** ~p~n", [ErrorMsg]),{ok, State}.
file_logger 中:
handle_event(ErrorMsg, Fd) ->io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),{ok, Fd}.
?
4.6 刪除事件處理器
4> gen_event:delete_handler(error_man, terminal_logger, []).
ok
這個函數會發送一條消息給注冊名為 error_man 的事件管理器,告訴它要刪除處理器 terminal_logger。此時管理器會調用 terminal_logger:terminate([], State),其中 [] 是 delete_handler 的第三個參數。terminate 中應該做與 init 相反的事情,做一些清理工作。它的返回值會被忽略。
terminal_logger 不需要做清理:
terminate(_Args, _State) ->ok.
file_logger 需要關閉 init 中開啟的文件描述符:
terminate(_Args, Fd) ->file:close(Fd).
?
4.7 終止
當事件管理器被終止,它會調用每個處理器的 terminate/2,和刪除處理器時一樣。
在監控樹中
如果管理器是監控樹的一部分,則不需要終止函數。管理器自動的被它的監控者終止,具體怎么終止通過 終止策略 來決定。
獨立的事件管理器
事件管理器可以通過調用以下函數終止:
> gen_event:stop(error_man).
ok
?
4.8 處理其他消息
如果想要處理事件之外的其他消息,需要實現回調函數 handle_info(Info, StateName, StateData)。比如說 exit 消息,當 gen_event 與其他進程(非它的監控者)連接,并且被設置為捕捉 exit 信號。
handle_info({'EXIT', Pid, Reason}, State) ->..code to handle exits here..{ok, NewState}.
code_change 函數也需要實現。
code_change(OldVsn, State, Extra) ->..code to convert state (and more) during code change{ok, NewState}
?
5?Supervisor Behaviour
這部分可與 stdblib 中的 supervisor(3) 教程(包含了所有細節)一起閱讀。
5.1 監控原則
監控者(supervisor)要負責開啟、終止和監控它的子進程。監控者的基本理念就是通過必要時的重啟,來保證子進程一直活著。
子進程規格說明指定了要啟動和監控的子進程。子進程根據規格列表依次啟動,終止順序和啟動順序相反。
?
5.2 例子
下面的例子是啟動 gen_server 子進程的監控樹:
-module(ch_sup). -behaviour(supervisor).-export([start_link/0]). -export([init/1]).start_link() ->supervisor:start_link(ch_sup, []).init(_Args) ->SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},ChildSpecs = [#{id => ch3,start => {ch3, start_link, []},restart => permanent,shutdown => brutal_kill,type => worker,modules => [cg3]}],{ok, {SupFlags, ChildSpecs}}.
返回值中的 SupFlags 即 supervisor flag,詳見下一小節。
ChildSpecs 是子進程規格列表。
?
5.3?supervisor flag
下面是 supervisor flag 的類型定義:
sup_flags() = #{strategy => strategy(), % optionalintensity => non_neg_integer(), % optionalperiod => pos_integer()} % optionalstrategy() = one_for_all| one_for_one| rest_for_one| simple_one_for_one
- strategy 指定了重啟策略。
- intensity 和 period 指定了最大重啟頻率。
?
5.4 重啟策略
重啟策略是由 init 返回的 map 中的 strategy 來指定的:
SupFlags = #{strategy => Strategy, ...}
strategy 是可選參數,如果沒有指定,默認為 one_for_one。
one_for_one
如果子進程終止,只有終止的子進程會被重啟。
圖5.1 one_for_one 監控樹
one_for_all
如果一個子進程終止,其他子進程都會被終止,然后所有子進程被重啟。
圖5.2 one_for_all 監控樹
rest_for_one
如果一個子進程終止,啟動順序在此子進程之后的子進程們都會被終止。然后這些終止的進程(包括自己終止的那位)被重啟。
simple_one_for_one
詳見 simple-one-for-one supervisors(譯者補充:本原則中也有提及simple_one_for_one)
?
5.5 最大重啟頻率
supervisor 內置了一個機制來限制給定時間間隔內的重啟次數。由 init 函數返回的 supervisor flag 中的 intensity 和 period 字段來指定:
SupFlags = #{intensity => MaxR, period => MaxT, ...}
如果 MaxT 秒內重啟了 MaxR 次,監控者會終止所有的子進程,然后退出。此時 supervisor 退出的理由是 shutdown。
當 supervisor 終止時,它的上一級 supervisor 會作出一些處理,重啟它,或者跟著退出。
這個重啟機制的目的是防止進程反復因為同一原因終止和重啟。
intensity 和 period 都是可選參數,如果沒有指定,它們缺省值分別為1和5。
調整 intensity 和 period
缺省值為5秒重啟1次。這個配置對大部分系統(即便是很深的監控樹)來說都是保險的,但你可能想為某些特殊的應用場景做出調整。
首先,intensity 決定了你能忍受多少次突發重啟。例如,你只能接受5~10次的重啟嘗試(盡管下一秒它可能會重啟成功)。
其次,如果崩潰持續發生,但是沒有頻繁到讓 supervisor 放棄,你需要考慮持續的失敗率。比如說你把 intensity 設置為10,而 period 為1,supervisor 會允許子進程在1秒內重啟10次,在人工干預前它會持續往日志中寫入 crash 報告。
此時你需要把 period 設置得足夠大,讓 supervisor 在你能接受的比值下運行。例如,你將 intensity 設置為5,period 為30s,會讓它在一段時間內允許平均6s的重啟間隔,這樣你的日志就不會太快被填滿,你可以觀察錯誤,然后作出修復。
這些選擇取決于你的問題作用域。如果你不會實時監測或者不能快速解決問題(例如在嵌入式系統中),你可能想1分鐘最多重啟一次,把問題交給更高層去自動清理錯誤。或者有時候,可能高失敗率時仍然嘗試重啟是更好的選擇,你可以設置成一秒1-2次重啟。
避免一些常見的錯誤:
- 不要忘記考慮爆發率。如果你把 intensity 設置為1,period 為6,它的長期錯誤率與5/30和10/60差不多,但是它不允許連續兩次重啟。這可能不是你想要的。
- 如果想容忍爆發,不要把 period 設置得很大。如果你把 intensity 設置為5,period 為3600(1小時),supervisor 允許短時間內重啟5次,然而接近(但不到)一個小時的一次崩潰會導致它放棄。而這兩撥崩潰可能是不同原因導致的,所以設置為5到10分鐘會更合理。
- 如果你的應用包含多級監控,不要簡單地把所有層的重啟頻率設置成相同的值。在頂層 supervisor 放棄重啟并終止應用之前,重啟的總次數是崩潰的子進程上層的所有 supervisor 的密度的乘積。
?????????? 例如,如果最上層允許10次重啟,第二層也允許10次,下層崩潰的子進程會被重啟100次,這太多了。最上層允許3次重啟可能更好。
?
5.6 子進程規格說明
下面是子進程規格(child specification)的類型定義:?
child_spec() = #{id => child_id(), % mandatorystart => mfargs(), % mandatoryrestart => restart(), % optionalshutdown => shutdown(), % optionaltype => worker(), % optionalmodules => modules()} % optionalchild_id() = term()mfargs() = {M :: module(), F :: atom(), A :: [term()]}modules() = [module()] | dynamicrestart() = permanent | transient | temporaryshutdown() = brutal_kill | timeout()worker() = worker | supervisor
- id 在 supervisor 內部被用來識別不同的 child specificaton。
??????? ?? id 是必填項
????????? 有時 id 會被稱為 name,現在一般都用 identifier 或者 id,但為了向后兼容,有時也能看到 name,例如在錯誤信息中。
- start 規定了啟動子進程的函數。它是一個 模塊-函數-參數 元組,用來傳遞給 apply(M, F, A) 。
?????? ? ? 它應該(或者最終應該)調用下面這些函數:
-
- supervisor:start_link
- gen_server:start_link
- gen_statem:start_link
- gen_event:start_link
- 跟這些函數類似的函數。詳見 supervisor(3) 的 start 參數。(譯者補充:函數應滿足條件:創建并且連接到子進程,且必須返回 {ok,Child} 或 {ok,Child,Info},其中 Child 是子進程 pid,Info 會被 supervisor 忽略)
?????? ? ? start 是必填項。
- restart 規定了什么時候一個終止的進程會觸發重啟
- permanent 表示進程總是觸發重啟
- temporary 表示進程不會被重啟(即便重啟策略是 rest_for_one 或 one_for_all,前置進程導致 temporary 進程終止)
- transient 僅在進程異常退出時重啟,即:終止理由不是 normal、shutdown 或 {shutdown,Term} 。
???????? ? restart 是可選項,缺省值為 permanent。
- shutdown 規定了進程被終止的方式
- brutal_kill 表示會使用 exit(Child, kill) 無條件終止子進程。
- 一個整數超時值,意味著 supervisor 會調用 exit(Child, shutdown) 通知子進程退出,然后等待退出信號返回。如果指定時間內沒有收到退出信號,子進程會被 exit(Child, kill) 無條件終止。
- 如果子進程是一個 supervisor,可以設置為 infinity 來讓子監控樹有足夠的時間退出。如果子進程是 worker 也可以設置為 infinity。警告:
警告:當子進程是 worker 時慎用 infinity。因為這種情況下,監控樹的退出取決于子進程的退出,必須要安全地實現子進程,確保它的清理過程必定會返回。
????? ? ?? shutdown 是可選項,如果子進程是 worker,默認為 5000;如果子進程是監控樹,默認為 infinity。
- type 標明子進程是 worker 還是 supervisor
?????? ? ? type 是可選項,缺省值為 worker。
- modules 當 Module 是回調模塊名,modules 是單元素的列表 [Module](子進程為 supervisor, gen_server, gen_statem);如果子進程是 gen_event,值應該為 dynamic。
?????????? 這個字段在發布管理的升級和降級中會用到,詳見 Release Handling。
?????????? modules 是可選項,缺省值為 [M],其中 M 來自子進程的啟動參數 {M,F,A} 。
例:前例中 ch3 的子進程規格如下:
#{id => ch3,start => {ch3, start_link, []},restart => permanent,shutdown => brutal_kill,type => worker,modules => [ch3]}
或者簡化一下,取默認值:
#{id => ch3,start => {ch3, start_link, []}shutdown => brutal_kill}
例:上文的 gen_event 子進程規格如下:
#{id => error_man,start => {gen_event, start_link, [{local, error_man}]},modules => dynamic}
這兩個都是注冊進程,都被期望一直能訪問到。所以他們被指定為 permanent 。
ch3 在終止前不需要做任何清理工作,所以不需要指定終止時間,shudown 值設置為 brutal_kill 就行了。而 error_man 需要時間去清理,所以設置為5000毫秒(默認值)。
例:啟動另一個 supervisor 的子進程規格:
#{id => sup,start => {sup, start_link, []},restart => transient,type => supervisor} % will cause default shutdown=>infinity (type為supervisor會導致shutdown的默認值為infinity)
?
5.7 啟動supervisor
前例中,supervisor 通過調用 ch_sup:start_link() 來啟動:
start_link() ->supervisor:start_link(ch_sup, []).
ch_sup:start_link?函數調用 supervisor:start_link/2,生成并連接了一個新進程(supervisor)。
- 第一個參數,ch_sup 是回調模塊的名字,也就是 init 函數所在的模塊。
- 第二個參數,[],是傳遞給 init 函數的參數,此例中 init 不需要任何輸入,忽略了此參數。
此例中 supervisor 沒有被注冊,因此必須用到它的 pid。可以通過調用 supervisor:start_link({local, Name}, Module, Args) 或 supervisor:start_link({global, Name}, Module, Args) 來指定它的名字。
這個新的?supervisor 進程會調用 init 回調 ch_sup:init([])。init 函數應該返回 {ok, {SupFlags, ChildSpecs}}。
init(_Args) ->SupFlags = #{},ChildSpecs = [#{id => ch3,start => {ch3, start_link, []},shutdown => brutal_kill}],{ok, {SupFlags, ChildSpecs}}.
然后 supervisor 會根據子進程規格列表,啟動所有的子進程。此例中只有一個子進程,ch3 。
supervisor:start_link 是同步調用,在所有子進程啟動之前它不會返回。
?
5.8 增加子進程
除了靜態的監控樹外,還可以動態地添加子進程到監控樹中:
supervisor:start_child(Sup, ChildSpec)
Sup 是 supervisor 的 pid 或注冊名。ChildSpec 是子進程規格。
使用 start_child/2 添加的子進程跟其他子進程行為一樣,除了一點:如果 supervisor 終止并被重啟,所有動態添加的進程都會丟失。
?
5.9 終止子進程
調用下面的函數,靜態或動態的子進程,都會根據規格終止:
supervisor:terminate_child(Sup, Id)
一個終止的子進程的規格可通過下面的函數刪除:
supervisor:delete_child(Sup, Id)
Sup 是 supervisor 的 pid 或注冊名。Id 是子進程規格中的 id 項。
刪除靜態的子進程規格會導致它跟動態子進程一樣,在 supervisor 重啟時丟失。
?
5.10 簡化的 one_for_one(simple_one_for_one)
重啟策略 simple_one_for_one 是簡化的 one_for_one,所有的子進程是相同過程的實例,被動態地添加到監控樹中。
下面是一個 simple_one_for_one 的 supervisor 回調模塊:
-module(simple_sup). -behaviour(supervisor).-export([start_link/0]). -export([init/1]).start_link() ->supervisor:start_link(simple_sup, []).init(_Args) ->SupFlags = #{strategy => simple_one_for_one,intensity => 0,period => 1},ChildSpecs = [#{id => call,start => {call, start_link, []},shutdown => brutal_kill}],{ok, {SupFlags, ChildSpecs}}.
啟動時,supervisor 沒有啟動任何子進程。所有的子進程是通過調用如下函數動態添加的:
supervisor:start_child(Pid, [id1])
子進程會通過調用 apply(call, start_link, []++[id1]) 來啟動,即:
call:start_link(id1)
simple_one_for_one 監程的子進程通過下面的方式來終止:
supervisor:terminate_child(Sup, Pid)
Sup 是 supervisor 的 pid 或注冊名。Pid 是子進程的 pid。
由于 simple_one_for_one 的監程可能有大量的子進程,所以它是異步終止它們的。就是說子進程平行地做清理工作,終止順序不可預測。
?
5.11 終止
由于 supervisor 是監控樹的一部分,它會自動地被它的 supervisor 終止。當被要求終止時,它會根據 shutdown 配置按照與啟動相反的順序(譯者補充:除了 simple_one_for_one 模式)終止所有的子進程,然后退出。
?
6?sys and proc_lib
sys 模塊包含一些函數,可以簡單地 debug 用 behaviour 實現的進程。還有一些函數可以和 proc_lib 模塊的函數一起,用來實現特殊的進程,這些特殊的進程不采用標準的 behaviour,但是滿足 OTP 設計原則。這些函數還可以用來實現用戶自定義(非標準)的 behaviour。
sys 和 proc_lib 模塊都屬于 STDLIB 應用。
6.1 簡易debug
sys 模塊包含一些函數,可以簡單地 debug 用 behaviour 實現的進程。用 gen_statem Behaviour 中的例子 code_lock 舉例:
Erlang/OTP 20 [DEVELOPMENT] [erts-9.0] [source-5ace45e] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false]Eshell V9.0 (abort with ^G) 1> code_lock:start_link([1,2,3,4]). Lock {ok,<0.63.0>} 2> sys:statistics(code_lock, true). ok 3> sys:trace(code_lock, true). ok 4> code_lock:button(1). *DBG* code_lock receive cast {button,1} in state locked ok *DBG* code_lock consume cast {button,1} in state locked 5> code_lock:button(2). *DBG* code_lock receive cast {button,2} in state locked ok *DBG* code_lock consume cast {button,2} in state locked 6> code_lock:button(3). *DBG* code_lock receive cast {button,3} in state locked ok *DBG* code_lock consume cast {button,3} in state locked 7> code_lock:button(4). *DBG* code_lock receive cast {button,4} in state locked ok Unlock *DBG* code_lock consume cast {button,4} in state locked *DBG* code_lock receive state_timeout lock in state open Lock *DBG* code_lock consume state_timeout lock in state open 8> sys:statistics(code_lock, get). {ok,[{start_time,{{2017,4,21},{16,8,7}}},{current_time,{{2017,4,21},{16,9,42}}},{reductions,2973},{messages_in,5},{messages_out,0}]} 9> sys:statistics(code_lock, false). ok 10> sys:trace(code_lock, false). ok 11> sys:get_status(code_lock). {status,<0.63.0>,{module,gen_statem},[[{'$initial_call',{code_lock,init,1}},{'$ancestors',[<0.61.0>]}],running,<0.61.0>,[],[{header,"Status for state machine code_lock"},{data,[{"Status",running},{"Parent",<0.61.0>},{"Logged Events",[]},{"Postponed",[]}]},{data,[{"State",{locked,#{code => [1,2,3,4],remaining => [1,2,3,4]}}}]}]]}
?
6.2 特殊的進程
此小節講述怎么不使用標準 behaviour 來寫一個程序,使它滿足 OTP 設計原則。這樣一個進程需要滿足:
- 提供啟動方式使它可以納入監控樹中
- 支持 sys 的debug工具
- 關心系統消息
系統消息是在監控樹中用到的、有特殊意義的消息。典型的系統消息有追蹤輸出的請求、掛起或恢復進程的請求(release handling 發布管理中用到)。使用標準 behaviour 實現的進程能自動處理這些消息。
例子
概述里面的簡單服務器,使用 sys 和 proc_lib 來實現以使其可納入監控樹中:
-module(ch4). -export([start_link/0]). -export([alloc/0, free/1]). -export([init/1]). -export([system_continue/3, system_terminate/4,write_debug/3,system_get_state/1, system_replace_state/2]).start_link() ->proc_lib:start_link(ch4, init, [self()]).alloc() ->ch4 ! {self(), alloc},receive{ch4, Res} ->Resend.free(Ch) ->ch4 ! {free, Ch},ok.init(Parent) ->register(ch4, self()),Chs = channels(),Deb = sys:debug_options([]),proc_lib:init_ack(Parent, {ok, self()}),loop(Chs, Parent, Deb).loop(Chs, Parent, Deb) ->receive{From, alloc} ->Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, alloc, From}),{Ch, Chs2} = alloc(Chs),From ! {ch4, Ch},Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3,ch4, {out, {ch4, Ch}, From}),loop(Chs2, Parent, Deb3);{free, Ch} ->Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, {free, Ch}}),Chs2 = free(Ch, Chs),loop(Chs2, Parent, Deb2);{system, From, Request} ->sys:handle_system_msg(Request, From, Parent,ch4, Deb, Chs)end.system_continue(Parent, Deb, Chs) ->loop(Chs, Parent, Deb).system_terminate(Reason, _Parent, _Deb, _Chs) ->exit(Reason).system_get_state(Chs) ->{ok, Chs}.system_replace_state(StateFun, Chs) ->NChs = StateFun(Chs),{ok, NChs, NChs}.write_debug(Dev, Event, Name) ->io:format(Dev, "~p event = ~p~n", [Name, Event]).
sys 模塊中的簡易 debug 也可用于 ch4:
% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]Eshell V5.2.3.6 (abort with ^G) 1> ch4:start_link(). {ok,<0.30.0>} 2> sys:statistics(ch4, true). ok 3> sys:trace(ch4, true). ok 4> ch4:alloc(). ch4 event = {in,alloc,<0.25.0>} ch4 event = {out,{ch4,ch1},<0.25.0>} ch1 5> ch4:free(ch1). ch4 event = {in,{free,ch1}} ok 6> sys:statistics(ch4, get). {ok,[{start_time,{{2003,6,13},{9,47,5}}},{current_time,{{2003,6,13},{9,47,56}}},{reductions,109},{messages_in,2},{messages_out,1}]} 7> sys:statistics(ch4, false). ok 8> sys:trace(ch4, false). ok 9> sys:get_status(ch4). {status,<0.30.0>,{module,ch4},[[{'$ancestors',[<0.25.0>]},{'$initial_call',{ch4,init,[<0.25.0>]}}],running,<0.25.0>,[],[ch1,ch2,ch3]]}
啟動進程
proc_lib 中的一些函數可用來啟動進程。有幾個函數可選,如:異步啟動 spawn_link/3,4 和同步啟動 start_link/3,4,5 。
使用這些函數啟動的進程會存儲一些信息(比如高層級進程 ancestor 和初始化回調 initial call),這些信息在監控樹中會被用到。
如果進程以除 normal 或 shutdown 之外的理由終止,會生成一個 crash 報告。可以在 SASL 的用戶手冊中了解更多 crash 報告的內容。
此例中,使用了同步啟動。進程通過 ch4:start_link() 來啟動:
start_link() ->proc_lib:start_link(ch4, init, [self()]).
ch4:start_link 調用了函數 proc_lib:start_link 。這個函數的參數為模塊名、函數名和參數列表,它創建并連接到一個新進程。新進程執行給定的函數來啟動,ch4:init(Pid),其中 Pid 是第一個進程的 pid,即父進程。
所有的初始化(包括名字注冊)都在 init 中完成。新進程需要通知父進程它的啟動:
init(Parent) ->...proc_lib:init_ack(Parent, {ok, self()}),loop(...).
proc_lib:start_link 是同步函數,在 proc_lib:init_ack 被調用前不會返回。
Debugging
要支持 sys 的 debug 工具,需要 debug 結構。Deb 通過 sys:debug_options/1 來初始生成:
init(Parent) ->...Deb = sys:debug_options([]),...loop(Chs, Parent, Deb).
sys:debug_options/1 的參數為一個選項列表。此例中列表為空,即初始時沒有 debug 被啟用。可用選項詳見 sys 模塊的用戶手冊。
然后,對于每個要記錄或追蹤的系統事件,下面的函數會被調用:
sys:handle_debug(Deb, Func, Info, Event) => Deb1
其中:
- Deb 是 debug 結構
- Func 指定了一個用戶自定義的函數,用來格式化追蹤輸出。對于每個系統事件,格式化函數會被調用 Func(Dev, Event, Info),其中:
- Dev 是要輸出到的 I/0 設備,詳見 io 模塊的手冊。
- Event 和 Info 是從 handle_debug 傳入的。
- Info 用來傳遞更多信息給 Func,可以是任何類型,會原樣傳給 Func。
- Event 是系統事件。用戶可以決定系統事件的定義和表現形式。一般至少輸入和輸出消息會被認為是系統事件,分別用 {in,Msg[,From]} 和 {out,Msg,To} 表示。
handle_debug 返回一個更新的 debug 結構 Deb1。
此例中,handle_debug 會在每次輸入和輸出信息時被調用。格式化函數 Func 即 ch4:write_debug/3,它調用 io:format/3 打印消息:
loop(Chs, Parent, Deb) ->receive{From, alloc} ->Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, alloc, From}),{Ch, Chs2} = alloc(Chs),From ! {ch4, Ch},Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3,ch4, {out, {ch4, Ch}, From}),loop(Chs2, Parent, Deb3);{free, Ch} ->Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, {free, Ch}}),Chs2 = free(Ch, Chs),loop(Chs2, Parent, Deb2);...end.write_debug(Dev, Event, Name) ->io:format(Dev, "~p event = ~p~n", [Name, Event]).
處理系統消息
收到的系統消息形如:
{system, From, Request}
這些消息的內容和意義,進程不需要理解,而是直接調用下面的函數:
sys:handle_system_msg(Request, From, Parent, Module, Deb, State)
這個函數不會返回。它處理了系統消息之后,如果要繼續執行,會調用:
Module:system_continue(Parent, Deb, State)
如果進程終止,調用:
Module:system_terminate(Reason, Parent, Deb, State)
監控樹中的進程應以父進程相同的理由退出。
- Request 和 From 是從系統消息中原樣傳遞的。
- Parent 是父進程的 pid。
- Module 是模塊名。
- Deb 是 debug 結構。
- State 是描述內部狀態的項,會被傳遞給 system_continue/system_terminate/ system_get_state/system_replace_state。
如果進程要返回它的狀態,handle_system_msg 會調用:
Module:system_get_state(State)
如果進程要調用函數 StateFun 替換它的狀態,handle_system_msg 會調用:
Module:system_replace_state(StateFun, State)
此例中對應代碼:
loop(Chs, Parent, Deb) ->receive...{system, From, Request} ->sys:handle_system_msg(Request, From, Parent,ch4, Deb, Chs)end.system_continue(Parent, Deb, Chs) ->loop(Chs, Parent, Deb).system_terminate(Reason, Parent, Deb, Chs) ->exit(Reason).system_get_state(Chs) ->{ok, Chs, Chs}.system_replace_state(StateFun, Chs) ->NChs = StateFun(Chs),{ok, NChs, NChs}.
如果這個特殊的進程設置為捕捉 exit 信號,并且父進程終止,它的預期行為是以同樣的理由終止:
init(...) ->...,process_flag(trap_exit, true),...,loop(...).loop(...) ->receive...{'EXIT', Parent, Reason} ->..maybe some cleaning up here..exit(Reason);...end.
?
6.3 自定義behaviour
要實現自定義 behaviour,代碼跟特殊進程差不多,除了要調用回調模塊里的函數來處理特殊的任務。
如果想要編譯器像對 OTP 的 behaviour 一樣,給缺少的回調函數報警告,需要在 behaviour 模塊增加 -callback 屬性來描述預期的回調:
-callback Name1(Arg1_1, Arg1_2, ..., Arg1_N1) -> Res1. -callback Name2(Arg2_1, Arg2_2, ..., Arg2_N2) -> Res2. ... -callback NameM(ArgM_1, ArgM_2, ..., ArgM_NM) -> ResM.
NameX 是預期的回調名。ArgX_Y 和 ResX 是 Types and Function Specifications 中所描述的類型。-callback 屬性支持 -spec 的所有語法。
-optional_callbacks 屬性可以用來指定可選的回調:
-optional_callbacks([OptName1/OptArity1, ..., OptNameK/OptArityK]).
其中每個 OptName/OptArity 指定了一個回調函數的名字和參數個數。-optional_callbacks 應與 -callback 一起使用,它不能與下文的 behaviour_info() 結合使用。
注意:我們推薦使用 -callback 而不是 behaviour_info() 函數。因為工具可以用額外的類型信息來生成文檔和找出矛盾。
你也可以實現并導出 behaviour_info() 來替代 -callback 和 -optional_callbacks 屬性:
behaviour_info(callbacks) ->[{Name1, Arity1},...,{NameN, ArityN}].
其中每個 {Name, Arity} 指定了回調函數的名字和參數個數。使用 -callback 屬性會自動生成這個函數。
當編譯器在模塊 Mod 中遇到屬性 -behaviour(Behaviour),它會調用 Behaviour:behaviour_info(callbacks),并且與 Mod 實際導出的函數集相比較,在缺少回調函數的時候發布一個警告。
例:
%% User-defined behaviour module -module(simple_server). -export([start_link/2, init/3, ...]).-callback init(State :: term()) -> 'ok'. -callback handle_req(Req :: term(), State :: term()) -> {'ok', Reply :: term()}. -callback terminate() -> 'ok'. -callback format_state(State :: term()) -> term().-optional_callbacks([format_state/1]).%% Alternatively you may define: %% %% -export([behaviour_info/1]). %% behaviour_info(callbacks) -> %% [{init,1}, %% {handle_req,2}, %% {terminate,0}]. start_link(Name, Module) ->proc_lib:start_link(?MODULE, init, [self(), Name, Module]).init(Parent, Name, Module) ->register(Name, self()),...,Dbg = sys:debug_options([]),proc_lib:init_ack(Parent, {ok, self()}),loop(Parent, Module, Deb, ...)....
在回調模塊中:
-module(db). -behaviour(simple_server).-export([init/1, handle_req/2, terminate/0])....
behaviour 模塊中 -callback 屬性指定的協議,在回調模塊中可以添加 -spec 屬性來優化。-callback 指定的協議一般都比較寬泛,所以 -spec 會非常有用。有協議的回調模塊:
-module(db). -behaviour(simple_server).-export([init/1, handle_req/2, terminate/0]).-record(state, {field1 :: [atom()], field2 :: integer()}).-type state() :: #state{}. -type request() :: {'store', term(), term()};{'lookup', term()}....-spec handle_req(request(), state()) -> {'ok', term()}....
每個 -spec 協議都是對應的 -callback 協議的子類型。
?
7?Applications
此部分可與 Kernel 手冊中的 app 和 application 部分一起閱讀。
7.1 應用概念
如果你編碼實現了一些特定的功能,你可能想把它封裝成一個應用,可以作為一個整體啟動和終止,在其他系統中可以重用等。
要做到這一點,需要創建一個應用回調模塊,描述怎么啟動和終止這個應用。
然后還需要一個應用規格說明(application specification),把它放在應用資源文件中。這個文件指定了組成應用的模塊列表以及回調模塊名。
如果你使用 Erlang/OTP 的代碼打包工具 systools(詳見 Releases),每個應用的代碼都放在不同的目錄下,并遵循預定義的目錄結構 。
?
7.2 應用回調模塊
在下面兩個回調函數中,指定了怎么啟動和終止應用(即監控樹):
start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State}
stop(State)
- start 函數在啟動應用的時候被調用,它通過啟動頂層的 supervisor 來創建監控樹。正常它會返回頂層 supervisor 的pid,和一個可選字段 State(默認為 [] )。State 會傳遞給 stop 函數。
- StartType 通常是 normal 。只有在接管或故障切換(譯者補充:分布式應用提供的功能)時它會有其他值,詳見 Distributed Applications 。
- StartArgs 在應用資源文件中由 mod 指定。
- stop/1 在應用停止之后調用,用來做清理工作。實際的應用終止(即監控樹的終止)是自動處理的,詳見啟動和終止應用。
打包前文 Supervisor Behaviour 的監控樹為一個應用,應用回調模塊如下:
-module(ch_app). -behaviour(application).-export([start/2, stop/1]).start(_Type, _Args) ->ch_sup:start_link().stop(_State) ->ok.
庫應用不需要啟動和終止,所以不需要應用回調模塊。
?
7.3 應用資源文件
應用的規格說明用來配置一個應用,它放在應用資源文件中,簡稱 .app 文件:
{application, Application, [Opt1,...,OptN]}.
- Application,atom 類型,應用的名字。資源文件名必須為 Application.app 。
- 每個 Opt 都是 {Key,Value} 元組,指定了應用的一個特定屬性。所有的 key 都是可選項,每個 key 都有缺省值。
庫應用的最簡短的 .app 文件長這樣(libapp 應用):
{application, libapp, []}.
有監控樹的應用最簡短的 .app 文件長這樣(ch_app 應用):
{application, ch_app,[{mod, {ch_app,[]}}]}.
mod 定義了應用的回調模塊(ch_app)和啟動參數([]),應用啟動時會調用:
ch_app:start(normal, [])
應用終止后會調用:
ch_app:stop([])
當使用 Erlang/OTP 的代碼打包工具 systools(詳見 Releases),還要指定 description、vsn、modules、registered 和 applications:
{application, ch_app,[{description, "Channel allocator"},{vsn, "1"},{modules, [ch_app, ch_sup, ch3]},{registered, [ch3]},{applications, [kernel, stdlib, sasl]},{mod, {ch_app,[]}}]}.
-
description - 簡短的描述,字符串,默認為 ""。
-
vsn - 版本號,字符串,默認為 ""。
-
modules - 應用引入的所有模塊,在生成啟動腳本和 tar 文件的時候 systools 會用到此列表。默認為 [] 。
-
registered - 應用中所有注冊的進程名。systools 會用它來檢測應用間的名字沖突。默認為 [] 。
- applications - 所有必須在此應用啟動前啟動的應用。systools 會用這個列表來生成正確的啟動腳本。默認為 [] 。注意,所有的應用都至少依賴于 Kernel 和 STDLIB 應用。
注意:應用資源文件的語法和內容,詳見Kernel中的app手冊
?
7.4 目錄結構
使用 systools 來打包代碼,每個應用的代碼會放在單獨的目錄下:lib/Application-Vsn,其中 Vsn 是版本號。
即便不用 systools 打包,由于 Erlang 是根據 OTP 原則打包,它會有一個特定的目錄結構。如果應用存在多個版本,code server(詳見 code(3) )會自動使用版本號最高的目錄的代碼。
開發環境的目錄結構準則
只要發布環境的目錄結構遵循規定,開發目錄結構怎么樣都行,但還是建議在開發環境中使用相同的目錄結構。目錄名中的版本號要略掉,因為版本是發布步驟的一部分。
有些子目錄是必須的。有些子目錄是可選的,應用需要才有。還有些子目錄是推薦有的,也就是說建議您按下面說的使用它。例如,文檔 doc 和測試 test 目錄是建議在應用中包含的,以成為一個合格的 OTP 應用。
─ ${application}├── doc│ ├── internal│ ├── examples│ └── src├── include├── priv├── src│ └── ${application}.app.src└── test
- src - 必須。容納 Erlang 源碼、.app 文件和應用內部使用的 include 文件。src 中可以創建子目錄用以組織源文件。子目錄不能超過一層。
- priv - 可選。存放應用相關的文件。
- include - 可選。存放能被其他應用訪問到的 include 文件。
- doc - 推薦。所有的源文檔應放在此目錄的子目錄下。
- doc/internal - 推薦。應用的實現細節(不對外)相關文檔放在此處。
- doc/examples - 推薦。存放示例源碼。建議大家把對外文檔的示例放在此處。
- doc/src - 推薦。存放所有的文檔源文件(包括Markdown、AsciiDoc 和 XML 文件)。
- test - 推薦。存放測試相關的所有文件,包括測試規范和測試集等。
開發環境可能還需要其他文件夾。例如,如果有其他語言的源碼,比如說 C 語言寫的 NIF,應該把它們放在其他目錄。按照慣例,應該以語言名為前綴命名目錄,比如說 C 語言用 c_src,Java 用 java_src,Go 用 go_src 。后綴 _src 意味著這個文件夾里的文件是編譯和應用步驟中的一部分。最終構建好的文件應放在 priv/lib 或 priv/bin 目錄下。
priv 目錄存放應用運行時需要的資源。可執行文件應放在 priv/bin 目錄,動態鏈接應放在 priv/bin 目錄。其他資源可以隨意放在 priv 目錄下,不過最好用結構化的方式組織。
生成 erlang 代碼的其他語言代碼,比如 ASN.1 和 Mibs,應該放在頂層目錄或 src 目錄的子目錄中,子目錄以語言名命名(如 asn1 和 mibs)。構建文件應放在相應的語言目錄下,比如 erlang 對應 src 目錄,java 對應 java_src 目錄。
開發環境的 .app 文件可能放在 ebin 目錄下,不過建議在構建時再把它放過去。慣常做法是使用 .app.src 文件,存放在 src 目錄。.app.src 文件和 .app 文件基本上是一樣的,只是某些字段會在構建階段被替換,比如應用版本號。
目錄名不應該用大寫字母。
建議刪掉空目錄。
發布環境的目錄結構
?應用的發布版必須遵循特定的目錄結構。
─ ${application}-${version}├── bin├── doc│ ├── html│ ├── man[1-9]│ ├── pdf│ ├── internal│ └── examples├── ebin│ └── ${application}.app├── include├── priv│ ├── lib│ └── bin└── src
- src - 可選。容納 Erlang 源碼、.app 文件和應用內部使用的 include 文件。發布版本中不必要用到。
- ebin - 必須。包含 Erlang 目標代碼 beam 文件,.app 文件也必須要放在這里。
- priv - 可選。存放應用相關的文件,可用 code:priv_dir/1 函數訪問此目錄。
- priv/lib - 推薦。存放應用需要用到的共享對象( shared-object )文件,比如 NIF 或 linked-in-driver 。
- priv/bin - 推薦。存放應用需要用到的可執行文件,例如 port-program 。
- include - 可選。存放能被其他應用訪問到的 include 文件。
- bin - 可選。存放應用生成的可執行文件,比如 escript 或 shell-script。
- doc - 可選。存放發布文檔。
- doc/man1 - 推薦。存放應用可執行文件的幫助文檔。
- doc/man3 - 推薦。存放模塊 API 的幫助文檔。
- doc/man6 - 推薦。存放應用概述幫助文檔。
- doc/html - 可選。存放應用的 html 文檔。
- doc/pdf - 可選。存放應用的 pdf 文檔。
src 目錄可用于 debug,但不是必須有的。include 目錄只有在應用有公開的 include 文件時會用到。
推薦大家以上面的方式發布幫助文檔(doc/man...),一般 HTML 和 PDF 會以其他方式發布。
建議刪掉空目錄。
?
7.5 應用控制器(application controller)
當 erlang 運行時系統啟動,Kernel 應用會啟動很多進程,其中一個進程是應用控制器(application controller)進程,注冊名為 application_controller 。
應用的所有操作都是通過控制器來協調的。它使用了 application 模塊的一些函數,詳見 application 模塊的文檔。它控制應用的加載、卸載、啟動和終止。
?
7.6 加載和卸載應用
應用啟動前,一定要先加載它。控制器會讀取并存儲 .app 文件中的信息:
1> application:load(ch_app). ok 2> application:loaded_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"},{stdlib,"ERTS CXC 138 10","1.11.4.3"},{ch_app,"Channel allocator","1"}]
終止或者未啟動的應用可以被卸載。卸載時,應用的信息會從控制器的內部數據庫中清除:
3> application:unload(ch_app). ok 4> application:loaded_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"},{stdlib,"ERTS CXC 138 10","1.11.4.3"}]
注意:加載或卸載應用不會加載或卸載應用的代碼。代碼加載是以平時的方式處理的。
?
7.7 啟動和終止應用
啟動應用:
5> application:start(ch_app). ok 6> application:which_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"},{stdlib,"ERTS CXC 138 10","1.11.4.3"},{ch_app,"Channel allocator","1"}]
如果應用沒被加載,控制器會先調用 application:load/1 來加載它。它校驗 applications 的值,確保這個配置中的所有應用在此應用運行前都已經啟動了。
然后控制器為應用創建一個 application master 。這個 master 是應用中所有進程的組長。master 通過調用應用回調函數 start/2 來啟動應用,應用回調由 mod 配置指定。
調用下面的函數,應用會被終止,但不會被卸載:
7> application:stop(ch_app). ok
master 通過 shutdown 頂層 supervisor 來終止應用。頂層 supervisor 通知它所有的子進程終止,層層下推,整個監控樹會以與啟動相反的順序終止。然后 master 會調用回調函數 stop/1(mod 配置指定的應用回調模塊)。
?
7.8 配置應用
可以通過配置參數來配置應用。配置參數就是 .app 文件中的 env 字段對應的一個 {Par,Val} 列表:
{application, ch_app,[{description, "Channel allocator"},{vsn, "1"},{modules, [ch_app, ch_sup, ch3]},{registered, [ch3]},{applications, [kernel, stdlib, sasl]},{mod, {ch_app,[]}},{env, [{file, "/usr/local/log"}]}]}.
其中 Par 必須是一個 atom,Val 可以是任意類型。可以調用 application:get_env(App, Par) 來獲取配置參數,還有一組類似函數,詳見 Kernel 模塊的 application 手冊。
例:
% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"/usr/local/log"}
.app 文件中的配置值會被系統配置文件中的配置覆蓋。配置文件包含了相關應用的配置參數:
[{Application1, [{Par11,Val11},...]},...,{ApplicationN, [{ParN1,ValN1},...]}].
系統配置文件名為 Name.config,erlang 啟動時可通過命令行參數 -config Name 來指定配置文件。詳見 Kernel 模塊的 config 文檔。
例:
文件 test.config 內容如下:
[{ch_app, [{file, "testlog"}]}].
file 的值會覆蓋 .app 文件中 file 對應的值:
% erl -config test Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"testlog"}
如果使用 release handling ,只能使用一個系統配置文件:sys.config 。
.app 文件和系統配置文件中的值都會被命令行中指定的值覆蓋:
% erl -ApplName Par1 Val1 ... ParN ValN
例:
% erl -ch_app file '"testlog"' Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"testlog"}
?
7.9 應用啟動類型
啟動類型在應用啟動時指定:
application:start(Application, Type)
application:start(Application) 相當于 application:start(Application, temporary) 。Type 還可以是 permanent 和 transient:
- permanent 意味著應用終止的時候,其他所有應用以及運行時系統都會終止。
- transient 應用如果終止理由是 normal,會有終止報告,但不會終止其他應用。如果 transient 應用異常終止(理由不是 normal),那么其他所有應用以及運行時系統都會終止。
- temporary 應用終止,只會有終止報告,其他應用不會終止。
通過調用 application:stop/1 可以顯式地終止一個應用,不管啟動類型是什么,其他應用都不會被影響。
transient 模式基本沒什么用,因為當監控樹退出,終止理由會是 shutdown 而不是 normal 。
?
8?Included Applications
8.1 引言
應用可以 include(譯作包含) 其他應用。被包含的應用(included application)有自己的應用目錄和 .app 文件,不過它是另一個應用的監控樹的一部分。
應用不能被多個應用包含。
被包含的應用可以包含其他應用。
沒有被任何應用包含的應用被稱為原初應用(primary application)。
圖8.1 原初應用和被包含的應用
應用控制器會在加載原初應用時,自動加載被包含的應用,但是不會啟動它們。被包含的應用頂層 supervisor 必須由包含它的應用的 supervisor 啟動。
也就是說運行時,被包含的應用實際上是原初應用的一部分,被包含應用中的進程會認為自己歸屬于原初應用。
?
8.2 指定被包含的應用
要包含哪些應用,是在 .app 文件的 included_applications 中指定的:
{application, prim_app,[{description, "Tree application"},{vsn, "1"},{modules, [prim_app_cb, prim_app_sup, prim_app_server]},{registered, [prim_app_server]},{included_applications, [incl_app]},{applications, [kernel, stdlib, sasl]},{mod, {prim_app_cb,[]}},{env, [{file, "/usr/local/log"}]}]}.
?
8.3 啟動時同步
被包含應用的監控樹,是包含它的應用的監控樹的一部分。如果需要在兩個應用間做同步,可以通過 start phase 來實現。
Start phase 是由 .app 文件中的 start_phases 字段指定的,它是一個 {Phase,PhaseArgs} 列表,其中 Phase 是一個 atom,PhaseArgs 可以是任何類型。
包含其他應用時,mod 字段必須為 {application_starter,[Module,StartArgs]}。其中 Module 是應用回調模塊,StartArgs 是傳遞給 Module:start/2 的參數:
{application, prim_app,[{description, "Tree application"},{vsn, "1"},{modules, [prim_app_cb, prim_app_sup, prim_app_server]},{registered, [prim_app_server]},{included_applications, [incl_app]},{start_phases, [{init,[]}, {go,[]}]},{applications, [kernel, stdlib, sasl]},{mod, {application_starter,[prim_app_cb,[]]}},{env, [{file, "/usr/local/log"}]}]}.{application, incl_app,[{description, "Included application"},{vsn, "1"},{modules, [incl_app_cb, incl_app_sup, incl_app_server]},{registered, []},{start_phases, [{go,[]}]},{applications, [kernel, stdlib, sasl]},{mod, {incl_app_cb,[]}}]}.
啟動包含了其他應用的原初應用,跟正常啟動應用是一樣的,也就是說:
- 應用控制器為應用創建 application master 。
- master 調用 Module:start(normal, StartArgs) 啟動頂層 supervisor 。
然后,原初應用和被包含應用按照從上到下從左到右的順序,master 依次為它們 start phase 。對每個應用,master 按照原初應用中指定的 phase 順序依次調用 Module:start_phase(Phase, Type, PhaseArgs) ,其中當前應用的 start_phases 中未指定的 phase 會被忽略。
被包含應用的 .app 文件需要如下內容:
- {mod, {Module,StartArgs}} 項必須有。這個選項指定了應用的回調模塊。StartArgs 會被忽略,因為只有原初應用會調用 Module:start/2 。
- 如果被包含的應用本身包含了其他應用,則需要使用 {mod, {application_starter, [Module,StartArgs]}} 。
- {start_phases, [{Phase,PhaseArgs}]} 字段必須要有,并且這個列表必須是原初應用指定的 Phase 的子集。
啟動上文定義的 prim_app 時,在 application:start(prim_app) 返回之前,應用控制器會調用下面的回調:
application:start(prim_app)=> prim_app_cb:start(normal, [])=> prim_app_cb:start_phase(init, normal, [])=> prim_app_cb:start_phase(go, normal, [])=> incl_app_cb:start_phase(go, normal, []) ok
9?Distributed Applications
9.1 引言
在擁有多個節點的分布式系統中,有必要以分布式的方式來管理應用。如果某應用所在的節點崩潰,則在另一個節點重啟這個應用。
這樣的應用被稱為分布式應用。注意,分布式指的是應用的“管理”。如果從跨節點使用服務的角度來說,所有的應用都能分布式。
分布式的應用可以在節點間遷移,所以需要尋址機制來確保不管它在哪個節點都能被其他應用訪問到。這個問題不在此討論,可通過 Kernel 應用的 global 和? pg2 模塊的某些功能來實現。
?
9.2 配置分布式應用
分布式的應用受兩個東西控制,應用控制器(application_controller)和分布式應用控制進程(dist_ac)。這兩個都是 Kernel 應用的一部分。所以分布式應用是通過配置 Kernel 應用來指定的,可以使用下面的配置參數(詳見 kernel 文檔):
distributed = [{Application, [Timeout,] NodeDesc}]
- 指定了應用 Application = atom() 能在哪里運行。
- NodeDesc = [Node | {Node,...,Node}] 是一個節點名列表,按優先級排列。元組 {} 中的節點沒有先后順序。
- Timeout = integer() 指定了等待多少毫秒后在其他節點上重啟應用。默認為0。
為了正確地管理分布式應用,可運行應用的節點必須互相連接,協商應用在哪里啟動。可在 Kernel 中使用下面的配置參數:
- sync_nodes_mandatory = [Node] - 指定了必須啟動的其他節點(在 sync_nodes_timeout 指定的時間內)。
- sync_nodes_optional = [Node] - 指定了可以啟動的其他節點(在 sync_nodes_timeout 指定的時間內)。
- sync_nodes_timeout = integer() | infinity- 指定了等待其他節點啟動的超時時長,單位毫秒。
節點啟動時會等待所有 sync_nodes_mandatory? 和 sync_nodes_optional 中的節點啟動。如果所有節點都啟動了,或必須啟動的節點啟動了,sync_nodes_timeout 時長后所有的應用會被啟動。如果有必須的節點沒啟動,當前節點會終止。
例:
應用 myapp 在 cp1@cave 中運行。如果此節點終止,myapp 將在 cp2@cave 或 cp3@cave 節點上重啟。cp1@cave 的系統配置 cp1.config 如下:
[{kernel,[{distributed, [{myapp, 5000, [cp1@cave, {cp2@cave, cp3@cave}]}]},{sync_nodes_mandatory, [cp2@cave, cp3@cave]},{sync_nodes_timeout, 5000}]} ].
cp2@cave 和 cp3@cave 的系統配置也是一樣的,除了必須啟動的節點分別是 [cp1@cave, cp3@cave] 和 [cp1@cave, cp2@cave] 。
注意:所有節點的 distributed 和 sync_nodes_timeout 值必須一致,否則該系統行為不會被定義。
?
9.3 啟動和終止分布式應用
當所有涉及(必須啟動)的節點被啟動,在所有這些節點中調用 application:start(Application) 就能啟動這個分布式應用。
可以用引導腳本(Releases)來自動啟動應用。
應用將在參數 distributed 配置的節點列表中的第一個可用節點啟動。和平常啟動應用一樣,創建了一個 application master,調用回調:
Module:start(normal, StartArgs)
例:
繼續上一小節的例子,啟動了三個節點,指定系統配置文件:
> erl -sname cp1 -config cp1 > erl -sname cp2 -config cp2 > erl -sname cp3 -config cp3
所有節點可用時,myapp 會被啟動。所有節點中調用 application:start(myapp) 即可。此時它會在 cp1 中啟動,如下圖所示:
圖9.1:應用 myapp - 情況 1
同樣地,在所有的節點中調用 application:stop(Application) 將終止應用。
?
9.4 故障切換
如果應用所在的節點終止,指定的超時時長后,應用將在 distributed 配置中指定的第一個可用節點中重啟。這就是故障切換。
應用在新節點中和平常一樣啟動,application master 調用:
Module:start(normal, StartArgs)
有一個例外,如果應用指定了 start_phases(詳見Included Applications),應用將這樣重啟:
Module:start({failover, Node}, StartArgs)
其中 Node 為終止的節點。
例:
如果 cp1 終止,系統會等待 cp1 重啟5秒,超時后在 cp2 和 cp3 中選擇一個運行的應用最少的。如果 cp1 沒有重啟,且 cp2 運行的應用比 cp3 少,myapp 將會 cp2 節點重啟。
圖9.2:應用 myapp - 情況 2
?假設 cp2 也崩潰了,并且5秒內沒有重啟。myapp 將在 cp3 重啟。
圖9.3:應用 myapp - 情況 3
?
9.5? 接管
如果一個在 distributed 配置中優先級較高的節點啟動,應用會在新節點重啟,在舊節點結束。這就是接管。
應用會通過如下方式啟動:
Module:start({takeover, Node}, StartArgs)
其中 Node 表示舊節點。
例:
如果 myapp 在 cp3 節點運行,此時 cp2 啟動,應用不會被重啟,因為 cp2 和 cp3 是沒有先后順序的。
圖9.4:應用 myapp - 情況 4
但如果 cp1 也重啟了,函數 application:takeover/2 會將 myapp 移動到 cp1,因為對 myapp 來說 cp1 比 cp3 優先級高。此時節點 cp1 會調用 Module:start({takeover, cp3@cave}, StartArgs) 來啟動應用。
圖9.5:應用 myapp - 情況 5
?
10?Releases
此章應與 SASL 部分的 rel、systemtools、script 教程一起閱讀。
10.1 概念
當你寫了一個或多個應用,你可能想用這些應用加 Erlang/OTP 應用的子集創建一個完整的系統。這就是 release 。
首先要創建一個 release 源文件,文件中指定了 release 所包含的應用。
此文件用于生成啟動腳本和 release 包。可移動和安裝到另一個地址的系統被稱為目標系統。系統原則(System Principles)中講了如何用 release 包創建目標系統。
?
10.2 Release 源文件
創建 release 源文件來描述一個 release,簡稱 .rel 文件。文件中指定了 release 的名字和版本號,它基于哪個版本的 ERTS,以及它由哪些應用組成:
{release, {Name,Vsn}, {erts, EVsn},[{Application1, AppVsn1},...{ApplicationN, AppVsnN}]}.
Name、Vsn、EVsn 和 AppVsn 都是字符串(string)。
文件名必須為 Rel.rel ,其中 Rel 是唯一的名字。
?Application (atom) 和 AppVsn 是 release 中各應用的名字和版本號。基于 Erlang/OTP 的最小的 release 由 Kernel 和 STDLIB 應用組成,這兩個應用一定要在應用列表中。
要升級 release 的話,還必須包含 SASL 應用。
例:Applications 章中的 ch_app 的 release 中有下面的 .app 文件:
{application, ch_app,[{description, "Channel allocator"},{vsn, "1"},{modules, [ch_app, ch_sup, ch3]},{registered, [ch3]},{applications, [kernel, stdlib, sasl]},{mod, {ch_app,[]}}]}.
?.rel 文件必須包含 kernel、stdlib 和 sasl,因為 ch_app 要用到這些應用。文件名 ch_rel-1.rel :
{release,{"ch_rel", "A"},{erts, "5.3"},[{kernel, "2.9"},{stdlib, "1.12"},{sasl, "1.10"},{ch_app, "1"}] }.
?
10.3 生成啟動腳本
SASL 應用的 systools 模塊包含了構建和檢查 release 的工具。這些函數讀取 .rel 和 .app 文件,執行語法和依賴檢測。用 systools:make_script/1,2 來生成啟動腳本(詳見 System Principles):
1> systools:make_script("ch_rel-1", [local]).
ok
這個會創建啟動腳本,可讀版本 ch_rel-1.script 和運行時系統用到的二進制版本 ch_rel-1.boot。
- ch_rel-1 是 .rel 文件的名字去掉擴展名。
- local 是個附加選項,意思是在啟動腳本中使用應用所在的目錄,而不是 $ROOT/lib($ROOT 是安裝后的 release 的根目錄)。
這在本地測試生成啟動腳本時有用處。
使用啟動腳本來啟動? Erlang/OTP 時,會自動加載和啟動 .rel 文件中所有的應用:
% erl -boot ch_rel-1 Erlang (BEAM) emulator version 5.3Eshell V5.3 (abort with ^G) 1> =PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===supervisor: {local,sasl_safe_sup}started: [{pid,<0.33.0>},{name,alarm_handler},{mfa,{alarm_handler,start_link,[]}},{restart_type,permanent},{shutdown,2000},{child_type,worker}]...=PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===application: saslstarted_at: nonode@nohost... =PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===application: ch_appstarted_at: nonode@nohost
?
10.4 創建 release 包
systools:make_tar/1,2? 函數以 .rel 文件作為輸入,輸出一個 zip 壓縮的 tar 文件,文件中包含指定應用的代碼,即 release 包:
1> systools:make_script("ch_rel-1"). ok 2> systools:make_tar("ch_rel-1"). ok
一個 release 包默認包含:
- .app 文件
- .rel 文件
- 所有應用的目標代碼,代碼根據應用目錄結構組織
- 二進制啟動腳本,重命名為 start.boot
% tar tf ch_rel-1.tar lib/kernel-2.9/ebin/kernel.app lib/kernel-2.9/ebin/application.beam ... lib/stdlib-1.12/ebin/stdlib.app lib/stdlib-1.12/ebin/beam_lib.beam ... lib/sasl-1.10/ebin/sasl.app lib/sasl-1.10/ebin/sasl.beam ... lib/ch_app-1/ebin/ch_app.app lib/ch_app-1/ebin/ch_app.beam lib/ch_app-1/ebin/ch_sup.beam lib/ch_app-1/ebin/ch3.beam releases/A/start.boot releases/A/ch_rel-1.rel releases/ch_rel-1.rel
Release 包生成前,生成了一個新的啟動腳本(不使用 local 選項)。在 release 包中,所有的應用目錄都放在 lib 目錄下。由于不知道 release 包會發布到哪里,所以不能寫死絕對路徑。
在 tar 文件中有兩個一樣的 rel 文件。最初這個文件只放在 releases 目錄下,這樣 release_handler 就能單獨提取這個文件。解壓 tar 文件后,release_handler 會自動把它拷貝到 releases/FIRST 目錄。但是有時 tar 文件解包時沒有 release_handler 參與(比如解壓第一個目標系統),所以改為在 tar 文件中有兩份,不需要再手動拷貝。
包里面還可能有 relup 文件和系統配置文件 sys.config,這些文件也會在 release 包中包含。詳見 Release Handling 。
?
10.5 目錄結構
release_handler 從 release 包安裝的代碼目錄結構如下:
$ROOT/lib/App1-AVsn1/ebin/priv/App2-AVsn2/ebin/priv.../AppN-AVsnN/ebin/priv/erts-EVsn/bin/releases/Vsn/bin
- lib - 應用目錄
- erts-EVsn/bin - Erlang 運行時系統可執行文件
- releases/Vsn - .rel 文件和啟動文件 start.boot。relup 和 sys.config 也在此目錄下
- bin - 最上層的 Erlang 運行時系統可執行文件
應用不一定要放在 $ROOT/lib 目錄。因此可以有多個安裝目錄,包含系統的不同部分。例如,上面的例子可以拓展成:
$SECOND_ROOT/.../SApp1-SAVsn1/ebin/priv/SApp2-SAVsn2/ebin/priv.../SAppN-SAVsnN/ebin/priv$THIRD_ROOT/TApp1-TAVsn1/ebin/priv/TApp2-TAVsn2/ebin/priv.../TAppN-TAVsnN/ebin/priv
$SECOND_ROOT 和 $THIRD_ROOT 在調用 systools:make_script/2 函數時作為參數傳入。
無磁盤或只讀客戶端
如果系統由無磁盤的或只讀的客戶端節點組成,$ROOT 目錄中還會有一個 clients 目錄。只讀的節點就是節點在一個只讀文件系統中。
每個客戶端節點在 clients 中有一個子目錄。每個子目錄的名字是對應的節點名。一個客戶端目錄至少包含 bin 和 releases 兩個子目錄。這些目錄用來存放 release 的信息,以及把當前 release 指派給客戶端。$ROOT 目錄如下所示:
$ROOT/.../clients/ClientName1/bin/releases/Vsn/ClientName2/bin/releases/Vsn.../ClientNameN/bin/releases/Vsn
這個結構用于所有客戶端都運行在同類型的 Erlang 虛擬機上。如果有不同類型的 Erlang 虛擬機,或者在不同的操作系統中,可以把 clients 分成每個類型一個子目錄。或者每個類型設置一個 $ROOT。此時 $ROOT 目錄相關的一些子目錄都需要包含進來:
$ROOT/.../clients/Type1/lib/erts-EVsn/bin/ClientName1/bin/releases/Vsn/ClientName2/bin/releases/Vsn.../ClientNameN/bin/releases/Vsn.../TypeN/lib/erts-EVsn/bin...
這個結構中,Type1 的客戶端的根目錄為 $ROOT/clients/Type1 。
?
11?Release Handling
11.1 Relase 管理原則
Erlang 的一個重要特點就是可以在運行時改變模塊代碼,即 Erlang Reference Manual(參考手冊)中說的代碼替換。
基于這個特點,OTP 應用 SASL 提供在運行時升級和降級整個 release 的框架。這就是 release 管理。
這個框架包含:
- 線下支持 - 用 systools 模塊生成腳本和創建 release 包
- 線上支持 - 用 release_handler 打包和安裝 release 包
包含 release 管理的基于 Erlang/OTP 的最小的系統,由 Kernel、STDLIB 和 SASL 應用組成。
Release 管理工作流
步驟 1:按 Releases 章所述創建一個 release。
步驟 2:在目標環境中安裝 release 。如何安裝第一個目標系統,詳見 System Principles 文檔。
步驟 3:在開發環境中修改代碼(比如錯誤修復)。
步驟 4:某個時間點,需要創建新版本 release 。更新相關的 .app 文件,創建 .rel 文件。
步驟 5:為每個修改的應用,創建 .appup 文件(應用升級文件)。該文件描述了怎么在應用的新舊版本間升降級。
步驟 6:基于 .appup 文件,創建 relup 文件 (release 升級文件)。該文件描述了怎么在整個 release 的新舊版本間升降級。
步驟 7:創建一個新的 release 包,放到目標系統上。
步驟 8:使用 release handler 解包。
步驟 9:使用 release handler 安裝新版 release 包。執行 relup 文件中的指令:添加、刪除或重新加載模塊,啟動、終止或重啟應用,等等。有時需要重啟整個模擬器。
- 如果安裝失敗,系統會被重啟。默認使用舊版本 release 。
- 如果安裝成功,新版本會變為默認版本,系統重啟時會使用新版本。
Release 管理特性
Appup Cookbook 章中有 .appup 文件的示例,包含了典型的運行時系統可以輕松處理的案例。然而有些情況下 release 管理會很復雜,例如:
- 復雜或者環形的依賴關系會讓事情變得很復雜,很難決定以什么順序執行才能不引起系統錯誤。依賴可能存在于:
- 節點之間
- 進程之間
- 模塊之間
- 在 release 管理過程中,不受影響的進程會繼續正常執行。這可能導致超時或其他問題。例如,掛起使用某模塊的進程,到加載該模塊新版本的過程中,創建的新進程可能執行舊代碼。
所以建議代碼做盡可能小的改動,永遠保持向后兼容。
?
11.2 必要條件
為了正確地執行 release 管理,運行時系統必須知道當前運行哪個 release 。必須能在運行時,改變重啟時要用哪個啟動腳本和系統配置文件,使其崩潰時還能生效。所以,Erlang 必須以嵌入式系統方式啟動,詳見 Embedded System 文檔。
為了系統重啟順利,系統啟動時必須啟動心跳監測,詳見 ERTS 部分的 erl 手冊和 Kernel 部分的 heart(3) 手冊。
其他必要條件:
- Release 包中的啟動腳本必須和 release 包從同一個 .rel 文件中生成。升降級時,應用信息從該腳本中獲取。
- 系統只能有一個系統配置文件 sys.config 。如果文件存在,創建 release 包時會自動包含進來。
- 所有版本的 release(除了第一個),必須包含 relup 文件。如果文件存在,創建 release 包時會自動包含進來。
?
11.3 分布式系統
如果系統由多個節點組成,每個節點可以擁有自己的 release 。release_handler 是一個本地注冊的進程,升降級時只能在節點中調用。Release 管理指令 sync_nodes 可以用來同步多個節點的 release 管理進程,詳見 SASL 的 appup(4) 手冊。
?
11.4 Release 管理指令
OTP 支持一系列 Release 管理指令,在創建 appup 文件時會用到。release_handler 能理解其中一部分,低級指令。還有一些高級指令,是為了用戶方便而設計的,調用 systools:make_relup 時會被轉化成低級指令。
此節描述了最常用的指令。完整的指令列表可見 SASL 的 appup(4) 手冊。
首先,給出一些定義:
- Residence module(駐地模塊) - 模塊中有進程的尾遞歸循環函數(進程 loop 所在)。如果多個模塊有這些函數,所有這些模塊都是這個進程的 residence 模塊。
- Functional module(功能模塊) - 不是任何進程的 residence 模塊的模塊。
對一個 OTP behaviour 實現的進程來說,behaviour 模塊就是它的駐地模塊,回調模塊就是功能模塊。
load_module
如果模塊做了簡單的擴展,加載模塊的新版本并移除舊版本就行了。這就是簡單的代碼替換,使用如下指令即可:
{load_module, Module}
update
如果有復雜的修改,比如改了 gen_server 的內部狀態格式,簡單的代碼替換就不夠了。必須做到:
- 掛起使用該模塊的進程(避免它們在代碼替換完成前處理請求)。
- 要求進程修改內部狀態格式,并切換到新版本代碼。
- 移除舊代碼。
- 恢復進程。
這個就是同步代碼替換,使用如下指令:
{update, Module, {advanced, Extra}}
{update, Module, supervisor}
當要改變上述 behaviour 的內部狀態時,使用 {advanced,Extra} 。它會導致進程調用回調函數 code_change,傳遞 Extra 和一些其他信息作為參數。詳見對應 behaviour 和 Appup Cookbook 。
改變監程的啟動規格時使用 supervisor 參數。詳見 Appup Cookbook 。
當模塊更新時,release_handler 會遍歷各應用的監控樹,檢查所有的子進程規格,找到用到該模塊的進程:
{Id, StartFunc, Restart, Shutdown, Type, Modules}
進程用到了某模塊,意思就是該模塊在子進程規格的 Modules 列表中。
如果 Modules=dynamic,如事件管理器,則事件管理器會通知 release_handler 當前安裝的事件處理器列表(gen_event),它會檢測這個列表的模塊名。
release_handler 通過 sys:suspend/1,2 、sys:change_code/4,5 和 sys:resume/1,2 來掛起、要求切換代碼以及恢復進程。
add_module 和 delete_module
使用下列指令引入新模塊:
{add_module, Module}
?這條指令加載了新模塊,在嵌入模式運行 Erlang 時必須使用它。交互模式下可以不使用這條指令,因為代碼服務器會自動搜尋和加載未加載的模塊。
delete_module 與 add_module 相反,它能卸載模塊:
{delete_module, Module}
當這條指令執行時,以 Module 為駐地模塊的所有進程都會被殺死。用戶必須保證在卸載模塊前,所有涉及進程都終止,以避免無謂的 supervisor 重啟。
應用指令
添加應用:
{add_application, Application}
添加一個應用,會先用 add_module 指令加載所有 .app 文件中 modules 字段所列模塊,然后啟動應用。
移除應用:
{remove_application, Application}
移除應用會終止應用,并且使用 delete_module 指令卸載模塊,最后會從應用控制器卸載應用的規格信息。
重啟應用:
{restart_application, Application}
重啟應用會先終止應用再啟動應用,相當于連續使用 remove_application 和 add_application 。
apply (低級指令)
讓 release_handler 調用任意函數:
{apply, {M, F, A}}
release_handler 會執行 apply(M, F, A) 。
restart_new_emulator (低級指令)
這條指令用于改變模擬器版本,或者升級核心應用 Kernel、STDLIB 或 SASL 。如果因為某種原因需要系統重啟,則應該使用 restart_emulator 指令。
這條指令要求系統啟動時必須啟動心跳監測,詳見 ERTS 部分的 erl 手冊和 Kernel 部分的 heart(3) 手冊。
restart_new_emulator 必須是 relup 文件的第一條指令,如果使用 systools:make_relup/3,4 生成 relup 文件,會默認放在最前面。
當 release_handler 執行這條命令,它會先生成一個臨時的啟動文件,文件指定新版本的模擬器和核心應用以及舊版本的其他應用。然后它調用 init:reboot()(詳見 Kernel 的 init(3) 手冊)關閉當前模擬器。所有進程優雅地終止,然后 heart 程序使用臨時啟動文件重啟系統。重啟后,會執行其他的 relup 指令,這個過程定義在臨時啟動文件中。
?警告:這個機制會在啟動時使用新版本的模擬器和核心應用,但是其他應用仍是舊版本。所以要額外注意兼容問題。有時核心應用中會做不兼容的修改。如果可能,新舊代碼先共存于一個 release,線上更新完成后再在此后的新 release 棄用舊代碼。為了保證應用不會因為不兼容的修改而崩潰,應盡可能早地停止調用棄用函數。?
升級完成會寫一條 info 報告。可以通過調用 release_handler:which_releases(current) ,檢查它是否返回預期的新的 release 。
當新模擬器可操作時,必須持久化新的 release 版本。否則系統重啟時仍會使用舊版。
在 UNIX 系統中,release_handler 會告訴 heart 程序使用哪條命令來重啟系統。此時 heart 程序使用的環境變量 HEART_COMMAND 會被忽略,默認命令為 $ROOT/bin/start 。也可以通過使用 SASL 的配置參數 start_prg 來指定其他命令,詳見 sasl(6) 手冊。
restart_emulator (低級命令)
這條命令不用于 ERTS 或核心應用的升級。在所有升級指令執行完后,可以用它來強制重啟模擬器。
relup 文件只能有一個 restart_emulator 指令,且必須放在最后。如果使用 systools:make_relup/3,4 生成 relup 文件,會默認放在最后。
當 release_handler 執行這條命令,它會調用 init:reboot()(詳見 Kernel 的 init(3) 手冊)關閉當前模擬器。所有進程優雅地終止,然后 heart 程序使用新版 release 來重啟系統。重啟后不會執行其他升級指令。
?
11.5 應用升級文件
創建應用升級文件來指定如何在當前版本和舊版本應用之間升降級,簡稱 .appup 文件。文件名為 Application.appup ,其中 Application 是應用名:
{Vsn,[{UpFromVsn1, InstructionsU1},...,{UpFromVsnK, InstructionsUK}],[{DownToVsn1, InstructionsD1},...,{DownToVsnK, InstructionsDK}]}.
- Vsn - 字符串,當前應用版本號( .app 文件中的版本號)。
- UpFromVsn - 升級前的版本號。
- DownToVsn - 要降級至的版本號。
- Instructions - release 管理指令列表。
.appup 文件的語法和內容,詳見 SASL 的 appup(4) 手冊。
Appup Cookbook 中有典型案例的 .appup 文件示例。
例:Releases 章中的例子。如果想在 ch3 中添加函數 available/0 ,返回可用 channel 的數量(修改的時候,在原目錄的副本里改,這樣第一版仍然可用):
-module(ch3). -behaviour(gen_server).-export([start_link/0]). -export([alloc/0, free/1]). -export([available/0]). -export([init/1, handle_call/3, handle_cast/2]).start_link() ->gen_server:start_link({local, ch3}, ch3, [], []).alloc() ->gen_server:call(ch3, alloc).free(Ch) ->gen_server:cast(ch3, {free, Ch}).available() ->gen_server:call(ch3, available).init(_Args) ->{ok, channels()}.handle_call(alloc, _From, Chs) ->{Ch, Chs2} = alloc(Chs),{reply, Ch, Chs2}; handle_call(available, _From, Chs) ->N = available(Chs),{reply, N, Chs}.handle_cast({free, Ch}, Chs) ->Chs2 = free(Ch, Chs),{noreply, Chs2}.
創建新版 ch_app.app 文件,修改版本號:
{application, ch_app,[{description, "Channel allocator"},{vsn, "2"},{modules, [ch_app, ch_sup, ch3]},{registered, [ch3]},{applications, [kernel, stdlib, sasl]},{mod, {ch_app,[]}}]}.
要讓 ch_app 從版本 "1" 升到 "2" 或從 "2" 降到 "1",只需要加載對應版本的 ch3 回調即可。在 ebin 目錄創建 ch_app.appup 應用升級文件:
{"2",[{"1", [{load_module, ch3}]}],[{"1", [{load_module, ch3}]}] }.
?
11.6 Release 升級文件
要指定如何在 release 的版本間切換,要創建一個 release 升級文件,簡稱 relup 文件。
可以使用 systools:make_relup/3,4 自動生成此文件,將相關版本的 .rel 文件、.app 文件和 .appup 文件作為輸入。它不包含要增刪哪些應用,哪些應用要升降級。這些指令會從 .appup 文件中獲取,按正確的順序轉化成低級指令列表。
如果 relup 文件很簡單,可以手動創建它。它只包含低級指令。
relup 文件的語法和內容詳見 SASL 的 relup(4) 手冊。
繼續前小節的例子:已經有新版 "2" 的 ch_app 應用以及 .appup 文件。還需新版的 .rel 文件。文件名 ch_rel-2.rel ,release 版本從 "A" 變為 "B":
{release,{"ch_rel", "B"},{erts, "5.3"},[{kernel, "2.9"},{stdlib, "1.12"},{sasl, "1.10"},{ch_app, "2"}] }.
生成 relup 文件:
1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"]).
ok
生成了一個 relup 文件,文件中有從版本 "A" ("ch_rel-1") 升級到版本 "B" ("ch_rel-2") 和從 "B" 降到 "A" 的指令。
新版和舊版的 .app 和 .rel 文件、.appup 文件和新的 .beam 文件都必須在代碼路徑中。代碼路徑可以使用選項 path 來擴展:
1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"], [{path,["../ch_rel-1", "../ch_rel-1/lib/ch_app-1/ebin"]}]). ok
?
11.7 安裝 release
有了一個新版的 release,就可以創建 release 包并放到目標環境中去。
在運行時系統中安裝新版 release 會用到 release handler 。它是 SASL 應用的一個進程,用于處理 release 包的解包、安裝和移除。它通過 release_handler 模塊通訊。詳見 SASL 的 release_handler(3) 手冊。
假設有一個可操作的目標系統,安裝根目錄為 $ROOT,新版 release 包應拷貝到 $ROOT/releases 目錄下。首先,解包。從包中提取文件:
release_handler:unpack_release(ReleaseName) => {ok, Vsn}
?
?
- ReleaseName - release 包的名字,不包含 .tar.gz 后綴。
- Vsn - release 的版本號,包的 .rel 文件中定義的。
release_handler:install_release(Vsn) => {ok, FromVsn, []}
如果安裝過程中有錯誤發生,系統會使用舊版 release 重啟。如果安裝成功,后續系統會用新版本,不過如果系統中途有重啟的話,還是會使用舊版本。
必須把新安裝的 release 持久化才能讓它成為默認版本,讓之前的版本變成舊版本:
release_handler:make_permanent(Vsn) => ok
系統在 $ROOT/releases/RELEASES 和 $ROOT/releases/start_erl.data 中保存版本信息。
從 Vsn 降級到 FromVsn 時,須再次調用 install_release:
release_handler:install_release(FromVsn) => {ok, Vsn, []}
安裝了的但是還沒持久化的 release 可以被移除。移除意味著 release 的信息會被從 $ROOT/releases/RELEASES 中移除。代碼也會被移除,也就是說,新的應用目錄和 $ROOT/releases/Vsn 目錄都會被刪掉。
release_handler:remove_release(Vsn) => ok
繼續前小節的例子
步驟 1)創建 Releases 中的版本 "A" 的目標系統。這回 sys.config 必須包含在 release 包中。如果不需要任何配置,這個文件中為一個空列表:
[].
步驟 2)啟動系統。現實中會以嵌入式系統啟動。不過,使用 erl 和正確的啟動腳本和配置就足以用來舉例說明:
% cd $ROOT % bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys ...
步驟 3)在另一個 Erlang shell,生成啟動腳本,并創建版本 "B" 的 release 包。記得包含 sys.config(可能有變化)和 relup 文件,詳見 Release 升級文件。
1> systools:make_script("ch_rel-2"). ok 2> systools:make_tar("ch_rel-2"). ok
新的 release 包現在包含版本 "2" 的 ch_app 和 relup 文件:
% tar tf ch_rel-2.tar lib/kernel-2.9/ebin/kernel.app lib/kernel-2.9/ebin/application.beam ... lib/stdlib-1.12/ebin/stdlib.app lib/stdlib-1.12/ebin/beam_lib.beam ... lib/sasl-1.10/ebin/sasl.app lib/sasl-1.10/ebin/sasl.beam ... lib/ch_app-2/ebin/ch_app.app lib/ch_app-2/ebin/ch_app.beam lib/ch_app-2/ebin/ch_sup.beam lib/ch_app-2/ebin/ch3.beam releases/B/start.boot releases/B/relup releases/B/sys.config releases/B/ch_rel-2.rel releases/ch_rel-2.rel
步驟 4)拷貝 release 包 ch_rel-2.tar.gz 到 $ROOT/releases 目錄。
步驟 5)在運行的目標系統中,解包:
1> release_handler:unpack_release("ch_rel-2").
{ok,"B"}
新版本應用 ch_app-2 被安裝在 $ROOT/lib 目錄,在 ch_app-1 附近。kernel、stdlib 和 sasl 目錄不受影響,因為它們沒有改變。
$ROOT/releases 下創建了一個新目錄 B,其中包含了 ch_rel-2.rel、start.boot、sys.config 和 relup 。
步驟 6)檢查 ch3:available/0 是否可用:
2> ch3:available().
** exception error: undefined function ch3:available/0
步驟 7)安裝新 release 。$ROOT/releases/B/relup 中的指令會一一被執行,新版 ch3 被加載進來。函數 ch3:available/0 現在可用了:
3> release_handler:install_release("B"). {ok,"A",[]} 4> ch3:available(). 3 5> code:which(ch3). ".../lib/ch_app-2/ebin/ch3.beam" 6> code:which(ch_sup). ".../lib/ch_app-1/ebin/ch_sup.beam"
ch_app 中的進程代碼不變,例如,supervisor 還在執行 ch_app-1 的代碼。
步驟 8)如果目標系統現在重啟,它會重新使用 "A" 版本。要在重啟時使用 "B" 版本,必須持久化:
7> release_handler:make_permanent("B").
ok
?
11.8 更新應用規格
當新版 release 安裝,所有加載的應用規格會自動更新。
注意:新的應用規格從 release 包中的啟動腳本中獲取。所以啟動腳本必須和 release 包從同一個 .rel 文件中生成。
確切地說,應用配置參數會根據下面的內容自動更新(優先級遞增):
- 啟動腳本從新的應用資源文件 App.app 中獲取到的信息。
- 新的 sys.config
- 命令行參數 -App Par Val
也就是說被其他系統配置文件設置的值,以及使用 application:set_env/3 設置的值都會被無視。
當安裝好的 release 被設置為永久時,系統進程 init 會指向新的 sys.config 文件。
安裝后,應用控制器會比較所有運行中的應用的新舊配置參數,并調用回調:
Module:config_change(Changed, New, Removed)
- Module - .app 文件中定義的 mod 字段,應用回調模塊。
- Changed 和 New 是一個 {Par,Val} 列表,包含了所有改變和增加的配置參數。
- Removed 是被刪除的所有配置參數 Par 的列表。
這個函數是可選的,在實現應用回調模塊時可以省略。
?
12?Appup Cookbook
此章包含典型案例的升降級 .appup 文件的例子。
12.1 修改功能模塊
如果功能模塊被修改,例如新加了一個函數或修復了一個 bug,簡單的代碼替換就夠了:
{"2",[{"1", [{load_module, m}]}],[{"1", [{load_module, m}]}] }.
?
12.2 修改駐地模塊
如果系統完全根據 OTP 設計原則來實現,除系統進程和特殊進程外的所有進程,都會駐扎在某個 behavior 中,supervisor、gen_server、gen_fsm、gen_statem 或 gen_event 。這些都屬于 STDLIB 應用,升降級一般來說需要模擬器重啟。
因此 OTP 沒有支持修改駐地模塊,除了一些特殊進程。
?
12.3 修改回調模塊
回調模塊屬于功能模塊,代碼擴展只需要簡單的代碼替換就行。
例:前文 Relase Handling 中的例子,在 ch3 中添加一個函數,ch_app.appup 內容如下:
{"2",[{"1", [{load_module, ch3}]}],[{"1", [{load_module, ch3}]}] }.
OPT 還支持修改 behaviour 進程的內部狀態,詳見下一小節。
?
12.4 修改內部狀態
這種情況下,簡單的代碼替換不能解決問題。在切換到新版回調模塊前,進程必須使用 code_change 回調顯示地修改它的狀態。此時要用到同步代碼替換(譯者補充:同步即需要等待進程作出一些反應)。
例:前文 gen_server Behaviour 中的 gen_server ch3,內部狀態為 Chs,表示可用的 channel 。假設你想增加一個計數器 N,記錄 alloc 請求次數。狀態的格式必須變為 {Chs,N} 。
.appup 文件內容如下:
{"2",[{"1", [{update, ch3, {advanced, []}}]}],[{"1", [{update, ch3, {advanced, []}}]}] }.
update 指令的第三個參數是一個元組 {advanced,Extra} ,意思是在加載新版模塊之前,受影響的進程要先修改狀態。修改狀態是通過讓進程調用 code_change 回調來完成的(詳見 STDLIB 的 gen_server(3) 手冊)。Extra(此例中是 [])會被傳遞到 code_change 函數:
-module(ch3). ... -export([code_change/3]). ... code_change({down, _Vsn}, {Chs, N}, _Extra) ->{ok, Chs}; code_change(_Vsn, Chs, _Extra) ->{ok, {Chs, 0}}.
code_change 第一個參數,如果降級則為 {down,Vsn},升級則為 Vsn 。Vsn 是模塊的“原”版,即升級前的版本。
如果模塊有 vsn 屬性的話,版本即該屬性的值。ch3 沒有這個屬性,所以此時版本號為 beam 文件的校驗和(大整數),此處它不重要被忽略。
ch3 的其他回調也要修改,還要加其他接口函數,不過此處不贅述。
?
12.5 模塊依賴
在模塊中增加了一個接口函數,比如前文 Release Handling 中的,在 ch3 中增加 available/0 。
假設在另一個模塊 m1 會調用此函數。在 release 升級過程中,如果先加載新版 m1,在 ch3 加載前 m1 調用 ch3:available/0 會引發一個 runtime error 。
所以升級時 ch3 必須在 m1 之前加載,降級時則相反。即 m1 依賴于 ch3 。在 release 處理指令中,用 DepMods 元素來表示:
{load_module, Module, DepMods}
{update, Module, {advanced, Extra}, DepMods}
DepMods 是模塊列表,表示 Module 所依賴的模塊。
?例:myapp 應用的模塊 m1 依賴于 ch_app 應用的模塊 ch3,從 "1" 升級到 "2",或從 "2" 降級到 "1" 時:
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 和 ch_app 屬于同一個應用,.appup 文件如下:
{"2",[{"1",[{load_module, ch3},{load_module, m1, [ch3]}]}],[{"1",[{load_module, ch3},{load_module, m1, [ch3]}]}] }.
降級時也是 m1 依賴于 ch3 。systools 能區分升降級,生成正確的 relup 文件,升級時先加載 ch3 再 m1,降級時先加載 m1 再 ch3 。
?
12.6 修改特殊進程的代碼
這種情況下,簡單的代碼替換不能解決問題。加載特殊進程的新版駐地模塊時,進程必須調用它的 loop 函數的全名,來切換至新代碼。此時,必須用同步代碼替換。
注意:用戶自定義的駐地模塊,必須在特殊進程的子進程規格的 Modules 列表中。否則 release_handler 會找不到該進程。
例:前文 sys and proc_lib 中的例子。通過 supervisor 啟動時,子進程規格如下:
{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}.
- 第一個參數是內部狀態 State,從函數 sys:handle_system_msg(Request, From, Parent, Module, Deb, State) 中傳入,sys:handle_system_msg 是特殊進程在收到系統消息時調用的。ch4 的內容狀態是可用的 Chs 集。
- 第二個參數是模塊名( ch4 )。
- 第三個參數是 Vsn 或 {down,Vsn},跟 12.4 小節中 gen_server:code_change/3 的參數一樣。
此例中,只用到了第一個參數,函數僅返回內部狀態。如果代碼只是被擴展,這樣就 ok 了。如果內部狀態改變(類似 12.4 小節),要在這個函數中進行改變,并返回 {ok,Chs2} 。
?
12.7 修改 supervisor
supervisor behaviour 支持修改內部狀態,也就是修改重啟策略、最大重啟頻率以及子進程規格。
可以添加或刪除子進程,不過不是自動處理的。必須在 .appup 中指定。
修改屬性
由于 supervisor 內部狀態有改動,必須使用同步代碼替換。需要一個特殊的 update 指令。
首先,加載新版回調模塊(升或降)。然后檢測 init/1 的新的返回值,并據此修改內部狀態。
supervisor 的升級指令如下:
{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 字段,會影響 release_handler 進程自身,因為這個字段用于在同步代碼替換中,確認哪些進程收到影響。
增加和刪除子進程
如前文所說,修改子進程規格,不影響現有子進程。新的規格會自動添加,但是不會刪除廢棄規格。子進程不會自動重啟或終止,必須使用 apply 指令來操作。
例:假設從 "1" 升到 "2" 時, ch_sup 增加了一個子進程 m1。降級時 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}]}] }.
指令的順序很重要。
supervisor 必須被注冊為 ch_sup 才能讓腳本生效。如果沒有注冊,不能從腳本中直接訪問它。此時必須寫一個幫助函數來尋找 supervisor 的 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、改變 supervisor 的子進程規格。降級時,子進程必須在規格改變和模塊被刪除前終止。
?
12.8 增加或刪除模塊
例:應用 ch_app 增加了一個新的功能模塊:
{"2",[{"1", [{add_module, m}]}],[{"1", [{delete_module, m}]}]
?
12.9 啟動或終止進程
一個根據 OTP 設計原則組織的系統中,所有的進程都是某 supervisor 的子進程,詳見增加和刪除子進程。
?
12.10 增加或移除應用
增加或移除應用時,不需要 .appup 文件。生成 relup 文件時,會比較 .rel 文件,并自動添加 add_application 和 remove_application 指令。
?
12.11 重啟應用
當修改太復雜時(如監控樹層級重構),可以重啟應用。
例:增加和刪除子進程中的例子,ch_sup 增加了一個子進程 m1,還可以通過重啟整個應用來更新 supervisor :
{"2",[{"1", [{restart_application, ch_app}]}],[{"1", [{restart_application, ch_app}]}] }.
?
12.12 修改應用規格
在執行 relup 腳本前,在安裝 release 時,應用規格就自動更新了。因此,不需要在 .appup 中增加指令:
{"2",[{"1", []}],[{"1", []}] }.
?
12.13 修改應用配置
可以通過修改 .app 文件中的 env 字段,來修改應用配置。
另外,還可以修改 sys.config 文件來修改應用配置參數。
?
12.14 修改被包含的應用
增加、移除、重啟應用的 release 處理指令,只適用于原初應用。被包含應用沒有相應的指令。但是因為實際上,被包含應用的最上層 supervisor 是包含它的應用的 supervisor 的子進程,我們可以手動創建 relup 文件。
例:假設一個 release 包含了應用 prim_app,它的監控樹中有一個監程 prim_sup 。
在新版本 release 中,應用 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 的指令。因為新的 .rel 文件中有 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]}}]}] }.
?
12.15 修改非 Erlang 代碼
修改其他語言寫的代碼,比如接口程序,是依賴于應用的,OTP 沒有提供特別的支持。
例:修改 port 程序,假設控制這個接口的 Erlang 進程是注冊為 portc 的 gen_server,通過回調 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} ->trueend,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 目錄被包含在新的 release 包中:
1> systools:make_tar("my_release", [{dirs,[priv]}]).
...
?
12.16 模擬器重啟和升級
兩條重啟模擬器的升級指令:
- restart_new_emulator
當 ERTS、Kernel、STDLIB 或 SASL 要升級時會用到。用 systools:make_relup/3,4 生成 relup 文件會自動添加這條指令。它會在所有其他指令之前執行。詳見前文的 restart_new_emulator(低級指令)。
- restart_emulator
在所有其他指令執行完后需要重啟模擬器時會用到。詳見前文的 restart_emulator 。
如果只需要重啟模擬器,不需要其他升級指令,可以手動創建一個 relup 文件:
{"B",[{"A",[],[restart_emulator]}],[{"A",[],[restart_emulator]}] }.
此時,release 管理框架會自動打包、解包、更新路徑等,且不需要指定 .appup 文件。
?
12.17 OTP R15 之前的模擬器升級
從 OTP R15 開始,模擬器升級會在加載代碼和運行其他應用升級指令前,使用新版的核心應用(Kernel、STDLIB 和 SASL)重啟模擬器來完成模擬器升級。這要求要升級的 release 必須是 OTP R15 或更晚版本。
如果 release 是早期版本,systools:make_relup 會生成一個向后兼容的 relup 文件。所有升級指令在模擬器重啟前執行,新的應用代碼會被加載到舊模擬器中。如果新代碼是用新模擬器編譯的,而新模擬器下的 beam 文件的格式有變化,可能導致加載 beam 文件失敗。用舊模擬器編譯新代碼,可以解決這個問題。
?