日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Erlang/OTP设计原则(文档翻译)

發(fā)布時(shí)間:2025/3/8 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Erlang/OTP设计原则(文档翻译) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

?

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

圖和代碼皆源自以上鏈接中Erlang官方文檔,翻譯時(shí)的版本為20.1。

這個(gè)設(shè)計(jì)原則,其實(shí)是說用戶在設(shè)計(jì)系統(tǒng)的時(shí)候應(yīng)遵循的標(biāo)準(zhǔn)和規(guī)范。閱讀前我一直以為寫的是作者在設(shè)計(jì) Erlang/OTP 框架時(shí)的一些原則。

閑話少敘。Let's go!

1.概述

OTP設(shè)計(jì)原則規(guī)定了如何使用進(jìn)程、模塊和目錄來組織 Erlang 代碼。

1.1 監(jiān)控樹

Erlang/OTP的一個(gè)基本概念就是監(jiān)控樹。它是基于 workers(工人)和 supervisors(監(jiān)工、監(jiān)程)的進(jìn)程組織模型。

  • Workers 是實(shí)際執(zhí)行運(yùn)算的進(jìn)程。
  • Supervisors 是負(fù)責(zé)監(jiān)控 workers 的進(jìn)程。如果 worker 發(fā)生異常,supervisor 可以重啟這個(gè) worker。
  • 監(jiān)控樹就是由 supervisors 和 workers 組成的層次結(jié)構(gòu),讓我們可以設(shè)計(jì)和編寫容錯(cuò)的軟件。

下圖中方塊表示 supervisor,圓圈表示 worker(圖源Erlang官方文檔):

圖1.1:? 監(jiān)控樹

?

1.2 Behaviours(行為模式)

在監(jiān)控樹中,很多進(jìn)程擁有一樣的結(jié)構(gòu),遵循一樣的行為模式。例如,supervisors 結(jié)構(gòu)上都是一樣的,唯一的不同就是他們監(jiān)控的子進(jìn)程不同。而很多 wokers 都是以 server/client、finite-state machines(有限狀態(tài)自動(dòng)機(jī))或是 error logger(錯(cuò)誤記錄器)之類的事件處理器的行為模式運(yùn)行。

Behaviour就是把這些通用行為模式形式化。也就是說,把進(jìn)程的代碼分成通用的部分(behaviour 模塊)和專有的部分(callback module 回調(diào)模塊).

Behaviour 是 Erlang/OTP 框架中的一部分。用戶如果要實(shí)現(xiàn)一個(gè)進(jìn)程(例如一個(gè) supervisor),只需要實(shí)現(xiàn)回調(diào)模塊,然后導(dǎo)出預(yù)先定義的函數(shù)集(回調(diào)函數(shù))就行了。

下面的例子表明了怎么把代碼分成通用部分和專有部分。我們把下面的代碼當(dāng)作是一個(gè)簡單的服務(wù)器(用普通Erlang編寫),用來記錄 channel 集合。其他進(jìn)程可以各自通過調(diào)用函數(shù) alloc/0 和 fee/1 來分配和釋放 channel。

-module(ch1). -export([start/0]). -export([alloc/0, free/1]). -export([init/0]).start() ->spawn(ch1, init, []).alloc() ->ch1 ! {self(), alloc},receive{ch1, Res} ->Resend.free(Ch) ->ch1 ! {free, Ch},ok.init() ->register(ch1, self()),Chs = channels(),loop(Chs).loop(Chs) ->receive{From, alloc} ->{Ch, Chs2} = alloc(Chs),From ! {ch1, Ch},loop(Chs2);{free, Ch} ->Chs2 = free(Ch, Chs),loop(Chs2)end.

?這個(gè)服務(wù)器可以重寫成一個(gè)通用部分 server.erl :

-module(server). -export([start/1]). -export([call/2, cast/2]). -export([init/1]).start(Mod) ->spawn(server, init, [Mod]).call(Name, Req) ->Name ! {call, self(), Req},receive{Name, Res} ->Resend.cast(Name, Req) ->Name ! {cast, Req},ok.init(Mod) ->register(Mod, self()),State = Mod:init(),loop(Mod, State).loop(Mod, State) ->receive{call, From, Req} ->{Res, State2} = Mod:handle_call(Req, State),From ! {Mod, Res},loop(Mod, State2);{cast, Req} ->State2 = Mod:handle_cast(Req, State),loop(Mod, State2)end.

和一個(gè)回調(diào)模塊 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

注意以下幾點(diǎn):

  • server 的代碼可以重用來構(gòu)建不同的服務(wù)器。
  • server 名字(在這個(gè)例子中是 ch2)對(duì)用戶函數(shù)來說是透明的。即,修改名字不會(huì)影響函數(shù)調(diào)用。
  • 協(xié)議(server 發(fā)送和接受到的消息)也是透明的。這是一個(gè)好的編碼慣例,修改協(xié)議不會(huì)影響到調(diào)用接口函數(shù)的代碼。
  • 擴(kuò)展 server 的功能不需要改變 ch2 或其他回調(diào)模塊。

上面的 ch1.erl 和 ch2.erl 中,channels/0, alloc/1 和 free/2 的實(shí)現(xiàn)被刻意遺漏,因?yàn)榕c本例無關(guān)。完整性起見,下面給出這些函數(shù)的一種實(shí)現(xiàn)方式。這只是個(gè)示例,現(xiàn)實(shí)中還必須能夠處理諸如 channel 用完無法分配等情況。

channels() ->{_Allocated = [], _Free = lists:seq(1,100)}.alloc({Allocated, [H|T] = _Free}) ->{H, {[H|Allocated], T}}.free(Ch, {Alloc, Free} = Channels) ->case lists:member(Ch, Alloc) oftrue ->{lists:delete(Ch, Alloc), [Ch|Free]};false ->Channelsend.

沒有使用 behaviour 的代碼可能效率更高,但是通用性差。將系統(tǒng)中的所有 applications 組織成一致的行為模式很重要。

而且使用 behaviour 能讓代碼易讀易懂。簡易的程序結(jié)構(gòu)可能會(huì)更有效率,但是比較難理解。

上面的 server 模塊其實(shí)就是一個(gè)簡化的 Erlang/OTP behaviour - gen_server。

Erlang/OTP的標(biāo)配 behaviour 有:

  • gen_server? 實(shí)現(xiàn) client/server 模式的服務(wù)器
  • gen_statem? 實(shí)現(xiàn)狀態(tài)機(jī)(譯者補(bǔ)充:舊版本中為 gen_fsm)
  • gen_event? 實(shí)現(xiàn)事件處理器
  • supervisor? 實(shí)現(xiàn)監(jiān)控樹中的監(jiān)控者

編譯器能識(shí)別模塊屬性 -behaviour(Behaviour) ,會(huì)對(duì)未實(shí)現(xiàn)的回調(diào)函數(shù)發(fā)出編譯警告,例如:

-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 自帶一些組件,每個(gè)組件實(shí)現(xiàn)了特定的功能。這些組件用 Erlang/OTP 術(shù)語叫做 application(應(yīng)用)。例如 Mnesia 就是一個(gè)Erlang/OTP 應(yīng)用,它包含了所有數(shù)據(jù)庫服務(wù)所需的功能,還有 Debugger,用來 debug Erlang 代碼。基于 Erlang/OTP 的系統(tǒng),至少必須包含下面兩個(gè) application:

  • Kernel - 運(yùn)行 Erlang 時(shí)必須的功能
  • STDLIB - Erlang 標(biāo)準(zhǔn)庫

應(yīng)用的概念適用于程序結(jié)構(gòu)(進(jìn)程)和目錄結(jié)構(gòu)(模塊)。

最簡單的應(yīng)用由一組功能模塊組成,不包含任何進(jìn)程,這種叫 library application(庫應(yīng)用)。STDLIB 就屬于這類。

有進(jìn)程的應(yīng)用可以使用標(biāo)準(zhǔn) behaviour 很容易地實(shí)現(xiàn)一個(gè)監(jiān)控樹。

如何編寫應(yīng)用詳見后文 Applications。

?

1.4 Releases(發(fā)布版本)

一個(gè) release 是一個(gè)完整的系統(tǒng),包含 Erlang/OTP 應(yīng)用的子集和一系列用戶定義的 application。

詳見后文?Releases。

怎么在目標(biāo)環(huán)境中部署 release 在系統(tǒng)原則的文檔中有講到。

?

1.5 Release Handling(管理發(fā)布)

管理 release 即在一個(gè) release 的不同版本之間升級(jí)或降級(jí),怎么在一個(gè)運(yùn)行中的系統(tǒng)操作這些,詳見后文 Release Handling。

?

2?gen_server Behaviour

這部分可與 stdblib 中的 gen_server(3) 教程(包含了 gen_server 所有接口函數(shù)和回調(diào)函數(shù))一起閱讀。

2.1 Client-Server 原則

?C/S模型就是一個(gè)服務(wù)器對(duì)應(yīng)任意多個(gè)客戶端。C/S模型是用來進(jìn)行資源管理,多個(gè)客戶端想分享一個(gè)公共資源。而服務(wù)器則用來管理這個(gè)資源。

圖 2.1: ? Client-Server Model

?

2.2 例子

前文有用普通 erlang 寫的簡單的服務(wù)器的例子。使用 gen_server 重寫,結(jié)果如下:

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

下一小節(jié)將解釋這段代碼。

?

2.3 啟動(dòng)一個(gè) gen_server

在上一小節(jié)的示例中,gen_server 通過調(diào)用 ch3:start_link() 啟動(dòng):

start_link() ->gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}

start_link 調(diào)用了函數(shù) gen_server:start_link/4 ,這個(gè)函數(shù)產(chǎn)生并連接了一個(gè)新進(jìn)程(一個(gè) gen_server)。

  • 第一個(gè)參數(shù),{local, ch3},指定了進(jìn)程名,gen_server 會(huì)在本地注冊為 ch3。

????????? 如果名字被省略,gen_server 不會(huì)被注冊,此時(shí)一定要用它的 pid。名字還可以用 {global, Name},這樣的話 gen_server 會(huì)調(diào)用 global:register_name/2 來注冊。

  • 第二個(gè)參數(shù),ch3,是回調(diào)模塊的名字,即回調(diào)函數(shù)所在的模塊名。

????????? 接口函數(shù) (start_link, alloc 和 free) 和回調(diào)函數(shù) (init, handle_call 和 handle_cast) 放在同一個(gè)模塊中。這是一個(gè)好的編程慣例,把與一個(gè)進(jìn)程相關(guān)的代碼放在同一個(gè)模塊中。

  • 第三個(gè)參數(shù),[],是用來傳遞給回調(diào)函數(shù) init 的參數(shù)。此例中 init 不需要輸入,所以忽視了這個(gè)參數(shù)。
  • 第四個(gè)參數(shù),[],是一個(gè)選項(xiàng)list。查看 gen_server(3) 可獲悉可用選項(xiàng)。

如果名字注冊成功,這個(gè)新的 gen_server 進(jìn)程會(huì)調(diào)用回調(diào)函數(shù) ch3:init([]) 。init 函數(shù)應(yīng)該返回 {ok, State},其中 State 是 gen_server 的內(nèi)部狀態(tài),在此例中,內(nèi)部狀態(tài)指的是 channel 集合。

init(_Args) ->{ok, channels()}.

gen_server:start_link 是同步調(diào)用,在 gen_server 初始化成功可接收請求之前它不會(huì)返回。

如果 gen_server 是一個(gè)監(jiān)控樹的一部分,supervisor 啟動(dòng) gen_server 時(shí)一定要使用 gen_server:start_link。還有一個(gè)函數(shù)是 gen_server:start ,這個(gè)函數(shù)會(huì)啟動(dòng)一個(gè)獨(dú)立的 gen_server,也就是說它不會(huì)成為監(jiān)控樹的一部分。

?

2.4 同步消息請求 - Call

同步的請求 alloc() 是用 gen_server:call/2 來實(shí)現(xiàn)的:

alloc() ->gen_server:call(ch3, alloc).

ch3 是 gen_server 的名字,要與進(jìn)程名字相符合才能使用。alloc 是實(shí)際的請求。

這個(gè)請求會(huì)被轉(zhuǎn)化成一個(gè)消息,發(fā)送給 gen_server。收到消息后,gen_server 調(diào)用 handle_call(Request, From, State) 來處理消息,正常會(huì)返回 {reply, Reply, State1}。Reply 是會(huì)發(fā)回給客戶端的回復(fù)內(nèi)容,State1 是 gen_server 新的內(nèi)部狀態(tài)。

handle_call(alloc, _From, Chs) ->{Ch, Chs2} = alloc(Chs),{reply, Ch, Chs2}.

此例中,回復(fù)內(nèi)容就是分配給它的 channel Ch,而新的內(nèi)部狀態(tài)是剩余的 channel 集合 Chs2。

就這樣,ch3:alloc() 返回了分配給它的 channel Ch,gen_server 則保存剩余的 channel 集合,繼續(xù)等待新的請求。

?

2.5 異步消息請求 - Cast

異步的請求 free(Ch) 是用 gen_server:cast/2 來實(shí)現(xiàn)的:

free(Ch) ->gen_server:cast(ch3, {free, Ch}).

ch3 是 gen_server 的名字,{free, Ch} 是實(shí)際的請求。

這個(gè)請求會(huì)被轉(zhuǎn)化成一個(gè)消息,發(fā)送給 gen_server。發(fā)送后直接返回 ok。

收到消息后,gen_server 調(diào)用?handle_cast(Request, State) 來處理消息,正常會(huì)返回 {noreply,State1}。State1 是 gen_server 新的內(nèi)部狀態(tài)。

handle_cast({free, Ch}, Chs) ->Chs2 = free(Ch, Chs),{noreply, Chs2}.

此例中,新的內(nèi)部狀態(tài)是新的剩余的 channel集合 Chs2。然后 gen_server 繼續(xù)等待新的請求。

?

2.6 終止

在監(jiān)控樹中

如果 gen_server 是監(jiān)控樹的一部分,則不需要終止函數(shù)。gen_server 會(huì)自動(dòng)被它的監(jiān)控者終止,具體怎么終止通過 終止策略 來決定。

如果要在終止前進(jìn)行一些操作,終止策略必須有一個(gè) time-out 值,且 gen_server 必須在 init 函數(shù)中被設(shè)置為捕捉 exit 信號(hào)。當(dāng)被要求終止時(shí),gen_server 會(huì)調(diào)用回調(diào)函數(shù) terminate(shutdown, State):

init(Args) ->...,process_flag(trap_exit, true),...,{ok, State}....terminate(shutdown, State) ->..code for cleaning up here..ok.

獨(dú)立的 gen_server

如果 gen_server 不是監(jiān)控樹的一部分,可以寫一個(gè) stop 函數(shù),例如:

... 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 消息的回調(diào)函數(shù)返回 {stop, normal, State1},normal 意味著這是一次自然死亡,而 State1 是一個(gè)新的 gen_server 內(nèi)部狀態(tài)。這會(huì)導(dǎo)致 gen_server 調(diào)用 terminate(normal, State1) 然后優(yōu)雅地……掛掉。

?

2.7 處理其他消息

如果 gen_server 會(huì)在除了請求之外接收其他消息,需要實(shí)現(xiàn)回調(diào)函數(shù) handle_info(Info, State) 來進(jìn)行處理。其他消息可能是 exit 消息,如果 gen_server 與其他進(jìn)程連接起來(不是 supervisor),并且被設(shè)置為捕捉 exit 信號(hào)。

handle_info({'EXIT', Pid, Reason}, State) ->..code to handle exits here..{noreply, State1}.

一定要實(shí)現(xiàn) code_change 函數(shù)。(譯者補(bǔ)充:在代碼熱更新時(shí)會(huì)用到)

code_change(OldVsn, State, Extra) ->..code to convert state (and more) during code change{ok, NewState}.

?

3?gen_statem Behavior

此章可結(jié)合 gen_statem(3) (包含全部接口函數(shù)和回調(diào)函數(shù)的詳述)教程一起看。

注意:這是 Erlang/OTP 19.0 引入的新 behavior。它已經(jīng)經(jīng)過了完整的 review,穩(wěn)定使用在至少兩個(gè)大型 OTP 應(yīng)用中并被保留下來。基于用戶反饋,我們覺得有必要在 Erlang/OTP 20.0 對(duì)它進(jìn)行小調(diào)整(不向后兼容)。

3.1 事件驅(qū)動(dòng)的狀態(tài)機(jī)

現(xiàn)在的自動(dòng)機(jī)理論沒有具體描述狀態(tài)變遷是如何觸發(fā)的,而是假定輸出是一個(gè)以輸入和當(dāng)前狀態(tài)為參數(shù)的函數(shù),它們是某種類型的值。

對(duì)一個(gè)事件驅(qū)動(dòng)的狀態(tài)機(jī)來說,輸入就是一個(gè)觸發(fā)狀態(tài)變遷的事件,輸出是狀態(tài)遷移過程中執(zhí)行的動(dòng)作。用類似有限狀態(tài)自動(dòng)機(jī)的數(shù)學(xué)模型來描述,它是一系列如下形式的關(guān)系:

State(S) x Event(E) -> Actions(A), State(S')

這些關(guān)系可以這么理解:如果我們現(xiàn)在處于 S 狀態(tài),事件 E 發(fā)生了,我們就要執(zhí)行動(dòng)作 A 并且轉(zhuǎn)移狀態(tài)為 S' 。注意: S’ 可能與 S 相同。

由于 A 和 S' 只取決于 S 和 E,這種狀態(tài)機(jī)被稱為 Mealy 機(jī)(可參見維基百科的描述)。

跟大多數(shù) gen_ 開頭的 behavior 一樣, gen_statem 保存了 server 的數(shù)據(jù)和狀態(tài)。而且狀態(tài)數(shù)是沒有限制的(假設(shè)虛擬機(jī)內(nèi)存足夠),輸入事件類型數(shù)也是沒有限制的,因此用這個(gè) behavior 實(shí)現(xiàn)的狀態(tài)機(jī)實(shí)際上是圖靈完備的。不過感覺上它更像一個(gè)事件驅(qū)動(dòng)的 Mealy 機(jī)。

?

3.2 回調(diào)模式

gen_statem 支持兩種回調(diào)模式:

  • state_functions 方式,狀態(tài)遷移規(guī)則以 erlang 函數(shù)的形式編寫,寫法如下:
StateName(EventType, EventContent, Data) ->... code for actions here ...{next_state, NewStateName, NewData}.

???????? 在示例部分用的最多的就是這種格式。

  • handle_event_function 方式,只用一個(gè) erlang 函數(shù)來裝載所有的狀態(tài)遷移規(guī)則:
handle_event(EventType, EventContent, State, Data) ->... code for actions here ...{next_state, NewState, NewData}

??????? 示例可見單個(gè)事件處理器這一小節(jié)。

這兩種函數(shù)都支持其他的返回值,具體可見 gen_statem 的教程頁面的?Module:StateName/3。其他的返回元組可以停止?fàn)顟B(tài)機(jī)、在狀態(tài)機(jī)引擎中執(zhí)行轉(zhuǎn)移動(dòng)作、發(fā)送回復(fù)等等。

選擇何種回調(diào)方式

這兩種回調(diào)方式有不同的功能和限制,但是目標(biāo)都一樣:要處理所有可能的事件和狀態(tài)的組合。

你可以同時(shí)只關(guān)心一種狀態(tài),確保每個(gè)狀態(tài)都處理了所有事件。或者只關(guān)心一個(gè)事件,確保它在所有狀態(tài)下都被處理。你也可以結(jié)合兩種策略

state_functions 方式中,狀態(tài)只能用 atom 表示,gen_statem 引擎通過狀態(tài)名來分發(fā)處理。它提倡回調(diào)模塊把一個(gè)狀態(tài)下的所有事件和動(dòng)作放在代碼的同一個(gè)地方,以此同時(shí)只關(guān)注一個(gè)狀態(tài)。

當(dāng)你的狀態(tài)圖確定時(shí),這種模式非常好。就像本小節(jié)舉的例子,狀態(tài)對(duì)應(yīng)的事件和動(dòng)作都放在一起,每個(gè)狀態(tài)有自己獨(dú)一無二的名字。

而通過 handle_event_function 方式,可以結(jié)合兩種策略,因?yàn)樗械氖录蜖顟B(tài)都在同一個(gè)回調(diào)函數(shù)中。

無論是想以狀態(tài)還是事件為中心,這種方式都能滿足。不過沒有分發(fā)到輔助函數(shù)的話,Module:handle_event/4 會(huì)迅速增長到無法管理。

?

3.3 狀態(tài)enter回調(diào)

不論回調(diào)模式是哪種,gen_statem 都會(huì)在狀態(tài)改變的時(shí)候(譯者補(bǔ)充:進(jìn)入狀態(tài)的時(shí)候調(diào)用)自動(dòng)調(diào)用回調(diào)函數(shù)call the state callback),所以你可以在狀態(tài)的轉(zhuǎn)移規(guī)則附近寫狀態(tài)入口回調(diào)。通常長這樣:

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

這可能會(huì)在特定情況下很有幫助,不過它要求你在所有狀態(tài)中都處理入口回調(diào)。詳見 State Entry Actions

?

3.4 動(dòng)作(Actions)

在第一小節(jié)事件驅(qū)動(dòng)的狀態(tài)機(jī)中,動(dòng)作(action)作為通用狀態(tài)機(jī)模型的一部分被提及。一般的動(dòng)作會(huì)在 gen_statem 處理事件的回調(diào)中執(zhí)行(返回到 gen_statem 引擎之前)。

還有一些特殊的狀態(tài)遷移動(dòng)作,在回調(diào)函數(shù)返回后指定 gen_statem 引擎去執(zhí)行。回調(diào)函數(shù)可以在返回的元組中指定一個(gè)動(dòng)作列表。這些動(dòng)作影響 gen_statem 引擎本身,可以做下列事情:

  • 延緩(postpone)當(dāng)前事件, 詳見延緩事件
  • 掛起(hibernate)狀態(tài)機(jī),詳見掛起
  • 狀態(tài)超時(shí)(state time-out),詳見狀態(tài)超時(shí)
  • 一般超時(shí)(generic time-out),詳見一般超時(shí)
  • 事件超時(shí)(event time-out),詳見事件超時(shí)
  • 回復(fù)調(diào)用者,詳見全狀態(tài)事件
  • 生成下一個(gè)要處理的事件,詳見自生成事件

詳見 gen_statem(3) 。你可以回復(fù)很多調(diào)用者、生成多個(gè)后續(xù)事件、設(shè)置相對(duì)時(shí)間或絕對(duì)時(shí)間的超時(shí)等等。

?

3.5 事件類型

事件分成不同的類型(event types)。同狀態(tài)下的不同類型的事件都在同一個(gè)回調(diào)函數(shù)中處理,回調(diào)函數(shù)以 EventType 和 EventContent 作為參數(shù)。

下面列出事件類型和來源的完整列表:

cast

?????? 由 gen_statem:cast 生成。

{call, From}

?????? 由 gen_statem:call 生成,狀態(tài)遷移動(dòng)作返回 {reply, From, Msg} 或調(diào)用 gen_statem:reply 時(shí),會(huì)用到 From 作為回復(fù)地址。

info

????? 發(fā)送給 gen_statem 進(jìn)程的常規(guī)進(jìn)程消息。

state_timeout

??? ? 狀態(tài)遷移動(dòng)作 {state_timeout,Time,EventContent} 生成。

{timeout,Name}

??? ? 狀態(tài)遷移動(dòng)作 {{timeout,Name},Time,EventContent} 生成。

timeout

??? ? 狀態(tài)遷移動(dòng)作 {timeout,Time,EventContent}(或簡寫為 Time)生成。

internal

??? ? 狀態(tài)遷移動(dòng)作 {next_event,internal,EventContent} 生成。

上述所有事件類型都可以用 {next_event,EventType,EventContent} 來生成。

?????

3.6 示例

密碼鎖的門可以用一個(gè)自動(dòng)機(jī)來表述。初始狀態(tài),門是鎖住的。當(dāng)有人按一個(gè)按鈕,即觸發(fā)一個(gè)事件。結(jié)合此前按下的按鈕,結(jié)果可能是正確、不完整或者錯(cuò)誤。如果正確,門鎖會(huì)開啟10秒鐘(10,000毫秒)。如果不完整,則等待下一個(gè)按鈕被按下。如果錯(cuò)了,一切從頭再來,等待新一輪按鈕。

圖3.1: 密碼鎖狀態(tài)圖

密碼鎖狀態(tài)機(jī)用 gen_statem 實(shí)現(xiàn),回調(diào)模塊如下:

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

下一小節(jié)解釋代碼。

?

3.7 啟動(dòng)狀態(tài)機(jī)

前例中,可調(diào)用 code_lock:start_link(Code) 來啟動(dòng) gen_statem:

start_link(Code) ->gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).

start_link 函數(shù)調(diào)用 gen_statem:start_link/4,生成并連接了一個(gè)新進(jìn)程(gen_statem)。

  • 第一個(gè)參數(shù),{local, ?NAME} 指定了名字。在此例中 gen_statem 在本地注冊為 code_lock(?NAME)。如果名字被省略,gen_statem 不會(huì)被注冊,此時(shí)必須用它的 pid。名字還可以用{global, Name},這樣的話 gen_server 會(huì)調(diào)用 global:register_name/2 來注冊。
  • 第二個(gè)參數(shù),?MODULE,這個(gè)參數(shù)就是回調(diào)模塊的名字,此例的回調(diào)模塊就是當(dāng)前模塊。接口函數(shù)(start_link/1 和 button/1)與回調(diào)函數(shù)(init/1, locked/3, 和 open/3)放在同一個(gè)模塊中。這是一個(gè)好的編程慣例,把 client 和 server 的代碼放在同一個(gè)模塊中。
  • 第三個(gè)參數(shù),Code,是一串?dāng)?shù)字,存儲(chǔ)了正確的門鎖密碼,將被傳遞給 init/1 函數(shù)。
  • 第四個(gè)參數(shù),[],是一個(gè)選項(xiàng)list。查看 gen_statem:start_link/3 可獲悉可用選項(xiàng)。

如果名字注冊成功,這個(gè)新的 gen_statem 進(jìn)程會(huì)調(diào)用 init 回調(diào) code_lock:init(Code)init 函數(shù)應(yīng)該返回 {ok, State, Data},其中 State 是初始狀態(tài)(此例中是鎖住狀態(tài),假設(shè)門一開始是鎖住的)。Data 是 gen_statem 的內(nèi)部數(shù)據(jù)。此例中 Data 是一個(gè)map,其中 code 對(duì)應(yīng)的是正確的密碼,remaining 對(duì)應(yīng)的是按鈕按對(duì)后剩余的密碼(初始與 code 一致)。

init(Code) ->do_lock(),Data = #{code => Code, remaining => Code},{ok,locked,Data}.

gen_statem:start_link 是同步調(diào)用,在 gen_statem 初始化成功可接收請求之前它不會(huì)返回。

如果 gen_statem 是一個(gè)監(jiān)控樹的一部分,supervisor 啟動(dòng) gen_statem 時(shí)一定要使用 gen_statem:start_link。還有一個(gè)函數(shù)是 gen_statem:start ,這個(gè)函數(shù)會(huì)啟動(dòng)一個(gè)獨(dú)立的 gen_statem,也就是說它不會(huì)成為監(jiān)控樹的一部分。

callback_mode() ->state_functions.

函數(shù) Module:callback_mode/0 規(guī)定了回調(diào)模塊的回調(diào)模式,此例中是 state_functions 模式,每個(gè)狀態(tài)有自己的處理函數(shù)。

?

3.8 事件處理

通知 code_lock 按鈕事件的函數(shù)是用?gen_statem:cast/2 實(shí)現(xiàn)的:

button(Digit) ->gen_statem:cast(?NAME, {button,Digit}).

第一個(gè)參數(shù)是 gen_statem 的名字,要與進(jìn)程名字相同,所以我們用了同樣的宏 ?NAME。{button, Digit} 是事件的內(nèi)容。

這個(gè)事件會(huì)被轉(zhuǎn)化成一個(gè)消息,發(fā)送給 gen_statem當(dāng)收到事件時(shí), gen_statem 調(diào)用 StateName(cast, Event, Data),一般會(huì)返回一個(gè)元組 {next_state, NewStateName, NewData}。StateName 是當(dāng)前狀態(tài)名,NewStateName是下一個(gè)狀態(tài)。NewData 是 gen_statem 的新的內(nèi)部數(shù)據(jù),Actions 是 gen_statem 引擎要執(zhí)行的動(dòng)作列表。

locked(cast, {button,Digit},#{code := Code, remaining := Remaining} = Data) ->case Remaining of[Digit] -> % Completedo_unlock(),{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};[Digit|Rest] -> % Incomplete{next_state, locked, Data#{remaining := Rest}};[_|_] -> % Wrong{next_state, locked, Data#{remaining := Code}}end.open(state_timeout, lock, Data) ->do_lock(),{next_state, locked, Data}; open(cast, {button,_}, Data) ->{next_state, open, Data}.

如果門是鎖著的,按鈕被按下,比較輸入按鈕和正確的按鈕。根據(jù)比較的結(jié)果,如果鎖開了,gen_statem 變?yōu)?open 狀態(tài),否則繼續(xù)保持 locked 狀態(tài)。

如果按鈕是錯(cuò)的,數(shù)據(jù)又變?yōu)槌跏嫉拿艽a列表。

狀態(tài)為 open 時(shí),按鈕事件會(huì)被忽略,狀態(tài)維持不變。還可以返回 {keep_state, Data} 表示狀態(tài)不變或者返回 keep_state_and_data 表示狀態(tài)和數(shù)據(jù)都不變

?

3.9 狀態(tài)超時(shí)

當(dāng)給出正確的密碼,門鎖開啟,locked/2 返回如下元組:

{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};

10,000 是以毫秒為單位的超時(shí)時(shí)長。10秒后,會(huì)觸發(fā)一個(gè)超時(shí),然后 StateName(state_timeout, lock, Data) 被調(diào)用,此后門重新鎖住:

open(state_timeout, lock, Data) ->do_lock(),{next_state, locked, Data};

狀態(tài)超時(shí)會(huì)在狀態(tài)改變的時(shí)候自動(dòng)取消。重新設(shè)置一個(gè)狀態(tài)超時(shí)相當(dāng)于重啟,舊的定時(shí)器被取消,新的定時(shí)器被啟動(dòng)。也就是說可以通過重啟一個(gè)時(shí)間為 infinite 的超時(shí)來取消狀態(tài)超時(shí)。

?

3.10 全狀態(tài)事件

有些事件可能在任何狀態(tài)下到達(dá) gen_statem。可以在一個(gè)公共的函數(shù)處理這些事件,所有的狀態(tài)函數(shù)都調(diào)用它來處理通用的事件。

假定一個(gè) code_length/0 函數(shù)返回正確密碼的長度(不敏感的信息)。我們把所有與狀態(tài)無關(guān)的事件分發(fā)到公共函數(shù) 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,調(diào)用者會(huì)等待 server 的回復(fù)。{reply,From,Reply} 元組表示回復(fù),{keep_state, ...} 用來保持狀態(tài)不變。這個(gè)返回格式在你想保持狀態(tài)不變(不管狀態(tài)是什么)的時(shí)候非常方便。

?

3.11 單個(gè)事件處理器

如果使用 handle_event_function 模式,所有的事件都會(huì)在 Module:handle_event/4 被處理,我們可以(也可以不)在第一層以事件為中心進(jìn)行分組,然后再判斷狀態(tài):

... -export([handle_event/4]).... callback_mode() ->handle_event_function.handle_event(cast, {button,Digit}, State, #{code := Code} = Data) ->case State oflocked ->case maps:get(remaining, Data) of[Digit] -> % Completedo_unlock(),{next_state, open, Data#{remaining := Code},[{state_timeout,10000,lock}]};[Digit|Rest] -> % Incomplete{keep_state, Data#{remaining := Rest}};[_|_] -> % Wrong{keep_state, Data#{remaining := Code}}end;open ->keep_state_and_dataend; handle_event(state_timeout, lock, open, Data) ->do_lock(),{next_state, locked, Data}....

?

3.12 終止

在監(jiān)控樹中

如果 gen_statem 是監(jiān)控樹的一部分,則不需要終止函數(shù)。gen_statem 自動(dòng)的被它的監(jiān)控者終止,具體怎么終止通過 終止策略 來決定。

如果需要在終止前進(jìn)行一些操作,那么終止策略必須有一個(gè) time-out 值,且 gen_statem 必須在 init 函數(shù)中被設(shè)置為捕捉 exit 信號(hào),調(diào)用 process_flag(trap_exit, true)

init(Args) ->process_flag(trap_exit, true),do_lock(),...

當(dāng)被要求終止時(shí),gen_statem 會(huì)調(diào)用回調(diào)函數(shù) terminate(shutdown, State, Data):

terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok.

獨(dú)立的 gen_statem

如果 gen_statem 不是監(jiān)控樹的一部分,可以寫一個(gè) stop 函數(shù)(使用 gen_statem:stop)。建議增加一個(gè) API :

... -export([start_link/1,stop/0]).... stop() -> gen_statem:stop(?NAME).

這會(huì)導(dǎo)致 gen_statem 調(diào)用 terminate/3(像監(jiān)控樹中的服務(wù)器被終止一樣),等待進(jìn)程終止。

?

3.13 事件超時(shí)

事件超時(shí)功能繼承自 gen_statem 的前輩 gen_fsm ,事件超時(shí)的定時(shí)器在有事件達(dá)到的時(shí)候就會(huì)被取消。你可以接收到一個(gè)事件或者一個(gè)超時(shí),但不會(huì)兩個(gè)都收到。

事件超時(shí)由狀態(tài)遷移動(dòng)作 {timeout,Time,EventContent} 指定,或者僅僅是 Time, 或者僅僅一個(gè) Timer 而不是動(dòng)作列表(繼承自 gen_fsm)。

不活躍情況下想做點(diǎn)什么時(shí),可以用此類超時(shí)。如果30秒內(nèi)沒人按鈕,重置密碼列表:

...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}; ...

接收到任意按鈕事件時(shí),啟動(dòng)一個(gè)30秒超時(shí),如果接收到超時(shí)事件就重置密碼列表。

接收到其他事件時(shí),事件超時(shí)會(huì)被取消,所以要么接收到其他事件要么接受到超時(shí)事件。所以不能也不必要重啟一個(gè)事件超時(shí)。因?yàn)槟闾幚淼娜魏问录紩?huì)取消事件超時(shí)。

?

3.14 一般超時(shí)

前面說的狀態(tài)超時(shí)只在狀態(tài)不改變時(shí)有效。而事件超時(shí)只在不被其他事件打斷的時(shí)候生效。

你可能想要在某個(gè)狀態(tài)下開啟一個(gè)定時(shí)器,而在另一個(gè)狀態(tài)下做處理,想要不改變狀態(tài)就取消一個(gè)定時(shí)器,或者希望同時(shí)存在多個(gè)定時(shí)器。這些都可以用過 generic time-outs 一般超時(shí)來實(shí)現(xiàn)。它們看起來有點(diǎn)像事件超時(shí),但是它們有名字,不同名字的可以同時(shí)存在多個(gè),并且不會(huì)被自動(dòng)取消。

下面是用一般超時(shí)實(shí)現(xiàn)來替代狀態(tài)超時(shí)的例子,定時(shí)器名字是 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}; ...

和狀態(tài)超時(shí)一樣,可以通過給特定的名字設(shè)置新的定時(shí)器或設(shè)置為infinite來取消定時(shí)器。

也可以不取消失效的定時(shí)器,而是在它到來的時(shí)候忽略它(確定已無用時(shí))。

?

3.15 Erlang 定時(shí)器

最全面的處理超時(shí)的方式就是使用 erlang 的定時(shí)器,詳見 erlang:start_timer3,4。大部分的超時(shí)任務(wù)可以通過 gen_statem 的超時(shí)功能來完成,但有時(shí)候你可能想獲取 erlang:cancel_timer(Tref) 的返回值(剩余時(shí)間)。

下面是用 erlang 定時(shí)器替代前文狀態(tài)超時(shí)的實(shí)現(xiàn):

... 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}; ...

?當(dāng)狀態(tài)遷移到 locked 時(shí),我們可以不從 Data 中清除 timer 的值,因?yàn)槊看芜M(jìn)入 open 狀態(tài)都是一個(gè)新的 timer 值。不過最好不要在 Data 中保留過期的值。

當(dāng)其他事件觸發(fā),你想清除一個(gè) timer 時(shí),可以使用 erlang:cancel_timer(Tref) 。如果沒有延緩(下一小節(jié)會(huì)講到),超時(shí)消息被 cancel 后就不會(huì)再被收到,所以要確認(rèn)是否一不小心延緩了這類消息。要注意的是,超時(shí)消息可能在你 cancel 它之前就到達(dá),所以要根據(jù) erlang:cancel_timer(Tref) 的返回值,把這消息從進(jìn)程郵箱里讀出來。

另一種處理方式是,不要 cancel 掉一個(gè) timer,而是在它到達(dá)之后忽略它。

?

3.16 延緩事件

如果你想在當(dāng)前狀態(tài)忽略某個(gè)事件,在后續(xù)的某個(gè)狀態(tài)中再處理,你可以延緩這個(gè)事件。延緩的事件會(huì)在狀態(tài)變化后重新觸發(fā),即:OldState =/= NewState

延緩是通過狀態(tài)遷移動(dòng)作 postpone 來指定的。

此例中,我們可以延緩在 open 狀態(tài)下的按鈕事件(而不是忽略它),這些事件會(huì)進(jìn)入等待隊(duì)列,等到 locked 狀態(tài)時(shí)再處理:

... open(cast, {button,_}, Data) ->{keep_state,Data,[postpone]}; ...

延緩的事件只會(huì)在狀態(tài)改變時(shí)重新觸發(fā),因此要考慮怎么保存內(nèi)部數(shù)據(jù)。內(nèi)部數(shù)據(jù)可以在數(shù)據(jù) Data 或者狀態(tài) State 中保存,比如用兩個(gè)幾乎一樣的狀態(tài)來表示布爾值,或者使用一個(gè)復(fù)合狀態(tài)(回調(diào)模塊的 handle_event_function)。如果某個(gè)值的變化會(huì)改變事件處理,那需要把這個(gè)值保存在狀態(tài) State 里。因?yàn)?Data 的變化不會(huì)觸發(fā)延緩的事件。

如果你沒有用延緩的話,這個(gè)不重要。但是如果你決定使用延緩功能,沒有用不同的狀態(tài)做區(qū)分,可能會(huì)產(chǎn)生很難發(fā)現(xiàn)的 bug。

模糊的狀態(tài)圖

狀態(tài)圖很可能沒有給特定的狀態(tài)指定事件處理方式。可能在相關(guān)的上下文中有提及。

可能模糊的動(dòng)作(譯者補(bǔ)充:在狀態(tài)圖中沒有給出處理方式,可能對(duì)應(yīng)的動(dòng)作):忽略(丟棄或者僅僅 log)事件、延緩事件至其他狀態(tài)處理。

選擇性 receive

Erlang 的選擇性 receive 語句經(jīng)常被用來寫簡單的狀態(tài)機(jī)(不用 gen_statem 的普通 erlang 代碼)。下面是可能的實(shí)現(xiàn)方式之一:

-module(code_lock). -define(NAME, code_lock_1). -export([start_link/1,button/1]).start_link(Code) ->spawn(fun () ->true = register(?NAME, self()),do_lock(),locked(Code, Code)end).button(Digit) ->?NAME ! {button,Digit}.locked(Code, [Digit|Remaining]) ->receive{button,Digit} when Remaining =:= [] ->do_unlock(),open(Code);{button,Digit} ->locked(Code, Remaining);{button,_} ->locked(Code, Code)end.open(Code) ->receiveafter 10000 ->do_lock(),locked(Code, Code)end.do_lock() ->io:format("Locked~n", []). do_unlock() ->io:format("Open~n", []).

此例中選擇性 receive 隱含了把 open 狀態(tài)接收到的所有事件延緩到 locked 狀態(tài)的邏輯。

選擇性 receive 語句不能用在 gen_statem 或者任何 gen_* 中,因?yàn)?receive 語句已經(jīng)在 gen_* 引擎中包含了。為了兼容?sys ,behavior 進(jìn)程必須對(duì)系統(tǒng)消息作出反應(yīng),并把非系統(tǒng)的消息傳遞給回調(diào)模塊,因此把 receive 集成在引擎層的 loop 里。

動(dòng)作 postpone(延緩)是被設(shè)計(jì)來模擬選擇性 receive 的。選擇性 receive 隱式地延緩所有不被接受的事件,而 postpone 動(dòng)作則是顯示地延緩一個(gè)收到的事件。

兩種機(jī)制邏輯復(fù)雜度和時(shí)間復(fù)雜度是一樣的,而選擇性 receive 語法的常因子更少。

?

3.17 entry動(dòng)作

假設(shè)你有一張狀態(tài)圖,圖中使用了狀態(tài) entry 動(dòng)作。只有一兩個(gè)狀態(tài)有 entry 動(dòng)作時(shí)你可以用自生成事件(詳見下一部分),但是使用內(nèi)置的狀態(tài)enter回調(diào)是更好的選擇。

callback_mode/0 函數(shù)的返回列表中加入 state_enter,會(huì)在每次狀態(tài)改變的時(shí)候傳入?yún)?shù) (enter, OldState, ...) 調(diào)用一次回調(diào)函數(shù)。你只需像事件一樣處理這些請求即可:

... 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 來重復(fù)執(zhí)行 entry 代碼,這些詞其他含義跟 keep_state 家族一樣(保持狀態(tài)、數(shù)據(jù)不變等等)。詳見 state_callback_result()

?

3.18 自生成事件

有時(shí)候可能需要在狀態(tài)機(jī)中生成事件,可以用狀態(tài)遷移動(dòng)作 {next_event,EventType,EventContent} 來實(shí)現(xiàn)。

你可以生成所有類型(type)的事件。其中 internal 類型只能通過 next_event 來生成,不會(huì)由外部產(chǎn)生,你可以確定一個(gè) internal 事件是來自狀態(tài)機(jī)自身。

你可以用自生成事件來預(yù)處理輸入數(shù)據(jù),例如解碼、用換行分隔數(shù)據(jù)。有強(qiáng)迫癥的人可能會(huì)說,應(yīng)該分出另一個(gè)狀態(tài)機(jī)來發(fā)送預(yù)處理好的數(shù)據(jù)給主狀態(tài)機(jī)。為了降低消耗,這個(gè)預(yù)處理狀態(tài)機(jī)可以通過一般的狀態(tài)事件處理來實(shí)現(xiàn)。

下面的例子為一個(gè)輸入模型,通過 put_chars(Chars) 輸入,enter() 來結(jié)束輸入:

... -export(put_chars/1, enter/0). ... put_chars(Chars) when is_binary(Chars) ->gen_statem:call(?NAME, {chars,Chars}).enter() ->gen_statem:call(?NAME, enter)....locked(enter, _OldState, Data) ->do_lock(),{keep_state,Data#{remaining => Code, buf => []}}; ...handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) ->{keep_state, Data#{buf := [Chars|Buf],[{reply,From,ok}]}; handle_event({call,From}, enter, #{buf := Buf} = Data) ->Chars = unicode:characters_to_binary(lists:reverse(Buf)),try binary_to_integer(Chars) ofDigit ->{keep_state, Data#{buf := []},[{reply,From,ok},{next_event,internal,{button,Chars}}]}catcherror:badarg ->{keep_state, Data#{buf := []},[{reply,From,{error,not_an_integer}}]}end; ...

code_lock:start([17]) 啟動(dòng)程序,然后就能通過 code_lock:put_chars(<<"001">>), code_lock:put_chars(<<"7">>), code_lock:enter() 這一系列動(dòng)作開鎖了。

?

3.19 重寫例子

這一小節(jié)包含了之前提到的大部分修改,用到了狀態(tài) enter 回調(diào),用一個(gè)新的狀態(tài)圖來表述:

圖 3.2:重寫密碼鎖狀態(tài)圖

注意,圖中沒有說明 open 狀態(tài)如何處理按鈕事件。需要從其他地方找,因?yàn)闆]標(biāo)明的事件不是被去掉了,而是在其他狀態(tài)中進(jìn)行處理了。圖中也沒有說明 code_length/0 需要在所有狀態(tài)中處理。

回調(diào)模式: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}.

回調(diào)模式:handle_event_function

這部分描述了如何使用一個(gè) handle_event/4? 函數(shù)來替換上面的例子。前文提到的在第一層以事件作區(qū)分的方式在此例中不太合適,因?yàn)橛袪顟B(tài) enter 調(diào)用,所以用第一層以狀態(tài)作區(qū)分的方式:

... -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 狀態(tài)延遲到 open 狀態(tài)感覺會(huì)很奇怪,它只是用來舉例說明事件延緩。

?

3.20 過濾狀態(tài)

目前實(shí)現(xiàn)的服務(wù)器,會(huì)在終止時(shí)的錯(cuò)誤日志中輸出所有的內(nèi)部狀態(tài)。包含了門鎖密碼和剩下需要按的按鈕。

這個(gè)信息屬于敏感信息,你可能不想因?yàn)橐恍┎豢深A(yù)料的事情在錯(cuò)誤日志中輸出這些。

還有可能內(nèi)部狀態(tài)數(shù)據(jù)太多,在錯(cuò)誤日志中包含了太多沒用的數(shù)據(jù),所以需要進(jìn)行篩選。

你可以通過實(shí)現(xiàn)函數(shù)?Module:format_status/2 來格式化錯(cuò)誤日志中通過?sys:get_status/1,2 獲得的內(nèi)部狀態(tài),例如:

... -export([init/1,terminate/3,code_change/4,format_status/2]). ...format_status(Opt, [_PDict,State,Data]) ->StateData ={State,maps:filter(fun (code, _) -> false;(remaining, _) -> false;(_, _) -> trueend,Data)},case Opt ofterminate ->StateData;normal ->[{data,[{"State",StateData}]}]end.

實(shí)現(xiàn) Module:format_status/2 并不是強(qiáng)制的。如果不實(shí)現(xiàn),默認(rèn)的實(shí)現(xiàn)方式就類似上面這個(gè)例子,除了默認(rèn)不會(huì)篩選 Data(即 StateData = {State,Data}),例子中因?yàn)橛忻舾行畔⒈仨氝M(jìn)行篩選。

?

3.21 復(fù)合狀態(tài)

回調(diào)模式?handle_event_function 支持使用非 atom 的狀態(tài)(詳見回調(diào)模式),比如一個(gè)復(fù)合狀態(tài)可能是一個(gè) tuple。

你可能想在狀態(tài)變化的時(shí)候取消狀態(tài)超時(shí),或者和延緩事件配合使用控制事件處理,這時(shí)候就要用到復(fù)合狀態(tài)。我們引入可配置的鎖門按鈕來完善前面的例子(這就是此問題中的狀態(tài)),這個(gè)按鈕可以在 open 狀態(tài)立馬鎖門,且可以通過 set_lock_button/1 這個(gè)接口來設(shè)置鎖門按鈕。

假設(shè)我們在開門的狀態(tài)調(diào)用 set_lock_button,并且此前已經(jīng)延緩了一個(gè)按鈕事件(不是舊的鎖門按鈕,譯者補(bǔ)充:是新的鎖門按鈕)。說這個(gè)按鈕按得太早不算是鎖門按鈕,合理。然而門鎖狀態(tài)變?yōu)?locked 時(shí),你就會(huì)驚奇地發(fā)現(xiàn)一個(gè)鎖門按鈕事件觸發(fā)了。

我們用?gen_statem:call 來實(shí)現(xiàn) button/1 函數(shù),仍在 open 狀態(tài)延緩它所有的按鈕事件。在 open 狀態(tài)調(diào)用 button/1,狀態(tài)變?yōu)?locked 之前它不會(huì)返回,因?yàn)?locked 狀態(tài)時(shí)事件才會(huì)被處理并且回復(fù)。

如果另一個(gè)進(jìn)程在 button/1 掛起,有人調(diào)用 set_lock_button/1 來改變鎖門按鈕,被掛起的 button 調(diào)用會(huì)立刻生效,門被鎖住。因此,我們把當(dāng)前的門鎖按鈕作為狀態(tài)的一部分,這樣當(dāng)我們改變門鎖按鈕時(shí),狀態(tài)會(huì)改變,所有的延緩事件會(huì)重新觸發(fā)。

我們定義狀態(tài)為 {StateName,LockButton},其中 StateName 和之前一樣,而 LockButton 則表示當(dāng)前的鎖門按鈕:

-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_3).-export([start_link/2,stop/0]). -export([button/1,code_length/0,set_lock_button/1]). -export([init/1,callback_mode/0,terminate/3,code_change/4,format_status/2]). -export([handle_event/4]).start_link(Code, LockButton) ->gen_statem:start_link({local,?NAME}, ?MODULE, {Code,LockButton}, []). stop() ->gen_statem:stop(?NAME).button(Digit) ->gen_statem:call(?NAME, {button,Digit}). code_length() ->gen_statem:call(?NAME, code_length). set_lock_button(LockButton) ->gen_statem:call(?NAME, {set_lock_button,LockButton}).init({Code,LockButton}) ->process_flag(trap_exit, true),Data = #{code => Code, remaining => undefined},{ok, {locked,LockButton}, Data}.callback_mode() ->[handle_event_function,state_enter].handle_event({call,From}, {set_lock_button,NewLockButton},{StateName,OldLockButton}, Data) ->{next_state, {StateName,NewLockButton}, Data,[{reply,From,OldLockButton}]}; handle_event({call,From}, code_length,{_StateName,_LockButton}, #{code := Code}) ->{keep_state_and_data,[{reply,From,length(Code)}]}; %% %% State: locked handle_event(EventType, EventContent,{locked,LockButton}, #{code := Code, remaining := Remaining} = Data) ->case {EventType, EventContent} of{enter, _OldState} ->do_lock(),{keep_state, Data#{remaining := Code}};{timeout, _} ->{keep_state, Data#{remaining := Code}};{{call,From}, {button,Digit}} ->case Remaining of[Digit] -> % Complete{next_state, {open,LockButton}, Data,[{reply,From,ok}]};[Digit|Rest] -> % Incomplete{keep_state, Data#{remaining := Rest, 30000},[{reply,From,ok}]};[_|_] -> % Wrong{keep_state, Data#{remaining := Code},[{reply,From,ok}]}endend; %% %% State: open handle_event(EventType, EventContent,{open,LockButton}, Data) ->case {EventType, EventContent} of{enter, _OldState} ->do_unlock(),{keep_state_and_data, [{state_timeout,10000,lock}]};{state_timeout, lock} ->{next_state, {locked,LockButton}, Data};{{call,From}, {button,Digit}} ->ifDigit =:= LockButton ->{next_state, {locked,LockButton}, Data,[{reply,From,locked}]};true ->{keep_state_and_data,[postpone]}endend.do_lock() ->io:format("Locked~n", []). do_unlock() ->io:format("Open~n", []).terminate(_Reason, State, _Data) ->State =/= locked andalso do_lock(),ok. code_change(_Vsn, State, Data, _Extra) ->{ok,State,Data}. format_status(Opt, [_PDict,State,Data]) ->StateData ={State,maps:filter(fun (code, _) -> false;(remaining, _) -> false;(_, _) -> trueend,Data)},case Opt ofterminate ->StateData;normal ->[{data,[{"State",StateData}]}]end.

對(duì)現(xiàn)實(shí)中的鎖來說,button/1 在狀態(tài)變?yōu)?locked 前被掛起不合理。但是作為一個(gè) API,還好。

?

3.22 掛起

(譯者補(bǔ)充:此掛起跟前文的掛起不同,前文的掛起僅意味著 receive 阻塞。)

如果一個(gè)節(jié)點(diǎn)中有很多個(gè) server,并且他們在生命周期中某些時(shí)候會(huì)空閑,那么這些 server 的堆內(nèi)存會(huì)造成浪費(fèi),通過?proc_lib:hibernate/3 來掛起 server 會(huì)把它的內(nèi)存占用降到最低。

注意:掛起一個(gè)進(jìn)程代價(jià)很高,詳見 erlang:hibernate/3 。不要在每個(gè)事件之后都掛起它。

此例中我們可以在 {open,_} 狀態(tài)掛起,因?yàn)檎碚f只有在一段時(shí)間后它才會(huì)收到狀態(tài)超時(shí),遷移至 locked 狀態(tài):

... %% 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]}; ...

最后一行的動(dòng)作列表中 hibernate 是唯一的修改。如果任何事件在 {open,_} 狀態(tài)到達(dá),我們不用再重新掛起,接收事件后 server 會(huì)一直處于活躍狀態(tài)。

如果要重新掛起,我們需要在更多的地方插入 hibernate 來改變。例如,跟狀態(tài)無關(guān)的 set_lock_buttoncode_length 操作,在 {open,_} 狀態(tài)可以讓他 hibernate,但是這樣會(huì)讓代碼很亂。

另一個(gè)不常用的方法是使用事件超時(shí),在一段時(shí)間的不活躍后觸發(fā)掛起。

本例可能不值得使用掛起來降低堆內(nèi)存。只有在運(yùn)行中產(chǎn)生了垃圾的 server 才會(huì)從掛起中受益,從這個(gè)層面說,上面的是個(gè)不好的例子。

?

4?gen_event Behaviour

此章可結(jié)合 gen_event(3)(包含全部接口函數(shù)和回調(diào)函數(shù)的詳述)教程一起看。

4.1 事件處理原則

在 OTP 中,一個(gè)事件管理器(event manager)是一個(gè)可以接收事件的指定的對(duì)象。事件(event)可能是要記錄日志的錯(cuò)誤、警告、信息等等。

事件管理器中可以安裝(install)0個(gè)、1個(gè)或更多的事件處理器(event handler)。當(dāng)事件管理器收到一個(gè)事件通知,這個(gè)事件被所有安裝好的事件處理器處理。例如,一個(gè)處理錯(cuò)誤的事件管理器可能內(nèi)置一個(gè)默認(rèn)的處理器,把錯(cuò)誤寫到終端。如果某段時(shí)間需要把錯(cuò)誤信息寫到文件,用戶可以添加另一個(gè)處理器來處理。不需要再寫入文件時(shí),則可以刪除這個(gè)處理器。

事件管理器是一個(gè)進(jìn)程,而事件處理器則是一個(gè)回調(diào)模塊。

事件管理器本質(zhì)上就是維護(hù)一個(gè) {Module, State} 列表,其中 Module 是一個(gè)事件處理器,State 則是處理器的內(nèi)部狀態(tài)。

?

4.2 例子

將錯(cuò)誤信息寫到終端的事件處理器的回調(diào)模塊可能長這樣:

-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.

將錯(cuò)誤信息寫到文件的事件處理器的回調(diào)模塊可能長這樣:

-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).

下一小節(jié)分析這些代碼。

?

4.3 開啟一個(gè)事件管理器

調(diào)用下面的函數(shù)來開啟一個(gè)前例中說的處理錯(cuò)誤的事件管理器:

gen_event:start_link({local, error_man})

這個(gè)函數(shù)創(chuàng)建并連接一個(gè)新進(jìn)程(事件管理器 event manager)。

參數(shù) {local, error_man} 指定了事件管理器的名字,事件管理器在本地注冊為 error_man

如果名字參數(shù)被忽略,事件管理器不會(huì)被注冊,則必須用到它的進(jìn)程 pid。名字還可以用{global, Name},這樣的話會(huì)調(diào)用 global:register_name/2 來注冊事件管理器。

如果 gen_event 是一個(gè)監(jiān)控樹的一部分,supervisor 啟動(dòng) gen_event 時(shí)一定要使用 gen_event:start_link。還有一個(gè)函數(shù)是 gen_event:start ,這個(gè)函數(shù)會(huì)啟動(dòng)一個(gè)獨(dú)立的 gen_event,也就是說它不會(huì)成為監(jiān)控樹的一部分。

?

4.4 添加一個(gè)事件處理器

下例表明了在 shell 中,如何開啟一個(gè)事件管理器,并為它添加一個(gè)事件處理器:

1> gen_event:start({local, error_man}). {ok,<0.31.0>} 2> gen_event:add_handler(error_man, terminal_logger, []). ok

這個(gè)函數(shù)會(huì)發(fā)送一個(gè)消息給事件處理器 error_man,告訴它需要添加一個(gè)事件處理器 terminal_logger。事件管理器會(huì)調(diào)用函數(shù) terminal_logger:init([]) (init 的參數(shù) [] 是 add_handler 的第三個(gè)參數(shù))。正常的話 init 會(huì)返回 {ok, State},State就是事件處理器的內(nèi)部狀態(tài)。

init(_Args) ->{ok, []}.

此例中 init 不需要任何輸入,因此忽略了它的參數(shù)。terminal_logger 中不需要用到內(nèi)部狀態(tài),file_logger 可以用內(nèi)部狀態(tài)來保存文件描述符。

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 是事件。

這個(gè)事件會(huì)以消息的形式發(fā)送給事件處理器。接收事件時(shí),事件管理器會(huì)按照安裝的順序,依次調(diào)用每個(gè)事件處理器的 handle_event(Event, State)。handle_event 正常會(huì)返回元組 {ok,State1},其中 State1 是事件處理器的新的內(nèi)部狀態(tài)。

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

這個(gè)函數(shù)會(huì)發(fā)送一條消息給注冊名為 error_man 的事件管理器,告訴它要?jiǎng)h除處理器 terminal_logger。此時(shí)管理器會(huì)調(diào)用 terminal_logger:terminate([], State),其中 [] 是 delete_handler 的第三個(gè)參數(shù)。terminate 中應(yīng)該做與 init 相反的事情,做一些清理工作。它的返回值會(huì)被忽略。

terminal_logger 不需要做清理:

terminate(_Args, _State) ->ok.

file_logger 需要關(guān)閉 init 中開啟的文件描述符:

terminate(_Args, Fd) ->file:close(Fd).

?

4.7 終止

當(dāng)事件管理器被終止,它會(huì)調(diào)用每個(gè)處理器的 terminate/2,和刪除處理器時(shí)一樣。

在監(jiān)控樹中

如果管理器是監(jiān)控樹的一部分,則不需要終止函數(shù)。管理器自動(dòng)的被它的監(jiān)控者終止,具體怎么終止通過 終止策略 來決定。

獨(dú)立的事件管理器

事件管理器可以通過調(diào)用以下函數(shù)終止:

> gen_event:stop(error_man). ok

?

4.8 處理其他消息

如果想要處理事件之外的其他消息,需要實(shí)現(xiàn)回調(diào)函數(shù) handle_info(Info, StateName, StateData)。比如說 exit 消息,當(dāng) gen_event 與其他進(jìn)程(非它的監(jiān)控者)連接,并且被設(shè)置為捕捉 exit 信號(hào)。

handle_info({'EXIT', Pid, Reason}, State) ->..code to handle exits here..{ok, NewState}.

code_change 函數(shù)也需要實(shí)現(xiàn)。

code_change(OldVsn, State, Extra) ->..code to convert state (and more) during code change{ok, NewState}

?

5?Supervisor Behaviour

這部分可與 stdblib 中的 supervisor(3) 教程(包含了所有細(xì)節(jié))一起閱讀。

5.1 監(jiān)控原則

監(jiān)控者(supervisor)要負(fù)責(zé)開啟、終止和監(jiān)控它的子進(jìn)程。監(jiān)控者的基本理念就是通過必要時(shí)的重啟,來保證子進(jìn)程一直活著。

子進(jìn)程規(guī)格說明指定了要啟動(dòng)和監(jiān)控的子進(jìn)程。子進(jìn)程根據(jù)規(guī)格列表依次啟動(dòng),終止順序和啟動(dòng)順序相反。

?

5.2 例子

下面的例子是啟動(dòng) gen_server 子進(jìn)程的監(jiān)控樹:

-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,詳見下一小節(jié)。

ChildSpecs 是子進(jìn)程規(guī)格列表。

?

5.3?supervisor flag

下面是 supervisor flag 的類型定義:

sup_flags() = #{strategy => strategy(), % optionalintensity => non_neg_integer(), % optionalperiod => pos_integer()} % optionalstrategy() = one_for_all| one_for_one| rest_for_one| simple_one_for_one
  • strategy 指定了重啟策略。
  • intensityperiod 指定了最大重啟頻率。

?

5.4 重啟策略

重啟策略是由 init 返回的 map 中的 strategy 來指定的:

SupFlags = #{strategy => Strategy, ...}

strategy 是可選參數(shù),如果沒有指定,默認(rèn)為 one_for_one

one_for_one

如果子進(jìn)程終止,只有終止的子進(jìn)程會(huì)被重啟。

圖5.1 one_for_one 監(jiān)控樹

one_for_all

如果一個(gè)子進(jìn)程終止,其他子進(jìn)程都會(huì)被終止,然后所有子進(jìn)程被重啟。

圖5.2 one_for_all 監(jiān)控樹

rest_for_one

如果一個(gè)子進(jìn)程終止,啟動(dòng)順序在此子進(jìn)程之后的子進(jìn)程們都會(huì)被終止。然后這些終止的進(jìn)程(包括自己終止的那位)被重啟。

simple_one_for_one

詳見 simple-one-for-one supervisors(譯者補(bǔ)充:本原則中也有提及simple_one_for_one

?

5.5 最大重啟頻率

supervisor 內(nèi)置了一個(gè)機(jī)制來限制給定時(shí)間間隔內(nèi)的重啟次數(shù)。由 init 函數(shù)返回的 supervisor flag 中的 intensityperiod 字段來指定:

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

如果 MaxT 秒內(nèi)重啟了 MaxR 次,監(jiān)控者會(huì)終止所有的子進(jìn)程,然后退出。此時(shí) supervisor 退出的理由是 shutdown

當(dāng) supervisor 終止時(shí),它的上一級(jí) supervisor 會(huì)作出一些處理,重啟它,或者跟著退出。

這個(gè)重啟機(jī)制的目的是防止進(jìn)程反復(fù)因?yàn)橥辉蚪K止和重啟。

intensityperiod 都是可選參數(shù),如果沒有指定,它們缺省值分別為1和5。

調(diào)整 intensity 和 period

缺省值為5秒重啟1次。這個(gè)配置對(duì)大部分系統(tǒng)(即便是很深的監(jiān)控樹)來說都是保險(xiǎn)的,但你可能想為某些特殊的應(yīng)用場景做出調(diào)整。

首先,intensity 決定了你能忍受多少次突發(fā)重啟。例如,你只能接受5~10次的重啟嘗試(盡管下一秒它可能會(huì)重啟成功)。

其次,如果崩潰持續(xù)發(fā)生,但是沒有頻繁到讓 supervisor 放棄,你需要考慮持續(xù)的失敗率。比如說你把 intensity 設(shè)置為10,而 period 為1,supervisor 會(huì)允許子進(jìn)程在1秒內(nèi)重啟10次,在人工干預(yù)前它會(huì)持續(xù)往日志中寫入 crash 報(bào)告。

此時(shí)你需要把 period 設(shè)置得足夠大,讓 supervisor 在你能接受的比值下運(yùn)行。例如,你將 intensity 設(shè)置為5,period 為30s,會(huì)讓它在一段時(shí)間內(nèi)允許平均6s的重啟間隔,這樣你的日志就不會(huì)太快被填滿,你可以觀察錯(cuò)誤,然后作出修復(fù)。

這些選擇取決于你的問題作用域。如果你不會(huì)實(shí)時(shí)監(jiān)測或者不能快速解決問題(例如在嵌入式系統(tǒng)中),你可能想1分鐘最多重啟一次,把問題交給更高層去自動(dòng)清理錯(cuò)誤。或者有時(shí)候,可能高失敗率時(shí)仍然嘗試重啟是更好的選擇,你可以設(shè)置成一秒1-2次重啟。

避免一些常見的錯(cuò)誤:

  • 不要忘記考慮爆發(fā)率。如果你把 intensity 設(shè)置為1,period 為6,它的長期錯(cuò)誤率與5/30和10/60差不多,但是它不允許連續(xù)兩次重啟。這可能不是你想要的。
  • 如果想容忍爆發(fā),不要把 period 設(shè)置得很大。如果你把 intensity 設(shè)置為5,period 為3600(1小時(shí)),supervisor 允許短時(shí)間內(nèi)重啟5次,然而接近(但不到)一個(gè)小時(shí)的一次崩潰會(huì)導(dǎo)致它放棄。而這兩撥崩潰可能是不同原因?qū)е碌?#xff0c;所以設(shè)置為5到10分鐘會(huì)更合理。
  • 如果你的應(yīng)用包含多級(jí)監(jiān)控,不要簡單地把所有層的重啟頻率設(shè)置成相同的值。在頂層 supervisor 放棄重啟并終止應(yīng)用之前,重啟的總次數(shù)是崩潰的子進(jìn)程上層的所有 supervisor 的密度的乘積。

?????????? 例如,如果最上層允許10次重啟,第二層也允許10次,下層崩潰的子進(jìn)程會(huì)被重啟100次,這太多了。最上層允許3次重啟可能更好。

?

5.6 子進(jìn)程規(guī)格說明

下面是子進(jìn)程規(guī)格(child specification)的類型定義:?

child_spec() = #{id => child_id(), % mandatorystart => mfargs(), % mandatoryrestart => restart(), % optionalshutdown => shutdown(), % optionaltype => worker(), % optionalmodules => modules()} % optionalchild_id() = term()mfargs() = {M :: module(), F :: atom(), A :: [term()]}modules() = [module()] | dynamicrestart() = permanent | transient | temporaryshutdown() = brutal_kill | timeout()worker() = worker | supervisor
  • id 在 supervisor 內(nèi)部被用來識(shí)別不同的 child specificaton。

??????? ?? id 是必填項(xiàng)

????????? 有時(shí) id 會(huì)被稱為 name,現(xiàn)在一般都用 identifier 或者 id,但為了向后兼容,有時(shí)也能看到 name,例如在錯(cuò)誤信息中。

  • start 規(guī)定了啟動(dòng)子進(jìn)程的函數(shù)。它是一個(gè) 模塊-函數(shù)-參數(shù) 元組,用來傳遞給 apply(M, F, A) 。

?????? ? ? 它應(yīng)該(或者最終應(yīng)該)調(diào)用下面這些函數(shù):

    • supervisor:start_link
    • gen_server:start_link
    • gen_statem:start_link
    • gen_event:start_link
    • 跟這些函數(shù)類似的函數(shù)。詳見 supervisor(3) 的 start 參數(shù)。(譯者補(bǔ)充:函數(shù)應(yīng)滿足條件:創(chuàng)建并且連接到子進(jìn)程,且必須返回 {ok,Child}{ok,Child,Info},其中 Child 是子進(jìn)程 pid,Info 會(huì)被 supervisor 忽略)

?????? ? ? start 是必填項(xiàng)。

  • restart 規(guī)定了什么時(shí)候一個(gè)終止的進(jìn)程會(huì)觸發(fā)重啟
    • permanent 表示進(jìn)程總是觸發(fā)重啟
    • temporary 表示進(jìn)程不會(huì)被重啟(即便重啟策略是 rest_for_oneone_for_all,前置進(jìn)程導(dǎo)致 temporary 進(jìn)程終止
    • transient 僅在進(jìn)程異常退出時(shí)重啟,即:終止理由不是 normal、shutdown 或 {shutdown,Term}

???????? ? restart 是可選項(xiàng),缺省值為 permanent

  • shutdown 規(guī)定了進(jìn)程被終止的方式
    • brutal_kill 表示會(huì)使用 exit(Child, kill) 無條件終止子進(jìn)程。
    • 一個(gè)整數(shù)超時(shí)值,意味著 supervisor 會(huì)調(diào)用 exit(Child, shutdown) 通知子進(jìn)程退出,然后等待退出信號(hào)返回。如果指定時(shí)間內(nèi)沒有收到退出信號(hào),子進(jìn)程會(huì)被 exit(Child, kill) 無條件終止。
    • 如果子進(jìn)程是一個(gè) supervisor,可以設(shè)置為 infinity 來讓子監(jiān)控樹有足夠的時(shí)間退出。如果子進(jìn)程是 worker 也可以設(shè)置為 infinity。警告:
警告:當(dāng)子進(jìn)程是 worker 時(shí)慎用 infinity。因?yàn)檫@種情況下,監(jiān)控樹的退出取決于子進(jìn)程的退出,必須要安全地實(shí)現(xiàn)子進(jìn)程,確保它的清理過程必定會(huì)返回。

????? ? ?? shutdown 是可選項(xiàng),如果子進(jìn)程是 worker,默認(rèn)為 5000;如果子進(jìn)程是監(jiān)控樹,默認(rèn)為 infinity。

  • type 標(biāo)明子進(jìn)程是 worker 還是 supervisor

?????? ? ? type 是可選項(xiàng),缺省值為 worker

  • modules 當(dāng) Module 是回調(diào)模塊名,modules 是單元素的列表 [Module](子進(jìn)程為 supervisor, gen_server, gen_statem);如果子進(jìn)程是 gen_event,值應(yīng)該為 dynamic。

?????????? 這個(gè)字段在發(fā)布管理的升級(jí)和降級(jí)中會(huì)用到,詳見 Release Handling。

?????????? modules 是可選項(xiàng),缺省值為 [M],其中 M 來自子進(jìn)程的啟動(dòng)參數(shù) {M,F,A}

:前例中 ch3 的子進(jìn)程規(guī)格如下:

#{id => ch3,start => {ch3, start_link, []},restart => permanent,shutdown => brutal_kill,type => worker,modules => [ch3]}

或者簡化一下,取默認(rèn)值:

#{id => ch3,start => {ch3, start_link, []}shutdown => brutal_kill}

例:上文的 gen_event 子進(jìn)程規(guī)格如下:

#{id => error_man,start => {gen_event, start_link, [{local, error_man}]},modules => dynamic}

這兩個(gè)都是注冊進(jìn)程,都被期望一直能訪問到。所以他們被指定為 permanent

ch3 在終止前不需要做任何清理工作,所以不需要指定終止時(shí)間,shudown 值設(shè)置為 brutal_kill 就行了。而 error_man 需要時(shí)間去清理,所以設(shè)置為5000毫秒(默認(rèn)值)。

例:啟動(dòng)另一個(gè) supervisor 的子進(jìn)程規(guī)格:

#{id => sup,start => {sup, start_link, []},restart => transient,type => supervisor} % will cause default shutdown=>infinity (type為supervisor會(huì)導(dǎo)致shutdown的默認(rèn)值為infinity)

?

5.7 啟動(dòng)supervisor

前例中,supervisor 通過調(diào)用 ch_sup:start_link() 來啟動(dòng):

start_link() ->supervisor:start_link(ch_sup, []).

ch_sup:start_link?函數(shù)調(diào)用 supervisor:start_link/2,生成并連接了一個(gè)新進(jìn)程(supervisor)。

  • 第一個(gè)參數(shù),ch_sup 是回調(diào)模塊的名字,也就是 init 函數(shù)所在的模塊
  • 第二個(gè)參數(shù),[],是傳遞給 init 函數(shù)的參數(shù),此例中 init 不需要任何輸入,忽略了此參數(shù)

此例中 supervisor 沒有被注冊,因此必須用到它的 pid。可以通過調(diào)用 supervisor:start_link({local, Name}, Module, Args)supervisor:start_link({global, Name}, Module, Args) 來指定它的名字。

這個(gè)新的?supervisor 進(jìn)程會(huì)調(diào)用 init 回調(diào) ch_sup:init([])。init 函數(shù)應(yīng)該返回 {ok, {SupFlags, ChildSpecs}}

init(_Args) ->SupFlags = #{},ChildSpecs = [#{id => ch3,start => {ch3, start_link, []},shutdown => brutal_kill}],{ok, {SupFlags, ChildSpecs}}.

然后 supervisor 會(huì)根據(jù)子進(jìn)程規(guī)格列表,啟動(dòng)所有的子進(jìn)程。此例中只有一個(gè)子進(jìn)程,ch3 。

supervisor:start_link 是同步調(diào)用,在所有子進(jìn)程啟動(dòng)之前它不會(huì)返回。

?

5.8 增加子進(jìn)程

除了靜態(tài)的監(jiān)控樹外,還可以動(dòng)態(tài)地添加子進(jìn)程到監(jiān)控樹中:

supervisor:start_child(Sup, ChildSpec)

Sup 是 supervisor 的 pid 或注冊名。ChildSpec 是子進(jìn)程規(guī)格。

使用 start_child/2 添加的子進(jìn)程跟其他子進(jìn)程行為一樣,除了一點(diǎn):如果 supervisor 終止并被重啟,所有動(dòng)態(tài)添加的進(jìn)程都會(huì)丟失。

?

5.9 終止子進(jìn)程

調(diào)用下面的函數(shù),靜態(tài)或動(dòng)態(tài)的子進(jìn)程,都會(huì)根據(jù)規(guī)格終止:

supervisor:terminate_child(Sup, Id)

一個(gè)終止的子進(jìn)程的規(guī)格可通過下面的函數(shù)刪除:

supervisor:delete_child(Sup, Id)

Sup 是 supervisor 的 pid 或注冊名。Id 是子進(jìn)程規(guī)格中的 id 項(xiàng)。

刪除靜態(tài)的子進(jìn)程規(guī)格會(huì)導(dǎo)致它跟動(dòng)態(tài)子進(jìn)程一樣,在 supervisor 重啟時(shí)丟失。

?

5.10 簡化的 one_for_one(simple_one_for_one)

重啟策略 simple_one_for_one 是簡化的 one_for_one,所有的子進(jìn)程是相同過程的實(shí)例,被動(dòng)態(tài)地添加到監(jiān)控樹中。

下面是一個(gè) simple_one_for_one 的 supervisor 回調(diào)模塊:

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

啟動(dòng)時(shí),supervisor 沒有啟動(dòng)任何子進(jìn)程。所有的子進(jìn)程是通過調(diào)用如下函數(shù)動(dòng)態(tài)添加的:

supervisor:start_child(Pid, [id1])

子進(jìn)程會(huì)通過調(diào)用 apply(call, start_link, []++[id1]) 來啟動(dòng),即:

call:start_link(id1)

simple_one_for_one 監(jiān)程的子進(jìn)程通過下面的方式來終止:

supervisor:terminate_child(Sup, Pid)

Sup 是 supervisor 的 pid 或注冊名。Pid 是子進(jìn)程的 pid。

由于 simple_one_for_one 的監(jiān)程可能有大量的子進(jìn)程,所以它是異步終止它們的。就是說子進(jìn)程平行地做清理工作,終止順序不可預(yù)測。

?

5.11 終止

由于 supervisor 是監(jiān)控樹的一部分,它會(huì)自動(dòng)地被它的 supervisor 終止。當(dāng)被要求終止時(shí),它會(huì)根據(jù) shutdown 配置按照與啟動(dòng)相反的順序(譯者補(bǔ)充:除了 simple_one_for_one 模式)終止所有的子進(jìn)程,然后退出。

?

6?sys and proc_lib

sys 模塊包含一些函數(shù),可以簡單地 debug 用 behaviour 實(shí)現(xiàn)的進(jìn)程。還有一些函數(shù)可以和 proc_lib 模塊的函數(shù)一起,用來實(shí)現(xiàn)特殊的進(jìn)程,這些特殊的進(jìn)程不采用標(biāo)準(zhǔn)的 behaviour,但是滿足 OTP 設(shè)計(jì)原則。這些函數(shù)還可以用來實(shí)現(xiàn)用戶自定義(非標(biāo)準(zhǔn))的 behaviour。

sysproc_lib 模塊都屬于 STDLIB 應(yīng)用。

6.1 簡易debug

sys 模塊包含一些函數(shù),可以簡單地 debug 用 behaviour 實(shí)現(xiàn)的進(jìn)程。用 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 特殊的進(jìn)程

此小節(jié)講述怎么不使用標(biāo)準(zhǔn) behaviour 來寫一個(gè)程序,使它滿足 OTP 設(shè)計(jì)原則。這樣一個(gè)進(jìn)程需要滿足:

  • 提供啟動(dòng)方式使它可以納入監(jiān)控樹中
  • 支持 sys 的debug工具
  • 關(guān)心系統(tǒng)消息

系統(tǒng)消息是在監(jiān)控樹中用到的、有特殊意義的消息。典型的系統(tǒng)消息有追蹤輸出的請求、掛起或恢復(fù)進(jìn)程的請求(release handling 發(fā)布管理中用到)。使用標(biāo)準(zhǔn) behaviour 實(shí)現(xiàn)的進(jìn)程能自動(dòng)處理這些消息。

例子

概述里面的簡單服務(wù)器,使用 sys 和 proc_lib 來實(shí)現(xiàn)以使其可納入監(jiān)控樹中:

-module(ch4). -export([start_link/0]). -export([alloc/0, free/1]). -export([init/1]). -export([system_continue/3, system_terminate/4,write_debug/3,system_get_state/1, system_replace_state/2]).start_link() ->proc_lib:start_link(ch4, init, [self()]).alloc() ->ch4 ! {self(), alloc},receive{ch4, Res} ->Resend.free(Ch) ->ch4 ! {free, Ch},ok.init(Parent) ->register(ch4, self()),Chs = channels(),Deb = sys:debug_options([]),proc_lib:init_ack(Parent, {ok, self()}),loop(Chs, Parent, Deb).loop(Chs, Parent, Deb) ->receive{From, alloc} ->Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, alloc, From}),{Ch, Chs2} = alloc(Chs),From ! {ch4, Ch},Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3,ch4, {out, {ch4, Ch}, From}),loop(Chs2, Parent, Deb3);{free, Ch} ->Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3,ch4, {in, {free, Ch}}),Chs2 = free(Ch, Chs),loop(Chs2, Parent, Deb2);{system, From, Request} ->sys:handle_system_msg(Request, From, Parent,ch4, Deb, Chs)end.system_continue(Parent, Deb, Chs) ->loop(Chs, Parent, Deb).system_terminate(Reason, _Parent, _Deb, _Chs) ->exit(Reason).system_get_state(Chs) ->{ok, Chs}.system_replace_state(StateFun, Chs) ->NChs = StateFun(Chs),{ok, NChs, NChs}.write_debug(Dev, Event, Name) ->io:format(Dev, "~p event = ~p~n", [Name, Event]).

sys 模塊中的簡易 debug 也可用于 ch4:

% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0]Eshell V5.2.3.6 (abort with ^G) 1> ch4:start_link(). {ok,<0.30.0>} 2> sys:statistics(ch4, true). ok 3> sys:trace(ch4, true). ok 4> ch4:alloc(). ch4 event = {in,alloc,<0.25.0>} ch4 event = {out,{ch4,ch1},<0.25.0>} ch1 5> ch4:free(ch1). ch4 event = {in,{free,ch1}} ok 6> sys:statistics(ch4, get). {ok,[{start_time,{{2003,6,13},{9,47,5}}},{current_time,{{2003,6,13},{9,47,56}}},{reductions,109},{messages_in,2},{messages_out,1}]} 7> sys:statistics(ch4, false). ok 8> sys:trace(ch4, false). ok 9> sys:get_status(ch4). {status,<0.30.0>,{module,ch4},[[{'$ancestors',[<0.25.0>]},{'$initial_call',{ch4,init,[<0.25.0>]}}],running,<0.25.0>,[],[ch1,ch2,ch3]]}

啟動(dòng)進(jìn)程

proc_lib 中的一些函數(shù)可用來啟動(dòng)進(jìn)程。有幾個(gè)函數(shù)可選,如:異步啟動(dòng) spawn_link/3,4 和同步啟動(dòng) start_link/3,4,5

使用這些函數(shù)啟動(dòng)的進(jìn)程會(huì)存儲(chǔ)一些信息(比如高層級(jí)進(jìn)程 ancestor 和初始化回調(diào) initial call),這些信息在監(jiān)控樹中會(huì)被用到。

如果進(jìn)程以除 normal 或 shutdown 之外的理由終止,會(huì)生成一個(gè) crash 報(bào)告。可以在 SASL 的用戶手冊中了解更多 crash 報(bào)告的內(nèi)容。

此例中,使用了同步啟動(dòng)。進(jìn)程通過 ch4:start_link() 來啟動(dòng):

start_link() ->proc_lib:start_link(ch4, init, [self()]).

ch4:start_link 調(diào)用了函數(shù) proc_lib:start_link 。這個(gè)函數(shù)的參數(shù)為模塊名、函數(shù)名和參數(shù)列表,它創(chuàng)建并連接到一個(gè)新進(jìn)程。新進(jìn)程執(zhí)行給定的函數(shù)來啟動(dòng),ch4:init(Pid),其中 Pid 是第一個(gè)進(jìn)程的 pid,即父進(jìn)程。

所有的初始化(包括名字注冊)都在 init 中完成。新進(jìn)程需要通知父進(jìn)程它的啟動(dòng):

init(Parent) ->...proc_lib:init_ack(Parent, {ok, self()}),loop(...).

proc_lib:start_link 是同步函數(shù),在 proc_lib:init_ack 被調(diào)用前不會(huì)返回。

Debugging

要支持 sys 的 debug 工具,需要 debug 結(jié)構(gòu)。Deb 通過 sys:debug_options/1 來初始生成:

init(Parent) ->...Deb = sys:debug_options([]),...loop(Chs, Parent, Deb).

sys:debug_options/1 的參數(shù)為一個(gè)選項(xiàng)列表。此例中列表為空,即初始時(shí)沒有 debug 被啟用。可用選項(xiàng)詳見 sys 模塊的用戶手冊。

然后,對(duì)于每個(gè)要記錄或追蹤的系統(tǒng)事件,下面的函數(shù)會(huì)被調(diào)用:

sys:handle_debug(Deb, Func, Info, Event) => Deb1

其中:

  • Deb 是 debug 結(jié)構(gòu)
  • Func 指定了一個(gè)用戶自定義的函數(shù),用來格式化追蹤輸出。對(duì)于每個(gè)系統(tǒng)事件,格式化函數(shù)會(huì)被調(diào)用 Func(Dev, Event, Info),其中:
    • Dev 是要輸出到的 I/0 設(shè)備,詳見 io 模塊的手冊。
    • EventInfo 是從 handle_debug 傳入的。
  • Info 用來傳遞更多信息給 Func,可以是任何類型,會(huì)原樣傳給 Func。
  • Event 是系統(tǒng)事件。用戶可以決定系統(tǒng)事件的定義和表現(xiàn)形式。一般至少輸入和輸出消息會(huì)被認(rèn)為是系統(tǒng)事件,分別用 {in,Msg[,From]} {out,Msg,To} 表示。

handle_debug 返回一個(gè)更新的 debug 結(jié)構(gòu) Deb1。

此例中,handle_debug 會(huì)在每次輸入和輸出信息時(shí)被調(diào)用。格式化函數(shù) Funcch4:write_debug/3,它調(diào)用 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]).

處理系統(tǒng)消息

收到的系統(tǒng)消息形如:

{system, From, Request}

這些消息的內(nèi)容和意義,進(jìn)程不需要理解,而是直接調(diào)用下面的函數(shù):

sys:handle_system_msg(Request, From, Parent, Module, Deb, State)

這個(gè)函數(shù)不會(huì)返回。它處理了系統(tǒng)消息之后,如果要繼續(xù)執(zhí)行,會(huì)調(diào)用:

Module:system_continue(Parent, Deb, State)

如果進(jìn)程終止,調(diào)用:

Module:system_terminate(Reason, Parent, Deb, State)

監(jiān)控樹中的進(jìn)程應(yīng)以父進(jìn)程相同的理由退出。

  • RequestFrom 是從系統(tǒng)消息中原樣傳遞的。
  • Parent 是父進(jìn)程的 pid。
  • Module 是模塊名。
  • Deb 是 debug 結(jié)構(gòu)。
  • State 是描述內(nèi)部狀態(tài)的項(xiàng),會(huì)被傳遞給 system_continue/system_terminate/ system_get_state/system_replace_state

如果進(jìn)程要返回它的狀態(tài),handle_system_msg 會(huì)調(diào)用:

Module:system_get_state(State)

如果進(jìn)程要調(diào)用函數(shù) StateFun 替換它的狀態(tài),handle_system_msg 會(huì)調(diào)用:

Module:system_replace_state(StateFun, State)

此例中對(duì)應(yīng)代碼:

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

如果這個(gè)特殊的進(jìn)程設(shè)置為捕捉 exit 信號(hào),并且父進(jìn)程終止,它的預(yù)期行為是以同樣的理由終止:

init(...) ->...,process_flag(trap_exit, true),...,loop(...).loop(...) ->receive...{'EXIT', Parent, Reason} ->..maybe some cleaning up here..exit(Reason);...end.

?

6.3 自定義behaviour

要實(shí)現(xiàn)自定義 behaviour,代碼跟特殊進(jìn)程差不多,除了要調(diào)用回調(diào)模塊里的函數(shù)來處理特殊的任務(wù)。

如果想要編譯器像對(duì) OTP 的 behaviour 一樣,給缺少的回調(diào)函數(shù)報(bào)警告,需要在 behaviour 模塊增加 -callback 屬性來描述預(yù)期的回調(diào):

-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 是預(yù)期的回調(diào)名。ArgX_YResXTypes and Function Specifications 中所描述的類型。-callback 屬性支持 -spec 的所有語法。

-optional_callbacks 屬性可以用來指定可選的回調(diào):

-optional_callbacks([OptName1/OptArity1, ..., OptNameK/OptArityK]).

其中每個(gè) OptName/OptArity 指定了一個(gè)回調(diào)函數(shù)的名字和參數(shù)個(gè)數(shù)。-optional_callbacks 應(yīng)與 -callback 一起使用,它不能與下文的 behaviour_info() 結(jié)合使用。

注意:我們推薦使用 -callback 而不是 behaviour_info() 函數(shù)。因?yàn)楣ぞ呖梢杂妙~外的類型信息來生成文檔和找出矛盾。

你也可以實(shí)現(xiàn)并導(dǎo)出 behaviour_info() 來替代 -callback-optional_callbacks 屬性:

behaviour_info(callbacks) ->[{Name1, Arity1},...,{NameN, ArityN}].

其中每個(gè) {Name, Arity} 指定了回調(diào)函數(shù)的名字和參數(shù)個(gè)數(shù)。使用 -callback 屬性會(huì)自動(dòng)生成這個(gè)函數(shù)。

當(dāng)編譯器在模塊 Mod 中遇到屬性 -behaviour(Behaviour),它會(huì)調(diào)用 Behaviour:behaviour_info(callbacks),并且與 Mod 實(shí)際導(dǎo)出的函數(shù)集相比較,在缺少回調(diào)函數(shù)的時(shí)候發(fā)布一個(gè)警告。

例:

%% 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, ...)....

在回調(diào)模塊中:

-module(db). -behaviour(simple_server).-export([init/1, handle_req/2, terminate/0])....

behaviour 模塊中 -callback 屬性指定的協(xié)議,在回調(diào)模塊中可以添加 -spec 屬性來優(yōu)化。-callback 指定的協(xié)議一般都比較寬泛,所以 -spec 會(huì)非常有用。有協(xié)議的回調(diào)模塊:

-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()}....

每個(gè) -spec 協(xié)議都是對(duì)應(yīng)的 -callback 協(xié)議的子類型。

?

7?Applications

此部分可與 Kernel 手冊中的 app 和 application 部分一起閱讀。

7.1 應(yīng)用概念

如果你編碼實(shí)現(xiàn)了一些特定的功能,你可能想把它封裝成一個(gè)應(yīng)用,可以作為一個(gè)整體啟動(dòng)和終止,在其他系統(tǒng)中可以重用等。

要做到這一點(diǎn),需要?jiǎng)?chuàng)建一個(gè)應(yīng)用回調(diào)模塊,描述怎么啟動(dòng)和終止這個(gè)應(yīng)用。

然后還需要一個(gè)應(yīng)用規(guī)格說明(application specification),把它放在應(yīng)用資源文件中。這個(gè)文件指定了組成應(yīng)用的模塊列表以及回調(diào)模塊名。

如果你使用 Erlang/OTP 的代碼打包工具 systools(詳見 Releases),每個(gè)應(yīng)用的代碼都放在不同的目錄下,并遵循預(yù)定義的目錄結(jié)構(gòu) 。

?

7.2 應(yīng)用回調(diào)模塊

在下面兩個(gè)回調(diào)函數(shù)中,指定了怎么啟動(dòng)和終止應(yīng)用(即監(jiān)控樹):

start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State} stop(State)
  • start 函數(shù)在啟動(dòng)應(yīng)用的時(shí)候被調(diào)用,它通過啟動(dòng)頂層的 supervisor 來創(chuàng)建監(jiān)控樹。正常它會(huì)返回頂層 supervisor 的pid,和一個(gè)可選字段 State(默認(rèn)為 [] )。State 會(huì)傳遞給 stop 函數(shù)。
  • StartType 通常是 normal 。只有在接管或故障切換(譯者補(bǔ)充:分布式應(yīng)用提供的功能)時(shí)它會(huì)有其他值,詳見 Distributed Applications 。
  • StartArgs 在應(yīng)用資源文件中由 mod 指定。
  • stop/1 在應(yīng)用停止之后調(diào)用,用來做清理工作。實(shí)際的應(yīng)用終止(即監(jiān)控樹的終止)是自動(dòng)處理的,詳見啟動(dòng)和終止應(yīng)用。

打包前文 Supervisor Behaviour 的監(jiān)控樹為一個(gè)應(yīng)用,應(yīng)用回調(diào)模塊如下:

-module(ch_app). -behaviour(application).-export([start/2, stop/1]).start(_Type, _Args) ->ch_sup:start_link().stop(_State) ->ok.

庫應(yīng)用不需要啟動(dòng)和終止,所以不需要應(yīng)用回調(diào)模塊。

?

7.3 應(yīng)用資源文件

應(yīng)用的規(guī)格說明用來配置一個(gè)應(yīng)用,它放在應(yīng)用資源文件中,簡稱 .app 文件:

{application, Application, [Opt1,...,OptN]}.
  • Application,atom 類型,應(yīng)用的名字。資源文件名必須為 Application.app 。
  • 每個(gè) Opt 都是 {Key,Value} 元組,指定了應(yīng)用的一個(gè)特定屬性。所有的 key 都是可選項(xiàng),每個(gè) key 都有缺省值。

庫應(yīng)用的最簡短的 .app 文件長這樣(libapp 應(yīng)用):

{application, libapp, []}.

有監(jiān)控樹的應(yīng)用最簡短的 .app 文件長這樣(ch_app 應(yīng)用):

{application, ch_app,[{mod, {ch_app,[]}}]}.

mod 定義了應(yīng)用的回調(diào)模塊(ch_app)和啟動(dòng)參數(shù)([]),應(yīng)用啟動(dòng)時(shí)會(huì)調(diào)用:

ch_app:start(normal, [])

應(yīng)用終止后會(huì)調(diào)用:

ch_app:stop([])

當(dāng)使用 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 - 簡短的描述,字符串,默認(rèn)為 ""。

  • vsn - 版本號(hào),字符串,默認(rèn)為 ""。

  • modules - 應(yīng)用引入的所有模塊,在生成啟動(dòng)腳本和 tar 文件的時(shí)候 systools 會(huì)用到此列表。默認(rèn)為 [] 。

  • registered - 應(yīng)用中所有注冊的進(jìn)程名。systools 會(huì)用它來檢測應(yīng)用間的名字沖突。默認(rèn)為 [] 。

  • applications - 所有必須在此應(yīng)用啟動(dòng)前啟動(dòng)的應(yīng)用。systools 會(huì)用這個(gè)列表來生成正確的啟動(dòng)腳本。默認(rèn)為 [] 。注意,所有的應(yīng)用都至少依賴于 Kernel 和 STDLIB 應(yīng)用。
注意:應(yīng)用資源文件的語法和內(nèi)容,詳見Kernel中的app手冊

?

7.4 目錄結(jié)構(gòu)

使用 systools 來打包代碼,每個(gè)應(yīng)用的代碼會(huì)放在單獨(dú)的目錄下:lib/Application-Vsn,其中 Vsn 是版本號(hào)。

即便不用 systools 打包,由于 Erlang 是根據(jù) OTP 原則打包,它會(huì)有一個(gè)特定的目錄結(jié)構(gòu)。如果應(yīng)用存在多個(gè)版本,code server(詳見 code(3) )會(huì)自動(dòng)使用版本號(hào)最高的目錄的代碼。

開發(fā)環(huán)境的目錄結(jié)構(gòu)準(zhǔn)則

只要發(fā)布環(huán)境的目錄結(jié)構(gòu)遵循規(guī)定,開發(fā)目錄結(jié)構(gòu)怎么樣都行,但還是建議在開發(fā)環(huán)境中使用相同的目錄結(jié)構(gòu)。目錄名中的版本號(hào)要略掉,因?yàn)榘姹臼前l(fā)布步驟的一部分。

有些子目錄是必須的。有些子目錄是可選的,應(yīng)用需要才有。還有些子目錄是推薦有的,也就是說建議您按下面說的使用它。例如,文檔 doc 和測試 test 目錄是建議在應(yīng)用中包含的,以成為一個(gè)合格的 OTP 應(yīng)用。

─ ${application}├── doc│ ├── internal│ ├── examples│ └── src├── include├── priv├── src│ └── ${application}.app.src└── test
  • src - 必須。容納 Erlang 源碼、.app 文件和應(yīng)用內(nèi)部使用的 include 文件。src 中可以創(chuàng)建子目錄用以組織源文件。子目錄不能超過一層。
  • priv - 可選。存放應(yīng)用相關(guān)的文件。
  • include - 可選。存放能被其他應(yīng)用訪問到的 include 文件。
  • doc - 推薦。所有的源文檔應(yīng)放在此目錄的子目錄下。
  • doc/internal - 推薦。應(yīng)用的實(shí)現(xiàn)細(xì)節(jié)(不對(duì)外)相關(guān)文檔放在此處。
  • doc/examples - 推薦。存放示例源碼。建議大家把對(duì)外文檔的示例放在此處。
  • doc/src - 推薦。存放所有的文檔源文件(包括Markdown、AsciiDoc 和 XML 文件)。
  • test - 推薦。存放測試相關(guān)的所有文件,包括測試規(guī)范和測試集等。

開發(fā)環(huán)境可能還需要其他文件夾。例如,如果有其他語言的源碼,比如說 C 語言寫的 NIF,應(yīng)該把它們放在其他目錄。按照慣例,應(yīng)該以語言名為前綴命名目錄,比如說 C 語言用 c_src,Java 用 java_src,Go 用 go_src 。后綴 _src 意味著這個(gè)文件夾里的文件是編譯和應(yīng)用步驟中的一部分。最終構(gòu)建好的文件應(yīng)放在 priv/libpriv/bin 目錄下。

priv 目錄存放應(yīng)用運(yùn)行時(shí)需要的資源。可執(zhí)行文件應(yīng)放在 priv/bin 目錄,動(dòng)態(tài)鏈接應(yīng)放在 priv/bin 目錄。其他資源可以隨意放在 priv 目錄下,不過最好用結(jié)構(gòu)化的方式組織。

生成 erlang 代碼的其他語言代碼,比如 ASN.1 和 Mibs,應(yīng)該放在頂層目錄或 src 目錄的子目錄中,子目錄以語言名命名(如 asn1 和 mibs)。構(gòu)建文件應(yīng)放在相應(yīng)的語言目錄下,比如 erlang 對(duì)應(yīng) src 目錄,java 對(duì)應(yīng) java_src 目錄。

開發(fā)環(huán)境的 .app 文件可能放在 ebin 目錄下,不過建議在構(gòu)建時(shí)再把它放過去。慣常做法是使用 .app.src 文件,存放在 src 目錄。.app.src 文件和 .app 文件基本上是一樣的,只是某些字段會(huì)在構(gòu)建階段被替換,比如應(yīng)用版本號(hào)。

目錄名不應(yīng)該用大寫字母。

建議刪掉空目錄。

發(fā)布環(huán)境的目錄結(jié)構(gòu)

?應(yīng)用的發(fā)布版必須遵循特定的目錄結(jié)構(gòu)。

─ ${application}-${version}├── bin├── doc│ ├── html│ ├── man[1-9]│ ├── pdf│ ├── internal│ └── examples├── ebin│ └── ${application}.app├── include├── priv│ ├── lib│ └── bin└── src
  • src - 可選。容納 Erlang 源碼、.app 文件和應(yīng)用內(nèi)部使用的 include 文件。發(fā)布版本中不必要用到。
  • ebin - 必須。包含 Erlang 目標(biāo)代碼 beam 文件,.app 文件也必須要放在這里。
  • priv - 可選。存放應(yīng)用相關(guān)的文件,可用 code:priv_dir/1 函數(shù)訪問此目錄。
  • priv/lib - 推薦。存放應(yīng)用需要用到的共享對(duì)象( shared-object )文件,比如 NIF 或 linked-in-driver 。
  • priv/bin - 推薦。存放應(yīng)用需要用到的可執(zhí)行文件,例如 port-program 。
  • include - 可選。存放能被其他應(yīng)用訪問到的 include 文件。
  • bin - 可選。存放應(yīng)用生成的可執(zhí)行文件,比如 escript 或 shell-script。
  • doc - 可選。存放發(fā)布文檔。
  • doc/man1 - 推薦。存放應(yīng)用可執(zhí)行文件的幫助文檔。
  • doc/man3 - 推薦。存放模塊 API 的幫助文檔。
  • doc/man6 - 推薦。存放應(yīng)用概述幫助文檔。
  • doc/html - 可選。存放應(yīng)用的 html 文檔。
  • doc/pdf - 可選。存放應(yīng)用的 pdf 文檔。

src 目錄可用于 debug,但不是必須有的。include 目錄只有在應(yīng)用有公開的 include 文件時(shí)會(huì)用到。

推薦大家以上面的方式發(fā)布幫助文檔(doc/man...),一般 HTML 和 PDF 會(huì)以其他方式發(fā)布。

建議刪掉空目錄。

?

7.5 應(yīng)用控制器(application controller)

當(dāng) erlang 運(yùn)行時(shí)系統(tǒng)啟動(dòng),Kernel 應(yīng)用會(huì)啟動(dòng)很多進(jìn)程,其中一個(gè)進(jìn)程是應(yīng)用控制器(application controller)進(jìn)程,注冊名為 application_controller

應(yīng)用的所有操作都是通過控制器來協(xié)調(diào)的。它使用了 application 模塊的一些函數(shù),詳見 application 模塊的文檔。它控制應(yīng)用的加載、卸載、啟動(dòng)和終止。

?

7.6 加載和卸載應(yīng)用

應(yīng)用啟動(dòng)前,一定要先加載它。控制器會(huì)讀取并存儲(chǔ) .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"}]

終止或者未啟動(dòng)的應(yīng)用可以被卸載。卸載時(shí),應(yīng)用的信息會(huì)從控制器的內(nèi)部數(shù)據(jù)庫中清除:

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"}] 注意:加載或卸載應(yīng)用不會(huì)加載或卸載應(yīng)用的代碼。代碼加載是以平時(shí)的方式處理的。

?

7.7 啟動(dòng)和終止應(yīng)用

啟動(dòng)應(yīng)用:

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"}]

如果應(yīng)用沒被加載,控制器會(huì)先調(diào)用 application:load/1 來加載它。它校驗(yàn) applications 的值,確保這個(gè)配置中的所有應(yīng)用在此應(yīng)用運(yùn)行前都已經(jīng)啟動(dòng)了。

然后控制器為應(yīng)用創(chuàng)建一個(gè) application master 。這個(gè) master 是應(yīng)用中所有進(jìn)程的組長。master 通過調(diào)用應(yīng)用回調(diào)函數(shù) start/2 來啟動(dòng)應(yīng)用,應(yīng)用回調(diào)由 mod 配置指定。

調(diào)用下面的函數(shù),應(yīng)用會(huì)被終止,但不會(huì)被卸載:

7> application:stop(ch_app). ok

master 通過 shutdown 頂層 supervisor 來終止應(yīng)用。頂層 supervisor 通知它所有的子進(jìn)程終止,層層下推,整個(gè)監(jiān)控樹會(huì)以與啟動(dòng)相反的順序終止。然后 master 會(huì)調(diào)用回調(diào)函數(shù) stop/1(mod 配置指定的應(yīng)用回調(diào)模塊)

?

7.8 配置應(yīng)用

可以通過配置參數(shù)來配置應(yīng)用。配置參數(shù)就是 .app 文件中的 env 字段對(duì)應(yīng)的一個(gè) {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 必須是一個(gè) atom,Val 可以是任意類型。可以調(diào)用 application:get_env(App, Par) 來獲取配置參數(shù),還有一組類似函數(shù),詳見 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 文件中的配置值會(huì)被系統(tǒng)配置文件中的配置覆蓋。配置文件包含了相關(guān)應(yīng)用的配置參數(shù):

[{Application1, [{Par11,Val11},...]},...,{ApplicationN, [{ParN1,ValN1},...]}].

系統(tǒng)配置文件名為 Name.config,erlang 啟動(dòng)時(shí)可通過命令行參數(shù) -config Name 來指定配置文件。詳見 Kernel 模塊的 config 文檔。

例:

文件 test.config 內(nèi)容如下:

[{ch_app, [{file, "testlog"}]}].

file 的值會(huì)覆蓋 .app 文件中 file 對(duì)應(yīng)的值:

% 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 ,只能使用一個(gè)系統(tǒng)配置文件:sys.config 。

.app 文件和系統(tǒng)配置文件中的值都會(huì)被命令行中指定的值覆蓋:

% 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 應(yīng)用啟動(dòng)類型

啟動(dòng)類型在應(yīng)用啟動(dòng)時(shí)指定:

application:start(Application, Type)

application:start(Application) 相當(dāng)于 application:start(Application, temporary) 。Type 還可以是 permanenttransient:

  • permanent 意味著應(yīng)用終止的時(shí)候,其他所有應(yīng)用以及運(yùn)行時(shí)系統(tǒng)都會(huì)終止。
  • transient 應(yīng)用如果終止理由是 normal,會(huì)有終止報(bào)告,但不會(huì)終止其他應(yīng)用。如果 transient 應(yīng)用異常終止(理由不是 normal),那么其他所有應(yīng)用以及運(yùn)行時(shí)系統(tǒng)都會(huì)終止。
  • temporary 應(yīng)用終止,只會(huì)有終止報(bào)告,其他應(yīng)用不會(huì)終止。

通過調(diào)用 application:stop/1 可以顯式地終止一個(gè)應(yīng)用,不管啟動(dòng)類型是什么,其他應(yīng)用都不會(huì)被影響。

transient 模式基本沒什么用,因?yàn)楫?dāng)監(jiān)控樹退出,終止理由會(huì)是 shutdown 而不是 normal 。

?

8?Included Applications

8.1 引言

應(yīng)用可以 include(譯作包含) 其他應(yīng)用。被包含的應(yīng)用(included application)有自己的應(yīng)用目錄和 .app 文件,不過它是另一個(gè)應(yīng)用的監(jiān)控樹的一部分。

應(yīng)用不能被多個(gè)應(yīng)用包含。

被包含的應(yīng)用可以包含其他應(yīng)用。

沒有被任何應(yīng)用包含的應(yīng)用被稱為原初應(yīng)用(primary application)。

圖8.1 原初應(yīng)用和被包含的應(yīng)用

應(yīng)用控制器會(huì)在加載原初應(yīng)用時(shí),自動(dòng)加載被包含的應(yīng)用,但是不會(huì)啟動(dòng)它們。被包含的應(yīng)用頂層 supervisor 必須由包含它的應(yīng)用的 supervisor 啟動(dòng)。

也就是說運(yùn)行時(shí),被包含的應(yīng)用實(shí)際上是原初應(yīng)用的一部分,被包含應(yīng)用中的進(jìn)程會(huì)認(rèn)為自己歸屬于原初應(yīng)用。

?

8.2 指定被包含的應(yīng)用

要包含哪些應(yīng)用,是在 .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 啟動(dòng)時(shí)同步

被包含應(yīng)用的監(jiān)控樹,是包含它的應(yīng)用的監(jiān)控樹的一部分。如果需要在兩個(gè)應(yīng)用間做同步,可以通過 start phase 來實(shí)現(xiàn)。

Start phase 是由 .app 文件中的 start_phases 字段指定的,它是一個(gè) {Phase,PhaseArgs} 列表,其中 Phase 是一個(gè) atom,PhaseArgs 可以是任何類型。

包含其他應(yīng)用時(shí),mod 字段必須為 {application_starter,[Module,StartArgs]}。其中 Module 是應(yīng)用回調(diào)模塊,StartArgs 是傳遞給 Module:start/2 的參數(shù):

{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,[]}}]}.

啟動(dòng)包含了其他應(yīng)用的原初應(yīng)用,跟正常啟動(dòng)應(yīng)用是一樣的,也就是說:

  • 應(yīng)用控制器為應(yīng)用創(chuàng)建 application master 。
  • master 調(diào)用 Module:start(normal, StartArgs) 啟動(dòng)頂層 supervisor 。

然后,原初應(yīng)用和被包含應(yīng)用按照從上到下從左到右的順序,master 依次為它們 start phase 。對(duì)每個(gè)應(yīng)用,master 按照原初應(yīng)用中指定的 phase 順序依次調(diào)用 Module:start_phase(Phase, Type, PhaseArgs) ,其中當(dāng)前應(yīng)用的 start_phases 中未指定的 phase 會(huì)被忽略。

被包含應(yīng)用的 .app 文件需要如下內(nèi)容:

  • {mod, {Module,StartArgs}} 項(xiàng)必須有。這個(gè)選項(xiàng)指定了應(yīng)用的回調(diào)模塊。StartArgs 會(huì)被忽略,因?yàn)橹挥性鯌?yīng)用會(huì)調(diào)用 Module:start/2 。
  • 如果被包含的應(yīng)用本身包含了其他應(yīng)用,則需要使用 {mod, {application_starter, [Module,StartArgs]}}
  • {start_phases, [{Phase,PhaseArgs}]} 字段必須要有,并且這個(gè)列表必須是原初應(yīng)用指定的 Phase 的子集。

啟動(dòng)上文定義的 prim_app 時(shí),在 application:start(prim_app) 返回之前,應(yīng)用控制器會(huì)調(diào)用下面的回調(diào):

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 引言

在擁有多個(gè)節(jié)點(diǎn)的分布式系統(tǒng)中,有必要以分布式的方式來管理應(yīng)用。如果某應(yīng)用所在的節(jié)點(diǎn)崩潰,則在另一個(gè)節(jié)點(diǎn)重啟這個(gè)應(yīng)用。

這樣的應(yīng)用被稱為分布式應(yīng)用。注意,分布式指的是應(yīng)用的“管理”。如果從跨節(jié)點(diǎn)使用服務(wù)的角度來說,所有的應(yīng)用都能分布式。

分布式的應(yīng)用可以在節(jié)點(diǎn)間遷移,所以需要尋址機(jī)制來確保不管它在哪個(gè)節(jié)點(diǎn)都能被其他應(yīng)用訪問到。這個(gè)問題不在此討論,可通過 Kernel 應(yīng)用的 global 和? pg2 模塊的某些功能來實(shí)現(xiàn)。

?

9.2 配置分布式應(yīng)用

分布式的應(yīng)用受兩個(gè)東西控制,應(yīng)用控制器(application_controller)和分布式應(yīng)用控制進(jìn)程(dist_ac)。這兩個(gè)都是 Kernel 應(yīng)用的一部分。所以分布式應(yīng)用是通過配置 Kernel 應(yīng)用來指定的,可以使用下面的配置參數(shù)(詳見 kernel 文檔):

distributed = [{Application, [Timeout,] NodeDesc}]

  • 指定了應(yīng)用 Application = atom() 能在哪里運(yùn)行。
  • NodeDesc = [Node | {Node,...,Node}] 是一個(gè)節(jié)點(diǎn)名列表,按優(yōu)先級(jí)排列。元組 {} 中的節(jié)點(diǎn)沒有先后順序。
  • Timeout = integer() 指定了等待多少毫秒后在其他節(jié)點(diǎn)上重啟應(yīng)用。默認(rèn)為0。

為了正確地管理分布式應(yīng)用,可運(yùn)行應(yīng)用的節(jié)點(diǎn)必須互相連接,協(xié)商應(yīng)用在哪里啟動(dòng)。可在 Kernel 中使用下面的配置參數(shù):

  • sync_nodes_mandatory = [Node] - 指定了必須啟動(dòng)的其他節(jié)點(diǎn)(在 sync_nodes_timeout 指定的時(shí)間內(nèi))。
  • sync_nodes_optional = [Node] - 指定了可以啟動(dòng)的其他節(jié)點(diǎn)(在 sync_nodes_timeout 指定的時(shí)間內(nèi))
  • sync_nodes_timeout = integer() | infinity- 指定了等待其他節(jié)點(diǎn)啟動(dòng)的超時(shí)時(shí)長,單位毫秒。

節(jié)點(diǎn)啟動(dòng)時(shí)會(huì)等待所有 sync_nodes_mandatory? sync_nodes_optional 中的節(jié)點(diǎn)啟動(dòng)。如果所有節(jié)點(diǎn)都啟動(dòng)了,或必須啟動(dòng)的節(jié)點(diǎn)啟動(dòng)了,sync_nodes_timeout 時(shí)長后所有的應(yīng)用會(huì)被啟動(dòng)。如果有必須的節(jié)點(diǎn)沒啟動(dòng),當(dāng)前節(jié)點(diǎn)會(huì)終止。

例:

應(yīng)用 myapp 在 cp1@cave 中運(yùn)行。如果此節(jié)點(diǎn)終止,myapp 將在 cp2@cave 或 cp3@cave 節(jié)點(diǎn)上重啟。cp1@cave 的系統(tǒng)配置 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 的系統(tǒng)配置也是一樣的,除了必須啟動(dòng)的節(jié)點(diǎn)分別是 [cp1@cave, cp3@cave][cp1@cave, cp2@cave]

注意:所有節(jié)點(diǎn)的 distributed 和 sync_nodes_timeout 值必須一致,否則該系統(tǒng)行為不會(huì)被定義。

?

9.3 啟動(dòng)和終止分布式應(yīng)用

當(dāng)所有涉及(必須啟動(dòng))的節(jié)點(diǎn)被啟動(dòng),在所有這些節(jié)點(diǎn)中調(diào)用 application:start(Application) 就能啟動(dòng)這個(gè)分布式應(yīng)用。

可以用引導(dǎo)腳本(Releases)來自動(dòng)啟動(dòng)應(yīng)用。

應(yīng)用將在參數(shù) distributed 配置的節(jié)點(diǎn)列表中的第一個(gè)可用節(jié)點(diǎn)啟動(dòng)。和平常啟動(dòng)應(yīng)用一樣,創(chuàng)建了一個(gè) application master,調(diào)用回調(diào):

Module:start(normal, StartArgs)

例:

繼續(xù)上一小節(jié)的例子,啟動(dòng)了三個(gè)節(jié)點(diǎn),指定系統(tǒng)配置文件:

> erl -sname cp1 -config cp1 > erl -sname cp2 -config cp2 > erl -sname cp3 -config cp3

所有節(jié)點(diǎn)可用時(shí),myapp 會(huì)被啟動(dòng)。所有節(jié)點(diǎn)中調(diào)用 application:start(myapp) 即可。此時(shí)它會(huì)在 cp1 中啟動(dòng),如下圖所示:

圖9.1:應(yīng)用 myapp - 情況 1

同樣地,在所有的節(jié)點(diǎn)中調(diào)用 application:stop(Application) 將終止應(yīng)用。

?

9.4 故障切換

如果應(yīng)用所在的節(jié)點(diǎn)終止,指定的超時(shí)時(shí)長后,應(yīng)用將在 distributed 配置中指定的第一個(gè)可用節(jié)點(diǎn)中重啟。這就是故障切換

應(yīng)用在新節(jié)點(diǎn)中和平常一樣啟動(dòng),application master 調(diào)用:

Module:start(normal, StartArgs)

有一個(gè)例外,如果應(yīng)用指定了 start_phases(詳見Included Applications),應(yīng)用將這樣重啟:

Module:start({failover, Node}, StartArgs)

其中 Node 為終止的節(jié)點(diǎn)。

例:

如果 cp1 終止,系統(tǒng)會(huì)等待 cp1 重啟5秒,超時(shí)后在 cp2 和 cp3 中選擇一個(gè)運(yùn)行的應(yīng)用最少的。如果 cp1 沒有重啟,且 cp2 運(yùn)行的應(yīng)用比 cp3 少,myapp 將會(huì) cp2 節(jié)點(diǎn)重啟。

圖9.2:應(yīng)用 myapp - 情況 2

?假設(shè) cp2 也崩潰了,并且5秒內(nèi)沒有重啟。myapp 將在 cp3 重啟。

圖9.3:應(yīng)用 myapp - 情況 3

?

9.5? 接管

如果一個(gè)在 distributed 配置中優(yōu)先級(jí)較高的節(jié)點(diǎn)啟動(dòng),應(yīng)用會(huì)在新節(jié)點(diǎn)重啟,在舊節(jié)點(diǎn)結(jié)束。這就是接管

應(yīng)用會(huì)通過如下方式啟動(dòng):

Module:start({takeover, Node}, StartArgs)

其中 Node 表示舊節(jié)點(diǎn)。

例:

如果 myapp 在 cp3 節(jié)點(diǎn)運(yùn)行,此時(shí) cp2 啟動(dòng),應(yīng)用不會(huì)被重啟,因?yàn)?cp2 和 cp3 是沒有先后順序的。

圖9.4:應(yīng)用 myapp - 情況 4

但如果 cp1 也重啟了,函數(shù) application:takeover/2 會(huì)將 myapp 移動(dòng)到 cp1,因?yàn)閷?duì) myapp 來說 cp1 比 cp3 優(yōu)先級(jí)高。此時(shí)節(jié)點(diǎn) cp1 會(huì)調(diào)用 Module:start({takeover, cp3@cave}, StartArgs) 來啟動(dòng)應(yīng)用。

圖9.5:應(yīng)用 myapp - 情況 5

?

10?Releases

此章應(yīng)與 SASL 部分的 rel、systemtools、script 教程一起閱讀。

10.1 概念

當(dāng)你寫了一個(gè)或多個(gè)應(yīng)用,你可能想用這些應(yīng)用加 Erlang/OTP 應(yīng)用的子集創(chuàng)建一個(gè)完整的系統(tǒng)。這就是 release

首先要?jiǎng)?chuàng)建一個(gè) release 源文件,文件中指定了 release 所包含的應(yīng)用。

此文件用于生成啟動(dòng)腳本和 release 包。可移動(dòng)和安裝到另一個(gè)地址的系統(tǒng)被稱為目標(biāo)系統(tǒng)。系統(tǒng)原則(System Principles)中講了如何用 release 包創(chuàng)建目標(biāo)系統(tǒng)。

?

10.2 Release 源文件

創(chuàng)建 release 源文件來描述一個(gè) release,簡稱 .rel 文件。文件中指定了 release 的名字和版本號(hào),它基于哪個(gè)版本的 ERTS,以及它由哪些應(yīng)用組成:

{release, {Name,Vsn}, {erts, EVsn},[{Application1, AppVsn1},...{ApplicationN, AppVsnN}]}.

Name、Vsn、EVsn 和 AppVsn 都是字符串(string)。

文件名必須為 Rel.rel ,其中 Rel 是唯一的名字。

?Application (atom)AppVsn 是 release 中各應(yīng)用的名字和版本號(hào)。基于 Erlang/OTP 的最小的 release 由 Kernel 和 STDLIB 應(yīng)用組成,這兩個(gè)應(yīng)用一定要在應(yīng)用列表中。

要升級(jí) release 的話,還必須包含 SASL 應(yīng)用。

例: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,因?yàn)?ch_app 要用到這些應(yīng)用。文件名 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 生成啟動(dòng)腳本

SASL 應(yīng)用的 systools 模塊包含了構(gòu)建和檢查 release 的工具。這些函數(shù)讀取 .rel 和 .app 文件,執(zhí)行語法和依賴檢測。用 systools:make_script/1,2 來生成啟動(dòng)腳本(詳見 System Principles):

1> systools:make_script("ch_rel-1", [local]). ok

這個(gè)會(huì)創(chuàng)建啟動(dòng)腳本,可讀版本 ch_rel-1.script 和運(yùn)行時(shí)系統(tǒng)用到的二進(jìn)制版本 ch_rel-1.boot

  • ch_rel-1 是 .rel 文件的名字去掉擴(kuò)展名。
  • local 是個(gè)附加選項(xiàng),意思是在啟動(dòng)腳本中使用應(yīng)用所在的目錄,而不是 $ROOT/lib$ROOT 是安裝后的 release 的根目錄)。

這在本地測試生成啟動(dòng)腳本時(shí)有用處。

使用啟動(dòng)腳本來啟動(dòng)? Erlang/OTP 時(shí),會(huì)自動(dòng)加載和啟動(dòng) .rel 文件中所有的應(yīng)用:

% erl -boot ch_rel-1 Erlang (BEAM) emulator version 5.3Eshell V5.3 (abort with ^G) 1> =PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===supervisor: {local,sasl_safe_sup}started: [{pid,<0.33.0>},{name,alarm_handler},{mfa,{alarm_handler,start_link,[]}},{restart_type,permanent},{shutdown,2000},{child_type,worker}]...=PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===application: saslstarted_at: nonode@nohost... =PROGRESS REPORT==== 13-Jun-2003::12:01:15 ===application: ch_appstarted_at: nonode@nohost

?

10.4 創(chuàng)建 release 包

systools:make_tar/1,2? 函數(shù)以 .rel 文件作為輸入,輸出一個(gè) zip 壓縮的 tar 文件,文件中包含指定應(yīng)用的代碼,即 release 包

1> systools:make_script("ch_rel-1"). ok 2> systools:make_tar("ch_rel-1"). ok

一個(gè) release 包默認(rèn)包含:

  • .app 文件
  • .rel 文件
  • 所有應(yīng)用的目標(biāo)代碼,代碼根據(jù)應(yīng)用目錄結(jié)構(gòu)組織
  • 二進(jìn)制啟動(dòng)腳本,重命名為 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 包生成前,生成了一個(gè)新的啟動(dòng)腳本(不使用 local 選項(xiàng))。在 release 包中,所有的應(yīng)用目錄都放在 lib 目錄下。由于不知道 release 包會(huì)發(fā)布到哪里,所以不能寫死絕對(duì)路徑。

在 tar 文件中有兩個(gè)一樣的 rel 文件。最初這個(gè)文件只放在 releases 目錄下,這樣 release_handler 就能單獨(dú)提取這個(gè)文件。解壓 tar 文件后,release_handler 會(huì)自動(dòng)把它拷貝到 releases/FIRST 目錄。但是有時(shí) tar 文件解包時(shí)沒有 release_handler 參與(比如解壓第一個(gè)目標(biāo)系統(tǒng)),所以改為在 tar 文件中有兩份,不需要再手動(dòng)拷貝。

包里面還可能有 relup 文件和系統(tǒng)配置文件 sys.config,這些文件也會(huì)在 release 包中包含。詳見 Release Handling 。

?

10.5 目錄結(jié)構(gòu)

release_handler 從 release 包安裝的代碼目錄結(jié)構(gòu)如下:

$ROOT/lib/App1-AVsn1/ebin/priv/App2-AVsn2/ebin/priv.../AppN-AVsnN/ebin/priv/erts-EVsn/bin/releases/Vsn/bin
  • lib - 應(yīng)用目錄
  • erts-EVsn/bin - Erlang 運(yùn)行時(shí)系統(tǒng)可執(zhí)行文件
  • releases/Vsn - .rel 文件和啟動(dòng)文件 start.boot。relup 和 sys.config 也在此目錄下
  • bin - 最上層的 Erlang 運(yùn)行時(shí)系統(tǒng)可執(zhí)行文件

應(yīng)用不一定要放在 $ROOT/lib 目錄。因此可以有多個(gè)安裝目錄,包含系統(tǒng)的不同部分。例如,上面的例子可以拓展成:

$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 在調(diào)用 systools:make_script/2 函數(shù)時(shí)作為參數(shù)傳入。

無磁盤或只讀客戶端

如果系統(tǒng)由無磁盤的或只讀的客戶端節(jié)點(diǎn)組成,$ROOT 目錄中還會(huì)有一個(gè) clients 目錄。只讀的節(jié)點(diǎn)就是節(jié)點(diǎn)在一個(gè)只讀文件系統(tǒng)中。

每個(gè)客戶端節(jié)點(diǎn)在 clients 中有一個(gè)子目錄。每個(gè)子目錄的名字是對(duì)應(yīng)的節(jié)點(diǎn)名。一個(gè)客戶端目錄至少包含 binreleases 兩個(gè)子目錄。這些目錄用來存放 release 的信息,以及把當(dāng)前 release 指派給客戶端。$ROOT 目錄如下所示:

$ROOT/.../clients/ClientName1/bin/releases/Vsn/ClientName2/bin/releases/Vsn.../ClientNameN/bin/releases/Vsn

這個(gè)結(jié)構(gòu)用于所有客戶端都運(yùn)行在同類型的 Erlang 虛擬機(jī)上。如果有不同類型的 Erlang 虛擬機(jī),或者在不同的操作系統(tǒng)中,可以clients 分成每個(gè)類型一個(gè)子目錄。或者每個(gè)類型設(shè)置一個(gè) $ROOT。此時(shí) $ROOT 目錄相關(guān)的一些子目錄都需要包含進(jìn)來:

$ROOT/.../clients/Type1/lib/erts-EVsn/bin/ClientName1/bin/releases/Vsn/ClientName2/bin/releases/Vsn.../ClientNameN/bin/releases/Vsn.../TypeN/lib/erts-EVsn/bin...

這個(gè)結(jié)構(gòu)中,Type1 的客戶端的根目錄為 $ROOT/clients/Type1 。

?

11?Release Handling

11.1 Relase 管理原則

Erlang 的一個(gè)重要特點(diǎn)就是可以在運(yùn)行時(shí)改變模塊代碼,即 Erlang Reference Manual(參考手冊)中說的代碼替換

基于這個(gè)特點(diǎn),OTP 應(yīng)用 SASL 提供在運(yùn)行時(shí)升級(jí)和降級(jí)整個(gè) release 的框架。這就是 release 管理

這個(gè)框架包含:

  • 線下支持 - 用 systools 模塊生成腳本和創(chuàng)建 release 包
  • 線上支持 - 用 release_handler 打包和安裝 release 包

包含 release 管理的基于 Erlang/OTP 的最小的系統(tǒng),由 Kernel、STDLIB 和 SASL 應(yīng)用組成。

Release 管理工作流

步驟 1:按 Releases 章所述創(chuàng)建一個(gè) release。

步驟 2:在目標(biāo)環(huán)境中安裝 release 。如何安裝第一個(gè)目標(biāo)系統(tǒng),詳見 System Principles 文檔。

步驟 3:在開發(fā)環(huán)境中修改代碼(比如錯(cuò)誤修復(fù))。

步驟 4:某個(gè)時(shí)間點(diǎn),需要?jiǎng)?chuàng)建新版本 release 。更新相關(guān)的 .app 文件,創(chuàng)建 .rel 文件。

步驟 5:為每個(gè)修改的應(yīng)用,創(chuàng)建 .appup 文件(應(yīng)用升級(jí)文件)。該文件描述了怎么在應(yīng)用的新舊版本間升降級(jí)。

步驟 6:基于 .appup 文件,創(chuàng)建 relup 文件 (release 升級(jí)文件)。該文件描述了怎么在整個(gè) release 的新舊版本間升降級(jí)。

步驟 7:創(chuàng)建一個(gè)新的 release 包,放到目標(biāo)系統(tǒng)上。

步驟 8:使用 release handler 解包。

步驟 9:使用 release handler 安裝新版 release 包。執(zhí)行 relup 文件中的指令:添加、刪除或重新加載模塊,啟動(dòng)、終止或重啟應(yīng)用,等等。有時(shí)需要重啟整個(gè)模擬器。

  • 如果安裝失敗,系統(tǒng)會(huì)被重啟。默認(rèn)使用舊版本 release 。
  • 如果安裝成功,新版本會(huì)變?yōu)槟J(rèn)版本,系統(tǒng)重啟時(shí)會(huì)使用新版本。

Release 管理特性

Appup Cookbook 章中有 .appup 文件的示例,包含了典型的運(yùn)行時(shí)系統(tǒng)可以輕松處理的案例。然而有些情況下 release 管理會(huì)很復(fù)雜,例如:

  • 復(fù)雜或者環(huán)形的依賴關(guān)系會(huì)讓事情變得很復(fù)雜,很難決定以什么順序執(zhí)行才能不引起系統(tǒng)錯(cuò)誤。依賴可能存在于:
    • 節(jié)點(diǎn)之間
    • 進(jìn)程之間
    • 模塊之間
  • 在 release 管理過程中,不受影響的進(jìn)程會(huì)繼續(xù)正常執(zhí)行。這可能導(dǎo)致超時(shí)或其他問題。例如,掛起使用某模塊的進(jìn)程,到加載該模塊新版本的過程中,創(chuàng)建的新進(jìn)程可能執(zhí)行舊代碼。

所以建議代碼做盡可能小的改動(dòng),永遠(yuǎn)保持向后兼容。

?

11.2 必要條件

為了正確地執(zhí)行 release 管理,運(yùn)行時(shí)系統(tǒng)必須知道當(dāng)前運(yùn)行哪個(gè) release 。必須能在運(yùn)行時(shí),改變重啟時(shí)要用哪個(gè)啟動(dòng)腳本和系統(tǒng)配置文件,使其崩潰時(shí)還能生效。所以,Erlang 必須以嵌入式系統(tǒng)方式啟動(dòng),詳見 Embedded System 文檔。

為了系統(tǒng)重啟順利,系統(tǒng)啟動(dòng)時(shí)必須啟動(dòng)心跳監(jiān)測,詳見 ERTS 部分的 erl 手冊和 Kernel 部分的 heart(3) 手冊。

其他必要條件:

  • Release 包中的啟動(dòng)腳本必須和 release 包從同一個(gè) .rel 文件中生成。升降級(jí)時(shí),應(yīng)用信息從該腳本中獲取。
  • 系統(tǒng)只能有一個(gè)系統(tǒng)配置文件 sys.config 。如果文件存在,創(chuàng)建 release 包時(shí)會(huì)自動(dòng)包含進(jìn)來。
  • 所有版本的 release(除了第一個(gè)),必須包含 relup 文件。如果文件存在,創(chuàng)建 release 包時(shí)會(huì)自動(dòng)包含進(jìn)來。

?

11.3 分布式系統(tǒng)

如果系統(tǒng)由多個(gè)節(jié)點(diǎn)組成,每個(gè)節(jié)點(diǎn)可以擁有自己的 release 。release_handler 是一個(gè)本地注冊的進(jìn)程,升降級(jí)時(shí)只能在節(jié)點(diǎn)中調(diào)用。Release 管理指令 sync_nodes 可以用來同步多個(gè)節(jié)點(diǎn)的 release 管理進(jìn)程,詳見 SASL 的 appup(4) 手冊。

?

11.4 Release 管理指令

OTP 支持一系列 Release 管理指令,在創(chuàng)建 appup 文件時(shí)會(huì)用到。release_handler 能理解其中一部分,低級(jí)指令。還有一些高級(jí)指令,是為了用戶方便而設(shè)計(jì)的,調(diào)用 systools:make_relup 時(shí)會(huì)被轉(zhuǎn)化成低級(jí)指令。

此節(jié)描述了最常用的指令。完整的指令列表可見 SASL 的 appup(4) 手冊。

首先,給出一些定義:

  • Residence module(駐地模塊) - 模塊中有進(jìn)程的尾遞歸循環(huán)函數(shù)(進(jìn)程 loop 所在)。如果多個(gè)模塊有這些函數(shù),所有這些模塊都是這個(gè)進(jìn)程的 residence 模塊。
  • Functional module(功能模塊) - 不是任何進(jìn)程的 residence 模塊的模塊。

對(duì)一個(gè) OTP behaviour 實(shí)現(xiàn)的進(jìn)程來說,behaviour 模塊就是它的駐地模塊,回調(diào)模塊就是功能模塊。

load_module

如果模塊做了簡單的擴(kuò)展,加載模塊的新版本并移除舊版本就行了。這就是簡單的代碼替換,使用如下指令即可:

{load_module, Module}

update

如果有復(fù)雜的修改,比如改了 gen_server 的內(nèi)部狀態(tài)格式,簡單的代碼替換就不夠了。必須做到:

  • 掛起使用該模塊的進(jìn)程(避免它們在代碼替換完成前處理請求)。
  • 要求進(jìn)程修改內(nèi)部狀態(tài)格式,并切換到新版本代碼。
  • 移除舊代碼。
  • 恢復(fù)進(jìn)程。

這個(gè)就是同步代碼替換,使用如下指令:

{update, Module, {advanced, Extra}} {update, Module, supervisor}

當(dāng)要改變上述 behaviour 的內(nèi)部狀態(tài)時(shí),使用 {advanced,Extra} 。它會(huì)導(dǎo)致進(jìn)程調(diào)用回調(diào)函數(shù) code_change,傳遞 Extra 和一些其他信息作為參數(shù)。詳見對(duì)應(yīng) behaviour 和 Appup Cookbook 。

改變監(jiān)程的啟動(dòng)規(guī)格時(shí)使用 supervisor 參數(shù)。詳見 Appup Cookbook 。

當(dāng)模塊更新時(shí),release_handler 會(huì)遍歷各應(yīng)用的監(jiān)控樹,檢查所有的子進(jìn)程規(guī)格,找到用到該模塊的進(jìn)程:

{Id, StartFunc, Restart, Shutdown, Type, Modules}

進(jìn)程用到了某模塊,意思就是該模塊在子進(jìn)程規(guī)格的 Modules 列表中。

如果 Modules=dynamic,如事件管理器,則事件管理器會(huì)通知 release_handler 當(dāng)前安裝的事件處理器列表(gen_event),它會(huì)檢測這個(gè)列表的模塊名。

release_handler 通過 sys:suspend/1,2sys:change_code/4,5sys:resume/1,2 來掛起、要求切換代碼以及恢復(fù)進(jìn)程。

add_module 和 delete_module

使用下列指令引入新模塊:

{add_module, Module}

?這條指令加載了新模塊,在嵌入模式運(yùn)行 Erlang 時(shí)必須使用它。交互模式下可以不使用這條指令,因?yàn)榇a服務(wù)器會(huì)自動(dòng)搜尋和加載未加載的模塊。

delete_module 與 add_module 相反,它能卸載模塊:

{delete_module, Module}

當(dāng)這條指令執(zhí)行時(shí),以 Module 為駐地模塊的所有進(jìn)程都會(huì)被殺死。用戶必須保證在卸載模塊前,所有涉及進(jìn)程都終止,以避免無謂的 supervisor 重啟。

應(yīng)用指令

添加應(yīng)用:

{add_application, Application}

添加一個(gè)應(yīng)用,會(huì)先用 add_module 指令加載所有 .app 文件中 modules 字段所列模塊,然后啟動(dòng)應(yīng)用。

移除應(yīng)用:

{remove_application, Application}

移除應(yīng)用會(huì)終止應(yīng)用,并且使用 delete_module 指令卸載模塊,最后會(huì)從應(yīng)用控制器卸載應(yīng)用的規(guī)格信息。

重啟應(yīng)用:

{restart_application, Application}

重啟應(yīng)用會(huì)先終止應(yīng)用再啟動(dòng)應(yīng)用,相當(dāng)于連續(xù)使用 remove_applicationadd_application

apply (低級(jí)指令)

讓 release_handler 調(diào)用任意函數(shù):

{apply, {M, F, A}}

release_handler 會(huì)執(zhí)行 apply(M, F, A) 。

restart_new_emulator (低級(jí)指令)

這條指令用于改變模擬器版本,或者升級(jí)核心應(yīng)用 Kernel、STDLIB 或 SASL 。如果因?yàn)槟撤N原因需要系統(tǒng)重啟,則應(yīng)該使用 restart_emulator 指令。

這條指令要求系統(tǒng)啟動(dòng)時(shí)必須啟動(dòng)心跳監(jiān)測,詳見 ERTS 部分的 erl 手冊和 Kernel 部分的 heart(3) 手冊。

restart_new_emulator 必須是 relup 文件的第一條指令,如果使用 systools:make_relup/3,4 生成 relup 文件,會(huì)默認(rèn)放在最前面。

當(dāng) release_handler 執(zhí)行這條命令,它會(huì)先生成一個(gè)臨時(shí)的啟動(dòng)文件,文件指定新版本的模擬器和核心應(yīng)用以及舊版本的其他應(yīng)用。然后它調(diào)用 init:reboot()(詳見 Kernel 的 init(3) 手冊)關(guān)閉當(dāng)前模擬器。所有進(jìn)程優(yōu)雅地終止,然后 heart 程序使用臨時(shí)啟動(dòng)文件重啟系統(tǒng)。重啟后,會(huì)執(zhí)行其他的 relup 指令,這個(gè)過程定義在臨時(shí)啟動(dòng)文件中。

?警告:這個(gè)機(jī)制會(huì)在啟動(dòng)時(shí)使用新版本的模擬器和核心應(yīng)用,但是其他應(yīng)用仍是舊版本。所以要額外注意兼容問題。有時(shí)核心應(yīng)用中會(huì)做不兼容的修改。如果可能,新舊代碼先共存于一個(gè) release,線上更新完成后再在此后的新 release 棄用舊代碼。為了保證應(yīng)用不會(huì)因?yàn)椴患嫒莸男薷亩罎?#xff0c;應(yīng)盡可能早地停止調(diào)用棄用函數(shù)。?

升級(jí)完成會(huì)寫一條 info 報(bào)告。可以通過調(diào)用 release_handler:which_releases(current) ,檢查它是否返回預(yù)期的新的 release 。

當(dāng)新模擬器可操作時(shí),必須持久化新的 release 版本。否則系統(tǒng)重啟時(shí)仍會(huì)使用舊版。

在 UNIX 系統(tǒng)中,release_handler 會(huì)告訴 heart 程序使用哪條命令來重啟系統(tǒng)。此時(shí) heart 程序使用的環(huán)境變量 HEART_COMMAND 會(huì)被忽略,默認(rèn)命令為 $ROOT/bin/start 。也可以通過使用 SASL 的配置參數(shù) start_prg 來指定其他命令,詳見 sasl(6) 手冊。

restart_emulator (低級(jí)命令)

這條命令不用于 ERTS 或核心應(yīng)用的升級(jí)。在所有升級(jí)指令執(zhí)行完后,可以用它來強(qiáng)制重啟模擬器。

relup 文件只能有一個(gè) restart_emulator 指令,且必須放在最后。如果使用 systools:make_relup/3,4 生成 relup 文件,會(huì)默認(rèn)放在最后

當(dāng) release_handler 執(zhí)行這條命令,它會(huì)調(diào)用 init:reboot()(詳見 Kernel 的 init(3) 手冊)關(guān)閉當(dāng)前模擬器。所有進(jìn)程優(yōu)雅地終止,然后 heart 程序使用新版 release 來重啟系統(tǒng)。重啟后不會(huì)執(zhí)行其他升級(jí)指令。

?

11.5 應(yīng)用升級(jí)文件

創(chuàng)建應(yīng)用升級(jí)文件來指定如何在當(dāng)前版本和舊版本應(yīng)用之間升降級(jí),簡稱 .appup 文件。文件名為 Application.appup ,其中 Application 是應(yīng)用名:

{Vsn,[{UpFromVsn1, InstructionsU1},...,{UpFromVsnK, InstructionsUK}],[{DownToVsn1, InstructionsD1},...,{DownToVsnK, InstructionsDK}]}.
  • Vsn - 字符串,當(dāng)前應(yīng)用版本號(hào)( .app 文件中的版本號(hào))。
  • UpFromVsn - 升級(jí)前的版本號(hào)。
  • DownToVsn - 要降級(jí)至的版本號(hào)。
  • Instructions - release 管理指令列表。

.appup 文件的語法和內(nèi)容,詳見 SASL 的 appup(4) 手冊。

Appup Cookbook 中有典型案例的 .appup 文件示例。

例:Releases 章中的例子。如果想在 ch3 中添加函數(shù) available/0 ,返回可用 channel 的數(shù)量(修改的時(shí)候,在原目錄的副本里改,這樣第一版仍然可用):

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

創(chuàng)建新版 ch_app.app 文件,修改版本號(hào):

{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",只需要加載對(duì)應(yīng)版本的 ch3 回調(diào)即可。在 ebin 目錄創(chuàng)建 ch_app.appup 應(yīng)用升級(jí)文件:

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

?

11.6 Release 升級(jí)文件

要指定如何在 release 的版本間切換,要?jiǎng)?chuàng)建一個(gè) release 升級(jí)文件,簡稱 relup 文件。

可以使用 systools:make_relup/3,4 自動(dòng)生成此文件,將相關(guān)版本的 .rel 文件、.app 文件和 .appup 文件作為輸入。它不包含要增刪哪些應(yīng)用,哪些應(yīng)用要升降級(jí)。這些指令會(huì)從 .appup 文件中獲取,按正確的順序轉(zhuǎn)化成低級(jí)指令列表。

如果 relup 文件很簡單,可以手動(dòng)創(chuàng)建它。它只包含低級(jí)指令。

relup 文件的語法和內(nèi)容詳見 SASL 的 relup(4) 手冊。

繼續(xù)前小節(jié)的例子:已經(jīng)有新版 "2" 的 ch_app 應(yīng)用以及 .appup 文件。還需新版的 .rel 文件。文件名 ch_rel-2.rel ,release 版本從 "A" 變?yōu)?"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

生成了一個(gè) relup 文件,文件中有從版本 "A" ("ch_rel-1") 升級(jí)到版本 "B" ("ch_rel-2") 和從 "B" 降到 "A" 的指令。

新版和舊版的 .app 和 .rel 文件、.appup 文件和新的 .beam 文件都必須在代碼路徑中。代碼路徑可以使用選項(xiàng) path 來擴(kuò)展:

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

有了一個(gè)新版的 release,就可以創(chuàng)建 release 包并放到目標(biāo)環(huán)境中去。

在運(yùn)行時(shí)系統(tǒng)中安裝新版 release 會(huì)用到 release handler 。它是 SASL 應(yīng)用的一個(gè)進(jìn)程,用于處理 release 包的解包、安裝和移除。它通過 release_handler 模塊通訊。詳見 SASL 的 release_handler(3) 手冊。

假設(shè)有一個(gè)可操作的目標(biāo)系統(tǒng),安裝根目錄為 $ROOT,新版 release 包應(yīng)拷貝到 $ROOT/releases 目錄下。首先,解包。從包中提取文件:

release_handler:unpack_release(ReleaseName) => {ok, Vsn}

?

?

  • ReleaseName - release 包的名字,不包含 .tar.gz 后綴。
  • Vsn - release 的版本號(hào),包的 .rel 文件中定義的。
release_handler 創(chuàng)建了 $ROOT/lib/releases/Vsn 目錄,目錄中含 .rel 文件、啟動(dòng)腳本 start.boot、系統(tǒng)配置文件 sys.config 以及 relup 文件。有新版本號(hào)的應(yīng)用會(huì)被放在 $ROOT/lib 目錄下,其他應(yīng)用不受影響。解包后的 release 可以安裝了。release_handler 會(huì)執(zhí)行 relup 文件中的指令: release_handler:install_release(Vsn) => {ok, FromVsn, []}

如果安裝過程中有錯(cuò)誤發(fā)生,系統(tǒng)會(huì)使用舊版 release 重啟。如果安裝成功,后續(xù)系統(tǒng)會(huì)用新版本,不過如果系統(tǒng)中途有重啟的話,還是會(huì)使用舊版本。

必須把新安裝的 release 持久化才能讓它成為默認(rèn)版本,讓之前的版本變成版本:

release_handler:make_permanent(Vsn) => ok

系統(tǒng)在 $ROOT/releases/RELEASES$ROOT/releases/start_erl.data 中保存版本信息。

從 Vsn 降級(jí)到 FromVsn 時(shí),須再次調(diào)用 install_release

release_handler:install_release(FromVsn) => {ok, Vsn, []}

安裝了的但是還沒持久化的 release 可以被移除。移除意味著 release 的信息會(huì)被從 $ROOT/releases/RELEASES 中移除。代碼也會(huì)被移除,也就是說,新的應(yīng)用目錄和 $ROOT/releases/Vsn 目錄都會(huì)被刪掉。

release_handler:remove_release(Vsn) => ok

繼續(xù)前小節(jié)的例子

步驟 1)創(chuàng)建 Releases 中的版本 "A" 的目標(biāo)系統(tǒng)。這回 sys.config 必須包含在 release 包中。如果不需要任何配置,這個(gè)文件中為一個(gè)空列表:

[].

步驟 2)啟動(dòng)系統(tǒng)。現(xiàn)實(shí)中會(huì)以嵌入式系統(tǒng)啟動(dòng)。不過,使用 erl 和正確的啟動(dòng)腳本和配置就足以用來舉例說明:

% cd $ROOT % bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys ...

步驟 3)在另一個(gè) Erlang shell,生成啟動(dòng)腳本,并創(chuàng)建版本 "B" 的 release 包。記得包含 sys.config(可能有變化)和 relup 文件,詳見 Release 升級(jí)文件。

1> systools:make_script("ch_rel-2"). ok 2> systools:make_tar("ch_rel-2"). ok

新的 release 包現(xiàn)在包含版本 "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)在運(yùn)行的目標(biāo)系統(tǒng)中,解包:

1> release_handler:unpack_release("ch_rel-2"). {ok,"B"}

新版本應(yīng)用 ch_app-2 被安裝在 $ROOT/lib 目錄,在 ch_app-1 附近。kernelstdlibsasl 目錄不受影響,因?yàn)樗鼈儧]有改變。

$ROOT/releases 下創(chuàng)建了一個(gè)新目錄 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 中的指令會(huì)一一被執(zhí)行,新版 ch3 被加載進(jìn)來。函數(shù) ch3:available/0 現(xiàn)在可用了:

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 中的進(jìn)程代碼不變,例如,supervisor 還在執(zhí)行 ch_app-1 的代碼。

步驟 8)如果目標(biāo)系統(tǒng)現(xiàn)在重啟,它會(huì)重新使用 "A" 版本。要在重啟時(shí)使用 "B" 版本,必須持久化:

7> release_handler:make_permanent("B"). ok

?

11.8 更新應(yīng)用規(guī)格

當(dāng)新版 release 安裝,所有加載的應(yīng)用規(guī)格會(huì)自動(dòng)更新。

注意:新的應(yīng)用規(guī)格從 release 包中的啟動(dòng)腳本中獲取。所以啟動(dòng)腳本必須和 release 包從同一個(gè) .rel 文件中生成。

確切地說,應(yīng)用配置參數(shù)會(huì)根據(jù)下面的內(nèi)容自動(dòng)更新(優(yōu)先級(jí)遞增):

  • 啟動(dòng)腳本從新的應(yīng)用資源文件 App.app 中獲取到的信息。
  • 新的 sys.config
  • 命令行參數(shù) -App Par Val

也就是說被其他系統(tǒng)配置文件設(shè)置的值,以及使用 application:set_env/3 設(shè)置的值都會(huì)被無視。

當(dāng)安裝好的 release 被設(shè)置為永久時(shí),系統(tǒng)進(jìn)程 init 會(huì)指向新的 sys.config 文件。

安裝后,應(yīng)用控制器會(huì)比較所有運(yùn)行中的應(yīng)用的新舊配置參數(shù),并調(diào)用回調(diào):

Module:config_change(Changed, New, Removed)
  • Module - .app 文件中定義的 mod 字段,應(yīng)用回調(diào)模塊。
  • ChangedNew 是一個(gè) {Par,Val} 列表,包含了所有改變和增加的配置參數(shù)。
  • Removed 是被刪除的所有配置參數(shù) Par 的列表。

這個(gè)函數(shù)是可選的,在實(shí)現(xiàn)應(yīng)用回調(diào)模塊時(shí)可以省略。

?

12?Appup Cookbook

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

12.1 修改功能模塊

如果功能模塊被修改,例如新加了一個(gè)函數(shù)或修復(fù)了一個(gè) bug,簡單的代碼替換就夠了:

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

?

12.2 修改駐地模塊

如果系統(tǒng)完全根據(jù) OTP 設(shè)計(jì)原則來實(shí)現(xiàn),除系統(tǒng)進(jìn)程和特殊進(jìn)程外的所有進(jìn)程,都會(huì)駐扎在某個(gè) behavior 中,supervisorgen_servergen_fsm、gen_statem 或 gen_event 。這些都屬于 STDLIB 應(yīng)用,升降級(jí)一般來說需要模擬器重啟。

因此 OTP 沒有支持修改駐地模塊,除了一些特殊進(jìn)程。

?

12.3 修改回調(diào)模塊

回調(diào)模塊屬于功能模塊,代碼擴(kuò)展只需要簡單的代碼替換就行。

例:前文 Relase Handling 中的例子,在 ch3 中添加一個(gè)函數(shù),ch_app.appup 內(nèi)容如下:

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

OPT 還支持修改 behaviour 進(jìn)程的內(nèi)部狀態(tài),詳見下一小節(jié)。

?

12.4 修改內(nèi)部狀態(tài)

這種情況下,簡單的代碼替換不能解決問題。在切換到新版回調(diào)模塊前,進(jìn)程必須使用 code_change 回調(diào)顯示地修改它的狀態(tài)。此時(shí)要用到同步代碼替換(譯者補(bǔ)充:同步即需要等待進(jìn)程作出一些反應(yīng))。

例:前文 gen_server Behaviour 中的 gen_server ch3,內(nèi)部狀態(tài)為 Chs,表示可用的 channel 。假設(shè)你想增加一個(gè)計(jì)數(shù)器 N,記錄 alloc 請求次數(shù)。狀態(tài)的格式必須變?yōu)?{Chs,N}

.appup 文件內(nèi)容如下:

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

update 指令的第三個(gè)參數(shù)是一個(gè)元組 {advanced,Extra} ,意思是在加載新版模塊之前,受影響的進(jìn)程要先修改狀態(tài)。修改狀態(tài)是通過讓進(jìn)程調(diào)用 code_change 回調(diào)來完成的(詳見 STDLIB 的 gen_server(3) 手冊)。Extra(此例中是 [])會(huì)被傳遞到 code_change 函數(shù):

-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 第一個(gè)參數(shù),如果降級(jí)則為 {down,Vsn},升級(jí)則為 VsnVsn 是模塊的“原”版,即升級(jí)前的版本。

如果模塊有 vsn 屬性的話,版本即該屬性的值。ch3 沒有這個(gè)屬性,所以此時(shí)版本號(hào)為 beam 文件的校驗(yàn)和(大整數(shù)),此處它不重要被忽略。

ch3 的其他回調(diào)也要修改,還要加其他接口函數(shù),不過此處不贅述。

?

12.5 模塊依賴

在模塊中增加了一個(gè)接口函數(shù),比如前文 Release Handling 中的,在 ch3 中增加 available/0 。

假設(shè)在另一個(gè)模塊 m1 會(huì)調(diào)用此函數(shù)。在 release 升級(jí)過程中,如果先加載新版 m1,在 ch3 加載前 m1 調(diào)用 ch3:available/0 會(huì)引發(fā)一個(gè) runtime error 。

所以升級(jí)時(shí) ch3 必須在 m1 之前加載,降級(jí)時(shí)則相反。即 m1 依賴于 ch3 。在 release 處理指令中,用 DepMods 元素來表示:

{load_module, Module, DepMods} {update, Module, {advanced, Extra}, DepMods}

DepMods 是模塊列表,表示 Module 所依賴的模塊。

?例:myapp 應(yīng)用的模塊 m1 依賴于 ch_app 應(yīng)用的模塊 ch3,從 "1" 升級(jí)到 "2",或從 "2" 降級(jí)到 "1" 時(shí):

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 屬于同一個(gè)應(yīng)用,.appup 文件如下:

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

降級(jí)時(shí)也是 m1 依賴于 ch3 。systools 能區(qū)分升降級(jí),生成正確的 relup 文件,升級(jí)時(shí)先加載 ch3 再 m1,降級(jí)時(shí)先加載 m1 再 ch3 。

?

12.6 修改特殊進(jìn)程的代碼

這種情況下,簡單的代碼替換不能解決問題。加載特殊進(jìn)程的新版駐地模塊時(shí),進(jìn)程必須調(diào)用它的 loop 函數(shù)的全名,來切換至新代碼。此時(shí),必須用同步代碼替換。

注意:用戶自定義的駐地模塊,必須在特殊進(jìn)程的子進(jìn)程規(guī)格的 Modules 列表中。否則 release_handler 會(huì)找不到該進(jìn)程。

例:前文 sys and proc_lib 中的例子。通過 supervisor 啟動(dòng)時(shí),子進(jìn)程規(guī)格如下:

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

如果 ch4 是應(yīng)用 sp_app 的一部分,從版本 "1" 升級(jí)到版本 "2" 時(shí),要加載該模塊的新版本,sp_app.appup 內(nèi)容如下:

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

update 指令必須包含元組 {advanced,Extra} 。這條指令讓特殊進(jìn)程調(diào)用回調(diào) system_code_change/4,這個(gè)回調(diào)必須要實(shí)現(xiàn)。Extra(此例中為 [] ),會(huì)被傳遞給 system_code_change/4 :

-module(ch4). ... -export([system_code_change/4]). ...system_code_change(Chs, _Module, _OldVsn, _Extra) ->{ok, Chs}.
  • 第一個(gè)參數(shù)是內(nèi)部狀態(tài) State,從函數(shù) sys:handle_system_msg(Request, From, Parent, Module, Deb, State) 中傳入,sys:handle_system_msg 是特殊進(jìn)程在收到系統(tǒng)消息時(shí)調(diào)用的。ch4 的內(nèi)容狀態(tài)是可用的 Chs 集。
  • 第二個(gè)參數(shù)是模塊名( ch4 )。
  • 第三個(gè)參數(shù)是 Vsn{down,Vsn},跟 12.4 小節(jié)中 gen_server:code_change/3 的參數(shù)一樣。

此例中,只用到了第一個(gè)參數(shù),函數(shù)僅返回內(nèi)部狀態(tài)。如果代碼只是被擴(kuò)展,這樣就 ok 了。如果內(nèi)部狀態(tài)改變(類似 12.4 小節(jié)),要在這個(gè)函數(shù)中進(jìn)行改變,并返回 {ok,Chs2}

?

12.7 修改 supervisor

supervisor behaviour 支持修改內(nèi)部狀態(tài),也就是修改重啟策略、最大重啟頻率以及子進(jìn)程規(guī)格。

可以添加或刪除子進(jìn)程,不過不是自動(dòng)處理的。必須在 .appup 中指定。

修改屬性

由于 supervisor 內(nèi)部狀態(tài)有改動(dòng),必須使用同步代碼替換。需要一個(gè)特殊的 update 指令。

首先,加載新版回調(diào)模塊(升或降)。然后檢測 init/1 的新的返回值,并據(jù)此修改內(nèi)部狀態(tài)。

supervisor 的升級(jí)指令如下:

{update, Module, supervisor}

例:把 ch_sup 的重啟策略,從 one_for_one 變?yōu)?one_for_all,要改 ch_sup.erl 中的回調(diào)函數(shù) 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}]}] }.

修改子進(jìn)程規(guī)格

修改已存在的子進(jìn)程規(guī)格,指令和 .appup 文件與前面的修改屬性一樣:

{"2",[{"1", [{update, ch_sup, supervisor}]}],[{"1", [{update, ch_sup, supervisor}]}] }.

這些修改不會(huì)影響已存在的子進(jìn)程。例如,修改啟動(dòng)函數(shù),只會(huì)影響子進(jìn)程重啟。

子進(jìn)程規(guī)格的 id 不能修改。

修改子進(jìn)程規(guī)格的 Modules 字段,會(huì)影響 release_handler 進(jìn)程自身,因?yàn)檫@個(gè)字段用于在同步代碼替換中,確認(rèn)哪些進(jìn)程收到影響。

增加和刪除子進(jìn)程

如前文所說,修改子進(jìn)程規(guī)格,不影響現(xiàn)有子進(jìn)程。新的規(guī)格會(huì)自動(dòng)添加,但是不會(huì)刪除廢棄規(guī)格。子進(jìn)程不會(huì)自動(dòng)重啟或終止,必須使用 apply 指令來操作。

例:假設(shè)從 "1" 升到 "2" 時(shí), ch_sup 增加了一個(gè)子進(jìn)程 m1。降級(jí)時(shí) m1 會(huì)被刪除:

{"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 才能讓腳本生效。如果沒有注冊,不能從腳本中直接訪問它。此時(shí)必須寫一個(gè)幫助函數(shù)來尋找 supervisor 的 pid,并調(diào)用 supervisor:restart_child 。然后在腳本中使用 apply 指令調(diào)用該幫助函數(shù)。

如果模塊 m1 在應(yīng)用 ch_app 的版本 "2" 引入,它必須在升級(jí)時(shí)加載、降級(jí)時(shí)刪除:

{"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}]}] }.

如前文所述,指令的順序很重要。升級(jí)時(shí),必須在啟動(dòng)新進(jìn)程之前,加載 m1、改變 supervisor 的子進(jìn)程規(guī)格。降級(jí)時(shí),子進(jìn)程必須在規(guī)格改變和模塊被刪除前終止。

?

12.8 增加或刪除模塊

例:應(yīng)用 ch_app 增加了一個(gè)新的功能模塊:

{"2",[{"1", [{add_module, m}]}],[{"1", [{delete_module, m}]}]

?

12.9 啟動(dòng)或終止進(jìn)程

一個(gè)根據(jù) OTP 設(shè)計(jì)原則組織的系統(tǒng)中,所有的進(jìn)程都是某 supervisor 的子進(jìn)程,詳見增加和刪除子進(jìn)程。

?

12.10 增加或移除應(yīng)用

增加或移除應(yīng)用時(shí),不需要 .appup 文件。生成 relup 文件時(shí),會(huì)比較 .rel 文件,并自動(dòng)添加 add_application remove_application 指令。

?

12.11 重啟應(yīng)用

當(dāng)修改太復(fù)雜時(shí)(如監(jiān)控樹層級(jí)重構(gòu)),可以重啟應(yīng)用。

例:增加和刪除子進(jìn)程中的例子,ch_sup 增加了一個(gè)子進(jìn)程 m1,還可以通過重啟整個(gè)應(yīng)用來更新 supervisor :

{"2",[{"1", [{restart_application, ch_app}]}],[{"1", [{restart_application, ch_app}]}] }.

?

12.12 修改應(yīng)用規(guī)格

在執(zhí)行 relup 腳本前,在安裝 release 時(shí),應(yīng)用規(guī)格就自動(dòng)更新了。因此,不需要在 .appup 中增加指令:

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

?

12.13 修改應(yīng)用配置

可以通過修改 .app 文件中的 env 字段,來修改應(yīng)用配置。

另外,還可以修改 sys.config 文件來修改應(yīng)用配置參數(shù)。

?

12.14 修改被包含的應(yīng)用

增加、移除、重啟應(yīng)用的 release 處理指令,只適用于原初應(yīng)用。被包含應(yīng)用沒有相應(yīng)的指令。但是因?yàn)閷?shí)際上,被包含應(yīng)用的最上層 supervisor 是包含它的應(yīng)用的 supervisor 的子進(jìn)程,我們可以手動(dòng)創(chuàng)建 relup 文件。

例:假設(shè)一個(gè) release 包含了應(yīng)用 prim_app,它的監(jiān)控樹中有一個(gè)監(jiān)程 prim_sup

在新版本 release 中,應(yīng)用 ch_app 被包含進(jìn)了 prim_app ,也就是說它的最上層監(jiān)程 ch_sup 是 prim_sup 的子進(jìn)程。

工作流如下:

步驟 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)創(chuàng)建新的 .rel 文件,包含 ch_app:

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

被包含的應(yīng)用可以通過兩種方式重啟。下面會(huì)說。

應(yīng)用重啟

步驟 4a)一種方式,是重啟整個(gè) prim_app 應(yīng)用。在 prim_app 的 .appup 文件中使用 restart_application 指令。

然而,如果這樣做,relup 文件不止包含了重啟(移除和添加)prim_app 的指令,它還會(huì)有啟動(dòng)(以及降級(jí)時(shí)移除)ch_app 的指令。因?yàn)樾碌?.rel 文件中有 ch_app,而舊的 .rel 文件沒有。

所以,應(yīng)該手動(dòng)創(chuàng)建正確的 relup 文件,重寫或在自動(dòng)生成的基礎(chǔ)上寫都行。用加載/卸載 ch_app 的指令,替換啟動(dòng)/停止的指令:

{"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]}}]}] }.

修改監(jiān)程

步驟 4b)另一種方式,是結(jié)合為 prim_sup 添加或刪除子進(jìn)程的指令,以及加載和卸載 ch_app 代碼和應(yīng)用規(guī)格的指令。

這種方式也需要手動(dòng)創(chuàng)建 relup 文件。重寫或在自動(dòng)生成的基礎(chǔ)上寫都行。先加載 ch_app 的代碼和應(yīng)用規(guī)格,然后再更新 prim_sup 。降級(jí)時(shí)先更新 prim_sup 再卸載 ch_app 的代碼和應(yīng)用規(guī)格。

{"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 代碼

修改其他語言寫的代碼,比如接口程序,是依賴于應(yīng)用的,OTP 沒有提供特別的支持。

例:修改 port 程序,假設(shè)控制這個(gè)接口的 Erlang 進(jìn)程是注冊為 portc 的 gen_server,通過回調(diào) init/1 來開啟接口:

init(...) ->...,PortPrg = filename:join(code:priv_dir(App), "portc"),Port = open_port({spawn,PortPrg}, [...]),...,{ok, #state{port=Port, ...}}.

要更新接口程序,gen_server 的代碼必須有 code_change 回調(diào),用來關(guān)閉接口和開啟新接口(如果有需要,還可以讓 gen_server 先從舊接口請求必要的數(shù)據(jù),然后傳遞給新接口):

code_change(_OldVsn, State, port) ->State#state.port ! close,receive{Port,close} ->trueend,PortPrg = filename:join(code:priv_dir(App), "portc"),Port = open_port({spawn,PortPrg}, [...]),{ok, #state{port=Port, ...}}.

更新 .app 文件的版本號(hào),并創(chuàng)建 .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 模擬器重啟和升級(jí)

兩條重啟模擬器的升級(jí)指令:

  • restart_new_emulator

當(dāng) ERTS、Kernel、STDLIB 或 SASL 要升級(jí)時(shí)會(huì)用到。用 systools:make_relup/3,4 生成 relup 文件會(huì)自動(dòng)添加這條指令。它會(huì)在所有其他指令之前執(zhí)行。詳見前文的 restart_new_emulator(低級(jí)指令)。

  • restart_emulator

在所有其他指令執(zhí)行完后需要重啟模擬器時(shí)會(huì)用到。詳見前文的 restart_emulator 。

如果只需要重啟模擬器,不需要其他升級(jí)指令,可以手動(dòng)創(chuàng)建一個(gè) relup 文件:

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

此時(shí),release 管理框架會(huì)自動(dòng)打包、解包、更新路徑等,且不需要指定 .appup 文件。

?

12.17 OTP R15 之前的模擬器升級(jí)

從 OTP R15 開始,模擬器升級(jí)會(huì)在加載代碼和運(yùn)行其他應(yīng)用升級(jí)指令前,使用新版的核心應(yīng)用(Kernel、STDLIB 和 SASL)重啟模擬器來完成模擬器升級(jí)。這要求要升級(jí)的 release 必須是 OTP R15 或更晚版本。

如果 release 是早期版本,systools:make_relup 會(huì)生成一個(gè)向后兼容的 relup 文件。所有升級(jí)指令在模擬器重啟前執(zhí)行,新的應(yīng)用代碼會(huì)被加載到舊模擬器中。如果新代碼是用新模擬器編譯的,而新模擬器下的 beam 文件的格式有變化,可能導(dǎo)致加載 beam 文件失敗。用舊模擬器編譯新代碼,可以解決這個(gè)問題。

?

轉(zhuǎn)載于:https://www.cnblogs.com/-wyp/p/6892632.html

總結(jié)

以上是生活随笔為你收集整理的Erlang/OTP设计原则(文档翻译)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 饥渴少妇勾引水电工av | 曰女同女同中文字幕 | 国产色婷婷一区二区三区竹菊影视 | 韩国不卡av | 国产精品www | 农村村妇真实偷人视频 | 一区二区视频免费看 | 内射一区二区 | 欧美一区| 色.com| 亚洲精品视频一区 | 精品96久久久久久中文字幕无 | 成 人片 黄 色 大 片 | 狠狠97| 永久免费av无码网站性色av | 欧美三级黄色大片 | 日韩av自拍 | 人妻无码一区二区三区久久 | 在线观看你懂的网址 | 免费看污黄网站在线观看 | 欧美18一19性内谢 | 一卡二卡三卡四卡五卡 | 黄色理论视频 | 中文字幕天堂在线 | 亚洲爱情岛论坛永久 | 日韩午夜在线 | 最好看的2019年中文视频 | 国产乱视频| 色呦呦免费观看 | 香蕉久久夜色精品国产使用方法 | 天堂资源最新在线 | 熟妇人妻精品一区二区三区视频 | 日日综合网 | 色噜噜狠狠狠综合曰曰曰88av | 中国黄色1级片 | 久久婷婷综合色 | 孕期1ⅴ1高h | 91网站免费在线观看 | 2019最新中文字幕 | 美女毛毛片| 大地资源二中文在线影视观看 | 日韩国产三级 | 日本韩国欧美一区 | 久草福利资源站 | 在线观看av网站 | 91精品成人 | 四虎最新域名 | 97成人资源站 | 在线成人免费 | 国产一区二区视频在线观看免费 | 色网导航站 | 狠狠操2019 | 亚洲av永久无码精品一百度影院 | 13日本xxxxxⅹxxx20| 国产极品一区二区 | 法国空姐在线观看完整版 | 亚洲综合视频在线播放 | 顶弄h校园1v1| 国产美女自慰在线观看 | 男女激情大尺度做爰视频 | 色撸撸av | 天堂网www. | 丁香激情五月 | 日本一级大毛片a一 | 深夜福利麻豆 | 亚洲国产欧美在线观看 | 天天噜 | 国产免费一区视频观看免费 | 午夜视频免费观看 | 啪啪网站免费 | 久久网一区 | 黄色大片一级 | 久久亚洲熟女cc98cm | 美女啪啪一区二区 | 在线视频亚洲欧美 | 99国产精品国产精品九九 | 成人在线观看免费高清 | 亚洲一二三av | 午夜性生活视频 | 国产精品6666 | 成人黄色在线视频 | www.蜜臀av.com | jizz内谢中国亚洲jizz | 欧美xxxx黑人又粗又长密月 | 熟女人妇 成熟妇女系列视频 | 涩涩片影院 | 亚洲欧美亚洲 | 日韩女优在线 | 一级大片儿| 午夜黄网| 亚欧精品在线观看 | 五月婷婷六月激情 | 亚洲精品无码久久久久 | 成人欧美一区二区三区黑人孕妇 | 亚洲色欲色欲www在线观看 | 日韩黄片一区二区三区 | 欧美老熟妇一区二区 | 精品少妇人妻一区二区黑料社区 | 99riAv国产精品无码鲁大师 |