Erlang/OTP設計原則(文檔翻譯)

 

http://erlang.org/doc/design_principles/des_princ.htmlhtml

圖和代碼皆源自以上連接中Erlang官方文檔,翻譯時的版本爲20.1。java

這個設計原則,實際上是說用戶在設計系統的時候應遵循的標準和規範。閱讀前我一直覺得寫的是做者在設計 Erlang/OTP 框架時的一些原則。node

閒話少敘。Let's go!git

1.概述

OTP設計原則規定了如何使用進程、模塊和目錄來組織 Erlang 代碼。shell

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 回調模塊).app

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} ->
            Res
    end.

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} ->
            Res
    end.

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) of
      true ->
         {lists:delete(Ch, Alloc), [Ch|Free]};
      false ->
         Channels
   end. 

沒有使用 behaviour 的代碼可能效率更高,可是通用性差。將系統中的全部 applications 組織成一致的行爲模式很重要。

並且使用 behaviour 能讓代碼易讀易懂。簡易的程序結構可能會更有效率,可是比較難理解。

上面的 server 模塊其實就是一個簡化的 Erlang/OTP behaviour - gen_server。

Erlang/OTP的標配 behaviour 有:

  • gen_server  實現 client/server 模式的服務器
  • gen_statem  實現狀態機(譯者補充:舊版本中爲 gen_fsm)

編譯器能識別模塊屬性 -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模型是用來進行資源管理,多個客戶端想分享一個公共資源。而服務器則用來管理這個資源。

IMAGE MISSING

圖 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(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 引擎自己,能夠作下列事情:

詳見 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] -> % Complete
        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}.

若是門是鎖着的,按鈕被按下,比較輸入按鈕和正確的按鈕。根據比較的結果,若是鎖開了,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 of
    locked ->
        case maps:get(remaining, Data) of
        [Digit] -> % Complete
            do_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_data
    end;
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) ->
    receive
    after 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) of
        Digit ->
            {keep_state, Data#{buf := []},
             [{reply,From,ok},
              {next_event,internal,{button,Chars}}]}
    catch
        error: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;
           (_, _) -> true
       end,
       Data)},
    case Opt of
    terminate ->
        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}]}
        end
    end;
%%
%% 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}} ->
        if
        Digit =:= LockButton ->
            {next_state, {locked,LockButton}, Data,
             [{reply,From,locked}]};
        true ->
            {keep_state_and_data,
             [postpone]}
        end
    end.

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;
           (_, _) -> true
       end,
       Data)},
    case Opt of
    terminate ->
        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_buttoncode_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(),         % optional
                intensity => non_neg_integer(), % optional
                period => pos_integer()}        % optional
    strategy() = one_for_all
               | one_for_one
               | rest_for_one
               | simple_one_for_one

 

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 中的 intensityperiod 字段來指定:

SupFlags = #{intensity => MaxR, period => MaxT, ...}

若是 MaxT 秒內重啓了 MaxR 次,監控者會終止全部的子進程,而後退出。此時 supervisor 退出的理由是 shutdown

當 supervisor 終止時,它的上一級 supervisor 會做出一些處理,重啓它,或者跟着退出。

這個重啓機制的目的是防止進程反覆由於同一緣由終止和重啓。

intensityperiod 都是可選參數,若是沒有指定,它們缺省值分別爲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(),       % mandatory
                 start => mfargs(),      % mandatory
                 restart => restart(),   % optional
                 shutdown => shutdown(), % optional
                 type => worker(),       % optional
                 modules => modules()}   % optional
    child_id() = term()
    mfargs() = {M :: module(), F :: atom(), A :: [term()]}
    modules() = [module()] | dynamic
    restart() = permanent | transient | temporary
    shutdown() = 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_oneone_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。

sysproc_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 設計原則。這樣一個進程須要知足:

系統消息是在監控樹中用到的、有特殊意義的消息。典型的系統消息有追蹤輸出的請求、掛起或恢復進程的請求(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} ->
            Res
    end.

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 模塊的手冊。
    • EventInfo 是從 handle_debug 傳入的。
  • Info 用來傳遞更多信息給 Func,能夠是任何類型,會原樣傳給 Func。
  • Event 是系統事件。用戶能夠決定系統事件的定義和表現形式。通常至少輸入和輸出消息會被認爲是系統事件,分別用 {in,Msg[,From]} {out,Msg,To} 表示。

handle_debug 返回一個更新的 debug 結構 Deb1。

此例中,handle_debug 會在每次輸入和輸出信息時被調用。格式化函數 Funcch4: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)

監控樹中的進程應以父進程相同的理由退出。

  • RequestFrom 是從系統消息中原樣傳遞的。
  • 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_YResXTypes 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),還要指定 descriptionvsnmodulesregistered 和 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/libpriv/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 還能夠是 permanenttransient:

  • 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@cavecp3@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 文件必須包含 kernelstdlibsasl,由於 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.3

Eshell 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: sasl
          started_at: nonode@nohost

...
=PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===
         application: ch_app
          started_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 中有一個子目錄。每一個子目錄的名字是對應的節點名。一個客戶端目錄至少包含 binreleases 兩個子目錄。這些目錄用來存放 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,2sys:change_code/4,5sys: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_applicationadd_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 建立了 $ROOT/lib/releases/Vsn 目錄,目錄中含 .rel 文件、啓動腳本 start.boot、系統配置文件 sys.config 以及 relup 文件。有新版本號的應用會被放在 $ROOT/lib 目錄下,其餘應用不受影響。解包後的 release 能夠 安裝了。release_handler 會執行 relup 文件中的指令:
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 附近。kernelstdlibsasl 目錄不受影響,由於它們沒有改變。

$ROOT/releases 下建立了一個新目錄 B,其中包含了 ch_rel-2.relstart.bootsys.configrelup

步驟 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 字段,應用回調模塊。
  • ChangedNew 是一個 {Par,Val} 列表,包含了全部改變和增長的配置參數。
  • Removed 是被刪除的全部配置參數 Par 的列表。

這個函數是可選的,在實現應用回調模塊時能夠省略。

 

12 Appup Cookbook

此章包含典型案例的升降級 .appup 文件的例子。

12.1 修改功能模塊

若是功能模塊被修改,例如新加了一個函數或修復了一個 bug,簡單的代碼替換就夠了:

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

 

12.2 修改駐地模塊

若是系統徹底根據 OTP 設計原則來實現,除系統進程和特殊進程外的全部進程,都會駐紮在某個 behavior 中,supervisorgen_servergen_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},升級則爲 VsnVsn 是模塊的「原」版,即升級前的版本。

若是模塊有 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}
   ]}]
}.

如前文所述,指令的順序很重要。升級時,必須在啓動新進程以前,加載 m一、改變 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} ->
            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 目錄被包含在新的 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 文件失敗。用舊模擬器編譯新代碼,能夠解決這個問題。

相關文章
相關標籤/搜索