http://erlang.org/doc/design_principles/des_princ.htmlhtml
图和代码皆源自以上连接中Erlang官方文档,翻译时的版本为20.1。java
这个设计原则,实际上是说用户在设计系统的时候应遵循的标准和规范。阅读前我一直觉得写的是做者在设计 Erlang/OTP 框架时的一些原则。node
闲话少叙。Let's go!git
OTP设计原则规定了如何使用进程、模块和目录来组织 Erlang 代码。shell
Erlang/OTP的一个基本概念就是监控树。它是基于 workers(工人)和 supervisors(监工、监程)的进程组织模型。数据库
下图中方块表示 supervisor,圆圈表示 worker(图源Erlang官方文档):编程
图1.1: 监控树安全
在监控树中,不少进程拥有同样的结构,遵循同样的行为模式。例如,supervisors 结构上都是同样的,惟一的不一样就是他们监控的子进程不一样。而不少 wokers 都是以 server/client、finite-state machines(有限状态自动机)或是 error logger(错误记录器)之类的事件处理器的行为模式运行。服务器
Behaviour就是把这些通用行为模式形式化。也就是说,把进程的代码分红通用的部分(behaviour 模块)和专有的部分(callback module 回调模块).app
Behaviour 是 Erlang/OTP 框架中的一部分。用户若是要实现一个进程(例如一个 supervisor),只须要实现回调模块,而后导出预先定义的函数集(回调函数)就好了。
下面的例子代表了怎么把代码分红通用部分和专有部分。咱们把下面的代码看成是一个简单的服务器(用普通Erlang编写),用来记录 channel 集合。其余进程能够各自经过调用函数 alloc/0 和 fee/1 来分配和释放 channel。
-module(ch1). -export([start/0]). -export([alloc/0, free/1]). -export([init/0]). start() -> spawn(ch1, init, []). alloc() -> ch1 ! {self(), alloc}, receive {ch1, Res} -> Res end. free(Ch) -> ch1 ! {free, Ch}, ok. init() -> register(ch1, self()), Chs = channels(), loop(Chs). loop(Chs) -> receive {From, alloc} -> {Ch, Chs2} = alloc(Chs), From ! {ch1, Ch}, loop(Chs2); {free, Ch} -> Chs2 = free(Ch, Chs), loop(Chs2) end.
这个服务器能够重写成一个通用部分 server.erl :
-module(server). -export([start/1]). -export([call/2, cast/2]). -export([init/1]). start(Mod) -> spawn(server, init, [Mod]). call(Name, Req) -> Name ! {call, self(), Req}, receive {Name, Res} -> Res end. cast(Name, Req) -> Name ! {cast, Req}, ok. init(Mod) -> register(Mod, self()), State = Mod:init(), loop(Mod, State). loop(Mod, State) -> receive {call, From, Req} -> {Res, State2} = Mod:handle_call(Req, State), From ! {Mod, Res}, loop(Mod, State2); {cast, Req} -> State2 = Mod:handle_cast(Req, State), loop(Mod, State2) end.
和一个回调模块 ch2.erl :
-module(ch2). -export([start/0]). -export([alloc/0, free/1]). -export([init/0, handle_call/2, handle_cast/2]). start() -> server:start(ch2). alloc() -> server:call(ch2, alloc). free(Ch) -> server:cast(ch2, {free, Ch}). init() -> channels(). handle_call(alloc, Chs) -> alloc(Chs). % => {Ch,Chs2} handle_cast({free, Ch}, Chs) -> free(Ch, Chs). % => Chs2
注意如下几点:
上面的 ch1.erl 和 ch2.erl 中,channels/0, alloc/1 和 free/2 的实现被刻意遗漏,由于与本例无关。完整性起见,下面给出这些函数的一种实现方式。这只是个示例,现实中还必须可以处理诸如 channel 用完没法分配等状况。
channels() -> {_Allocated = [], _Free = lists:seq(1,100)}. alloc({Allocated, [H|T] = _Free}) -> {H, {[H|Allocated], T}}. free(Ch, {Alloc, Free} = Channels) -> case lists:member(Ch, Alloc) of true -> {lists:delete(Ch, Alloc), [Ch|Free]}; false -> Channels end.
没有使用 behaviour 的代码可能效率更高,可是通用性差。将系统中的全部 applications 组织成一致的行为模式很重要。
并且使用 behaviour 能让代码易读易懂。简易的程序结构可能会更有效率,可是比较难理解。
上面的 server 模块其实就是一个简化的 Erlang/OTP behaviour - gen_server。
Erlang/OTP的标配 behaviour 有:
编译器能识别模块属性 -behaviour(Behaviour) ,会对未实现的回调函数发出编译警告,例如:
-module(chs3). -behaviour(gen_server). ... 3> c(chs3). ./chs3.erl:10: Warning: undefined call-back function handle_call/3 {ok,chs3}
Erlang/OTP 自带一些组件,每一个组件实现了特定的功能。这些组件用 Erlang/OTP 术语叫作 application(应用)。例如 Mnesia 就是一个Erlang/OTP 应用,它包含了全部数据库服务所需的功能,还有 Debugger,用来 debug Erlang 代码。基于 Erlang/OTP 的系统,至少必须包含下面两个 application:
应用的概念适用于程序结构(进程)和目录结构(模块)。
最简单的应用由一组功能模块组成,不包含任何进程,这种叫 library application(库应用)。STDLIB 就属于这类。
有进程的应用可使用标准 behaviour 很容易地实现一个监控树。
如何编写应用详见后文 Applications。
一个 release 是一个完整的系统,包含 Erlang/OTP 应用的子集和一系列用户定义的 application。
详见后文 Releases。
怎么在目标环境中部署 release 在系统原则的文档中有讲到。
管理 release 即在一个 release 的不一样版本之间升级或降级,怎么在一个运行中的系统操做这些,详见后文 Release Handling。
这部分可与 stdblib 中的 gen_server(3) 教程(包含了 gen_server 全部接口函数和回调函数)一块儿阅读。
C/S模型就是一个服务器对应任意多个客户端。C/S模型是用来进行资源管理,多个客户端想分享一个公共资源。而服务器则用来管理这个资源。
图 2.1: Client-Server Model
前文有用普通 erlang 写的简单的服务器的例子。使用 gen_server 重写,结果以下:
-module(ch3). -behaviour(gen_server). -export([start_link/0]). -export([alloc/0, free/1]). -export([init/1, handle_call/3, handle_cast/2]). start_link() -> gen_server:start_link({local, ch3}, ch3, [], []). alloc() -> gen_server:call(ch3, alloc). free(Ch) -> gen_server:cast(ch3, {free, Ch}). init(_Args) -> {ok, channels()}. handle_call(alloc, _From, Chs) -> {Ch, Chs2} = alloc(Chs), {reply, Ch, Chs2}. handle_cast({free, Ch}, Chs) -> Chs2 = free(Ch, Chs), {noreply, Chs2}.
下一小节将解释这段代码。
在上一小节的示例中,gen_server 经过调用 ch3:start_link() 启动:
start_link() ->
gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}
start_link 调用了函数 gen_server:start_link/4 ,这个函数产生并链接了一个新进程(一个 gen_server)。
若是名字被省略,gen_server 不会被注册,此时必定要用它的 pid。名字还能够用 {global, Name},这样的话 gen_server 会调用 global:register_name/2 来注册。
接口函数 (start_link, alloc 和 free) 和回调函数 (init, handle_call 和 handle_cast) 放在同一个模块中。这是一个好的编程惯例,把与一个进程相关的代码放在同一个模块中。
若是名字注册成功,这个新的 gen_server 进程会调用回调函数 ch3:init([]) 。init 函数应该返回 {ok, State},其中 State 是 gen_server 的内部状态,在此例中,内部状态指的是 channel 集合。
init(_Args) ->
{ok, channels()}.
gen_server:start_link 是同步调用,在 gen_server 初始化成功可接收请求以前它不会返回。
若是 gen_server 是一个监控树的一部分,supervisor 启动 gen_server 时必定要使用 gen_server:start_link。还有一个函数是 gen_server:start ,这个函数会启动一个独立的 gen_server,也就是说它不会成为监控树的一部分。
同步的请求 alloc() 是用 gen_server:call/2 来实现的:
alloc() ->
gen_server:call(ch3, alloc).
ch3 是 gen_server 的名字,要与进程名字相符合才能使用。alloc 是实际的请求。
这个请求会被转化成一个消息,发送给 gen_server。收到消息后,gen_server 调用 handle_call(Request, From, State) 来处理消息,正常会返回 {reply, Reply, State1}。Reply 是会发回给客户端的回复内容,State1 是 gen_server 新的内部状态。
handle_call(alloc, _From, Chs) -> {Ch, Chs2} = alloc(Chs), {reply, Ch, Chs2}.
此例中,回复内容就是分配给它的 channel Ch,而新的内部状态是剩余的 channel 集合 Chs2。
就这样,ch3:alloc() 返回了分配给它的 channel Ch,gen_server 则保存剩余的 channel 集合,继续等待新的请求。
异步的请求 free(Ch) 是用 gen_server:cast/2 来实现的:
free(Ch) ->
gen_server:cast(ch3, {free, Ch}).
ch3 是 gen_server 的名字,{free, Ch} 是实际的请求。
这个请求会被转化成一个消息,发送给 gen_server。发送后直接返回 ok。
收到消息后,gen_server 调用 handle_cast(Request, State) 来处理消息,正常会返回 {noreply,State1}。State1 是 gen_server 新的内部状态。
handle_cast({free, Ch}, Chs) -> Chs2 = free(Ch, Chs), {noreply, Chs2}.
此例中,新的内部状态是新的剩余的 channel集合 Chs2。而后 gen_server 继续等待新的请求。
在监控树中
若是 gen_server 是监控树的一部分,则不须要终止函数。gen_server 会自动被它的监控者终止,具体怎么终止经过 终止策略 来决定。
若是要在终止前进行一些操做,终止策略必须有一个 time-out 值,且 gen_server 必须在 init 函数中被设置为捕捉 exit 信号。当被要求终止时,gen_server 会调用回调函数 terminate(shutdown, State):
init(Args) -> ..., process_flag(trap_exit, true), ..., {ok, State}. ... terminate(shutdown, State) -> ..code for cleaning up here.. ok.
独立的 gen_server
若是 gen_server 不是监控树的一部分,能够写一个 stop 函数,例如:
... export([stop/0]). ... stop() -> gen_server:cast(ch3, stop). ... handle_cast(stop, State) -> {stop, normal, State}; handle_cast({free, Ch}, State) -> .... ... terminate(normal, State) -> ok.
处理 stop 消息的回调函数返回 {stop, normal, State1},normal 意味着这是一次天然死亡,而 State1 是一个新的 gen_server 内部状态。这会致使 gen_server 调用 terminate(normal, State1) 而后优雅地……挂掉。
若是 gen_server 会在除了请求以外接收其余消息,须要实现回调函数 handle_info(Info, State) 来进行处理。其余消息多是 exit 消息,若是 gen_server 与其余进程链接起来(不是 supervisor),而且被设置为捕捉 exit 信号。
handle_info({'EXIT', Pid, Reason}, State) ->
..code to handle exits here..
{noreply, State1}.
必定要实现 code_change 函数。(译者补充:在代码热更新时会用到)
code_change(OldVsn, State, Extra) -> ..code to convert state (and more) during code change {ok, NewState}.
此章可结合 gen_statem(3) (包含所有接口函数和回调函数的详述)教程一块儿看。
注意:这是 Erlang/OTP 19.0 引入的新 behavior。它已经通过了完整的 review,稳定使用在至少两个大型 OTP 应用中并被保留下来。基于用户反馈,咱们以为有必要在 Erlang/OTP 20.0 对它进行小调整(不向后兼容)。
如今的自动机理论没有具体描述状态变迁是如何触发的,而是假定输出是一个以输入和当前状态为参数的函数,它们是某种类型的值。
对一个事件驱动的状态机来讲,输入就是一个触发状态变迁的事件,输出是状态迁移过程当中执行的动做。用相似有限状态自动机的数学模型来描述,它是一系列以下形式的关系:
State(S) x Event(E) -> Actions(A), State(S')
这些关系能够这么理解:若是咱们如今处于 S 状态,事件 E 发生了,咱们就要执行动做 A 而且转移状态为 S' 。注意: S’ 可能与 S 相同。
因为 A 和 S' 只取决于 S 和 E,这种状态机被称为 Mealy 机(可参见维基百科的描述)。
跟大多数 gen_ 开头的 behavior 同样, gen_statem 保存了 server 的数据和状态。并且状态数是没有限制的(假设虚拟机内存足够),输入事件类型数也是没有限制的,所以用这个 behavior 实现的状态机其实是图灵完备的。不过感受上它更像一个事件驱动的 Mealy 机。
gen_statem 支持两种回调模式:
StateName(EventType, EventContent, Data) ->
... code for actions here ...
{next_state, NewStateName, NewData}.
在示例部分用的最多的就是这种格式。
handle_event(EventType, EventContent, State, Data) ->
... code for actions here ...
{next_state, NewState, NewData}
示例可见单个事件处理器这一小节。
这两种函数都支持其余的返回值,具体可见 gen_statem 的教程页面的 Module:StateName/3。其余的返回元组能够中止状态机、在状态机引擎中执行转移动做、发送回复等等。
选择何种回调方式
这两种回调方式有不一样的功能和限制,可是目标都同样:要处理全部可能的事件和状态的组合。
你能够同时只关心一种状态,确保每一个状态都处理了全部事件。或者只关心一个事件,确保它在全部状态下都被处理。你也能够结合两种策略。
state_functions 方式中,状态只能用 atom 表示,gen_statem 引擎经过状态名来分发处理。它提倡回调模块把一个状态下的全部事件和动做放在代码的同一个地方,以此同时只关注一个状态。
当你的状态图肯定时,这种模式很是好。就像本小节举的例子,状态对应的事件和动做都放在一块儿,每一个状态有本身独一无二的名字。
而经过 handle_event_function 方式,能够结合两种策略,由于全部的事件和状态都在同一个回调函数中。
不管是想以状态仍是事件为中心,这种方式都能知足。不过没有分发到辅助函数的话,Module:handle_event/4 会迅速增加到没法管理。
不论回调模式是哪一种,gen_statem 都会在状态改变的时候(译者补充:进入状态的时候调用)自动调用回调函数(call the state callback),因此你能够在状态的转移规则附近写状态入口回调。一般长这样:
StateName(enter, _OldState, Data) -> ... code for state entry actions here ... {keep_state, NewData}; StateName(EventType, EventContent, Data) -> ... code for actions here ... {next_state, NewStateName, NewData}.
这可能会在特定状况下颇有帮助,不过它要求你在全部状态中都处理入口回调。详见 State Entry Actions。
在第一小节事件驱动的状态机中,动做(action)做为通用状态机模型的一部分被说起。通常的动做会在 gen_statem 处理事件的回调中执行(返回到 gen_statem 引擎以前)。
还有一些特殊的状态迁移动做,在回调函数返回后指定 gen_statem 引擎去执行。回调函数能够在返回的元组中指定一个动做列表。这些动做影响 gen_statem 引擎自己,能够作下列事情:
详见 gen_statem(3) 。你能够回复不少调用者、生成多个后续事件、设置相对时间或绝对时间的超时等等。
事件分红不一样的类型(event types)。同状态下的不一样类型的事件都在同一个回调函数中处理,回调函数以 EventType 和 EventContent 做为参数。
下面列出事件类型和来源的完整列表:
cast
由 gen_statem:cast 生成。
{call, From}
由 gen_statem:call 生成,状态迁移动做返回 {reply, From, Msg} 或调用 gen_statem:reply 时,会用到 From 做为回复地址。
info
发送给 gen_statem 进程的常规进程消息。
state_timeout
状态迁移动做 {state_timeout,Time,EventContent} 生成。
状态迁移动做 {{timeout,Name},Time,EventContent} 生成。
timeout
状态迁移动做 {timeout,Time,EventContent}(或简写为 Time)生成。
internal
状态迁移动做 {next_event,internal,EventContent} 生成。
上述全部事件类型均可以用 {next_event,EventType,EventContent} 来生成。
密码锁的门能够用一个自动机来表述。初始状态,门是锁住的。当有人按一个按钮,即触发一个事件。结合此前按下的按钮,结果多是正确、不完整或者错误。若是正确,门锁会开启10秒钟(10,000毫秒)。若是不完整,则等待下一个按钮被按下。若是错了,一切从头再来,等待新一轮按钮。
图3.1: 密码锁状态图
密码锁状态机用 gen_statem 实现,回调模块以下:
-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock). -export([start_link/1]). -export([button/1]). -export([init/1,callback_mode/0,terminate/3,code_change/4]). -export([locked/3,open/3]). start_link(Code) -> gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). button(Digit) -> gen_statem:cast(?NAME, {button,Digit}). init(Code) -> do_lock(), Data = #{code => Code, remaining => Code}, {ok, locked, Data}. callback_mode() -> state_functions. locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> do_unlock(), {next_state, open, Data#{remaining := Code}, [{state_timeout,10000,lock}]}; [Digit|Rest] -> % Incomplete {next_state, locked, Data#{remaining := Rest}}; _Wrong -> {next_state, locked, Data#{remaining := Code}} end. open(state_timeout, lock, Data) -> do_lock(), {next_state, locked, Data}; open(cast, {button,_}, Data) -> {next_state, open, Data}. do_lock() -> io:format("Lock~n", []). do_unlock() -> io:format("Unlock~n", []). terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}.
下一小节解释代码。
前例中,可调用 code_lock:start_link(Code) 来启动 gen_statem:
start_link(Code) ->
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
start_link 函数调用 gen_statem:start_link/4,生成并链接了一个新进程(gen_statem)。
若是名字注册成功,这个新的 gen_statem 进程会调用 init 回调 code_lock:init(Code)。init 函数应该返回 {ok, State, Data},其中 State 是初始状态(此例中是锁住状态,假设门一开始是锁住的)。Data 是 gen_statem 的内部数据。此例中 Data 是一个map,其中 code 对应的是正确的密码,remaining 对应的是按钮按对后剩余的密码(初始与 code 一致)。
init(Code) -> do_lock(), Data = #{code => Code, remaining => Code}, {ok,locked,Data}.
gen_statem:start_link 是同步调用,在 gen_statem 初始化成功可接收请求以前它不会返回。
若是 gen_statem 是一个监控树的一部分,supervisor 启动 gen_statem 时必定要使用 gen_statem:start_link。还有一个函数是 gen_statem:start ,这个函数会启动一个独立的 gen_statem,也就是说它不会成为监控树的一部分。
callback_mode() ->
state_functions.
函数 Module:callback_mode/0 规定了回调模块的回调模式,此例中是 state_functions 模式,每一个状态有本身的处理函数。
通知 code_lock 按钮事件的函数是用 gen_statem:cast/2 实现的:
button(Digit) ->
gen_statem:cast(?NAME, {button,Digit}).
第一个参数是 gen_statem 的名字,要与进程名字相同,因此咱们用了一样的宏 ?NAME。{button, Digit} 是事件的内容。
这个事件会被转化成一个消息,发送给 gen_statem。当收到事件时, gen_statem 调用 StateName(cast, Event, Data),通常会返回一个元组 {next_state, NewStateName, NewData}。StateName 是当前状态名,NewStateName是下一个状态。NewData 是 gen_statem 的新的内部数据,Actions 是 gen_statem 引擎要执行的动做列表。
locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete do_unlock(), {next_state, open, Data#{remaining := Code}, [{state_timeout,10000,lock}]}; [Digit|Rest] -> % Incomplete {next_state, locked, Data#{remaining := Rest}}; [_|_] -> % Wrong {next_state, locked, Data#{remaining := Code}} end. open(state_timeout, lock, Data) -> do_lock(), {next_state, locked, Data}; open(cast, {button,_}, Data) -> {next_state, open, Data}.
若是门是锁着的,按钮被按下,比较输入按钮和正确的按钮。根据比较的结果,若是锁开了,gen_statem 变为 open 状态,不然继续保持 locked 状态。
若是按钮是错的,数据又变为初始的密码列表。
状态为 open 时,按钮事件会被忽略,状态维持不变。还能够返回 {keep_state, Data} 表示状态不变或者返回 keep_state_and_data 表示状态和数据都不变。
当给出正确的密码,门锁开启,locked/2 返回以下元组:
{next_state, open, Data#{remaining := Code},
[{state_timeout,10000,lock}]};
10,000 是以毫秒为单位的超时时长。10秒后,会触发一个超时,而后 StateName(state_timeout, lock, Data) 被调用,此后门从新锁住:
open(state_timeout, lock, Data) ->
do_lock(),
{next_state, locked, Data};
状态超时会在状态改变的时候自动取消。从新设置一个状态超时至关于重启,旧的定时器被取消,新的定时器被启动。也就是说能够经过重启一个时间为 infinite 的超时来取消状态超时。
有些事件可能在任何状态下到达 gen_statem。能够在一个公共的函数处理这些事件,全部的状态函数都调用它来处理通用的事件。
假定一个 code_length/0 函数返回正确密码的长度(不敏感的信息)。咱们把全部与状态无关的事件分发到公共函数 handle_event/3:
... -export([button/1,code_length/0]). ... code_length() -> gen_statem:call(?NAME, code_length). ... locked(...) -> ... ; locked(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). ... open(...) -> ... ; open(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). handle_event({call,From}, code_length, #{code := Code} = Data) -> {keep_state, Data, [{reply,From,length(Code)}]}.
此例使用 gen_statem:call/2,调用者会等待 server 的回复。{reply,From,Reply} 元组表示回复,{keep_state, ...} 用来保持状态不变。这个返回格式在你想保持状态不变(无论状态是什么)的时候很是方便。
若是使用 handle_event_function 模式,全部的事件都会在 Module:handle_event/4 被处理,咱们能够(也能够不)在第一层以事件为中心进行分组,而后再判断状态:
... -export([handle_event/4]). ... callback_mode() -> handle_event_function. handle_event(cast, {button,Digit}, State, #{code := Code} = Data) -> case State of locked -> case maps:get(remaining, Data) of [Digit] -> % Complete do_unlock(), {next_state, open, Data#{remaining := Code}, [{state_timeout,10000,lock}]}; [Digit|Rest] -> % Incomplete {keep_state, Data#{remaining := Rest}}; [_|_] -> % Wrong {keep_state, Data#{remaining := Code}} end; open -> keep_state_and_data end; handle_event(state_timeout, lock, open, Data) -> do_lock(), {next_state, locked, Data}. ...
在监控树中
若是 gen_statem 是监控树的一部分,则不须要终止函数。gen_statem 自动的被它的监控者终止,具体怎么终止经过 终止策略 来决定。
若是须要在终止前进行一些操做,那么终止策略必须有一个 time-out 值,且 gen_statem 必须在 init 函数中被设置为捕捉 exit 信号,调用 process_flag(trap_exit, true):
init(Args) -> process_flag(trap_exit, true), do_lock(), ...
当被要求终止时,gen_statem 会调用回调函数 terminate(shutdown, State, Data):
terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok.
独立的 gen_statem
若是 gen_statem 不是监控树的一部分,能够写一个 stop 函数(使用 gen_statem:stop)。建议增长一个 API :
...
-export([start_link/1,stop/0]). ... stop() -> gen_statem:stop(?NAME).
这会致使 gen_statem 调用 terminate/3(像监控树中的服务器被终止同样),等待进程终止。
事件超时功能继承自 gen_statem 的前辈 gen_fsm ,事件超时的定时器在有事件达到的时候就会被取消。你能够接收到一个事件或者一个超时,但不会两个都收到。
事件超时由状态迁移动做 {timeout,Time,EventContent} 指定,或者仅仅是 Time, 或者仅仅一个 Timer 而不是动做列表(继承自 gen_fsm)。
不活跃状况下想作点什么时,能够用此类超时。若是30秒内没人按钮,重置密码列表:
... locked( timeout, _, #{code := Code, remaining := Remaining} = Data) -> {next_state, locked, Data#{remaining := Code}}; locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> ... [Digit|Rest] -> % Incomplete {next_state, locked, Data#{remaining := Rest}, 30000}; ...
接收到任意按钮事件时,启动一个30秒超时,若是接收到超时事件就重置密码列表。
接收到其余事件时,事件超时会被取消,因此要么接收到其余事件要么接受到超时事件。因此不能也没必要要重启一个事件超时。由于你处理的任何事件都会取消事件超时。
前面说的状态超时只在状态不改变时有效。而事件超时只在不被其余事件打断的时候生效。
你可能想要在某个状态下开启一个定时器,而在另外一个状态下作处理,想要不改变状态就取消一个定时器,或者但愿同时存在多个定时器。这些均可以用过 generic time-outs 通常超时来实现。它们看起来有点像事件超时,可是它们有名字,不一样名字的能够同时存在多个,而且不会被自动取消。
下面是用通常超时实现来替代状态超时的例子,定时器名字是 open_tm :
... locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> do_unlock(), {next_state, open, Data#{remaining := Code}, [{{timeout,open_tm},10000,lock}]}; ... open({timeout,open_tm}, lock, Data) -> do_lock(), {next_state,locked,Data}; open(cast, {button,_}, Data) -> {keep_state,Data}; ...
和状态超时同样,能够经过给特定的名字设置新的定时器或设置为infinite来取消定时器。
也能够不取消失效的定时器,而是在它到来的时候忽略它(肯定已无用时)。
最全面的处理超时的方式就是使用 erlang 的定时器,详见 erlang:start_timer3,4。大部分的超时任务能够经过 gen_statem 的超时功能来完成,但有时候你可能想获取 erlang:cancel_timer(Tref) 的返回值(剩余时间)。
下面是用 erlang 定时器替代前文状态超时的实现:
... locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> do_unlock(), Tref = erlang:start_timer(10000, self(), lock), {next_state, open, Data#{remaining := Code, timer => Tref}}; ... open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> do_lock(), {next_state,locked,maps:remove(timer, Data)}; open(cast, {button,_}, Data) -> {keep_state,Data}; ...
当状态迁移到 locked 时,咱们能够不从 Data 中清除 timer 的值,由于每次进入 open 状态都是一个新的 timer 值。不过最好不要在 Data 中保留过时的值。
当其余事件触发,你想清除一个 timer 时,可使用 erlang:cancel_timer(Tref) 。若是没有延缓(下一小节会讲到),超时消息被 cancel 后就不会再被收到,因此要确认是否一不当心延缓了这类消息。要注意的是,超时消息可能在你 cancel 它以前就到达,因此要根据 erlang:cancel_timer(Tref) 的返回值,把这消息从进程邮箱里读出来。
另外一种处理方式是,不要 cancel 掉一个 timer,而是在它到达以后忽略它。
若是你想在当前状态忽略某个事件,在后续的某个状态中再处理,你能够延缓这个事件。延缓的事件会在状态变化后从新触发,即:OldState =/= NewState 。
延缓是经过状态迁移动做 postpone 来指定的。
此例中,咱们能够延缓在 open 状态下的按钮事件(而不是忽略它),这些事件会进入等待队列,等到 locked 状态时再处理:
... open(cast, {button,_}, Data) -> {keep_state,Data,[postpone]}; ...
延缓的事件只会在状态改变时从新触发,所以要考虑怎么保存内部数据。内部数据能够在数据 Data 或者状态 State 中保存,好比用两个几乎同样的状态来表示布尔值,或者使用一个复合状态(回调模块的 handle_event_function)。若是某个值的变化会改变事件处理,那须要把这个值保存在状态 State 里。由于 Data 的变化不会触发延缓的事件。
若是你没有用延缓的话,这个不重要。可是若是你决定使用延缓功能,没有用不一样的状态作区分,可能会产生很难发现的 bug。
模糊的状态图
状态图极可能没有给特定的状态指定事件处理方式。可能在相关的上下文中有说起。
可能模糊的动做(译者补充:在状态图中没有给出处理方式,可能对应的动做):忽略(丢弃或者仅仅 log)事件、延缓事件至其余状态处理。
选择性 receive
Erlang 的选择性 receive 语句常常被用来写简单的状态机(不用 gen_statem 的普通 erlang 代码)。下面是可能的实现方式之一:
-module(code_lock). -define(NAME, code_lock_1). -export([start_link/1,button/1]). start_link(Code) -> spawn( fun () -> true = register(?NAME, self()), do_lock(), locked(Code, Code) end). button(Digit) -> ?NAME ! {button,Digit}. locked(Code, [Digit|Remaining]) -> receive {button,Digit} when Remaining =:= [] -> do_unlock(), open(Code); {button,Digit} -> locked(Code, Remaining); {button,_} -> locked(Code, Code) end. open(Code) -> receive after 10000 -> do_lock(), locked(Code, Code) end. do_lock() -> io:format("Locked~n", []). do_unlock() -> io:format("Open~n", []).
此例中选择性 receive 隐含了把 open 状态接收到的全部事件延缓到 locked 状态的逻辑。
选择性 receive 语句不能用在 gen_statem 或者任何 gen_* 中,由于 receive 语句已经在 gen_* 引擎中包含了。为了兼容 sys ,behavior 进程必须对系统消息做出反应,并把非系统的消息传递给回调模块,所以把 receive 集成在引擎层的 loop 里。
动做 postpone(延缓)是被设计来模拟选择性 receive 的。选择性 receive 隐式地延缓全部不被接受的事件,而 postpone 动做则是显示地延缓一个收到的事件。
两种机制逻辑复杂度和时间复杂度是同样的,而选择性 receive 语法的常因子更少。
假设你有一张状态图,图中使用了状态 entry 动做。只有一两个状态有 entry 动做时你能够用自生成事件(详见下一部分),可是使用内置的状态enter回调是更好的选择。
在 callback_mode/0 函数的返回列表中加入 state_enter,会在每次状态改变的时候传入参数 (enter, OldState, ...) 调用一次回调函数。你只需像事件同样处理这些请求便可:
... init(Code) -> process_flag(trap_exit, true), Data = #{code => Code}, {ok, locked, Data}. callback_mode() -> [state_functions,state_enter]. locked(enter, _OldState, Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> {next_state, open, Data}; ... open(enter, _OldState, _Data) -> do_unlock(), {keep_state_and_data, [{state_timeout,10000,lock}]}; open(state_timeout, lock, Data) -> {next_state, locked, Data}; ...
你能够返回 {repeat_state, ...} 、{repeat_state_and_data,_} 或 repeat_state_and_data 来重复执行 entry 代码,这些词其余含义跟 keep_state 家族同样(保持状态、数据不变等等)。详见 state_callback_result() 。
有时候可能须要在状态机中生成事件,能够用状态迁移动做 {next_event,EventType,EventContent} 来实现。
你能够生成全部类型(type)的事件。其中 internal 类型只能经过 next_event 来生成,不会由外部产生,你能够肯定一个 internal 事件是来自状态机自身。
你能够用自生成事件来预处理输入数据,例如解码、用换行分隔数据。有强迫症的人可能会说,应该分出另外一个状态机来发送预处理好的数据给主状态机。为了下降消耗,这个预处理状态机能够经过通常的状态事件处理来实现。
下面的例子为一个输入模型,经过 put_chars(Chars) 输入,enter() 来结束输入:
... -export(put_chars/1, enter/0). ... put_chars(Chars) when is_binary(Chars) -> gen_statem:call(?NAME, {chars,Chars}). enter() -> gen_statem:call(?NAME, enter). ... locked(enter, _OldState, Data) -> do_lock(), {keep_state,Data#{remaining => Code, buf => []}}; ... handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) -> {keep_state, Data#{buf := [Chars|Buf], [{reply,From,ok}]}; handle_event({call,From}, enter, #{buf := Buf} = Data) -> Chars = unicode:characters_to_binary(lists:reverse(Buf)), try binary_to_integer(Chars) of Digit -> {keep_state, Data#{buf := []}, [{reply,From,ok}, {next_event,internal,{button,Chars}}]} catch error:badarg -> {keep_state, Data#{buf := []}, [{reply,From,{error,not_an_integer}}]} end; ...
用 code_lock:start([17]) 启动程序,而后就能经过 code_lock:put_chars(<<"001">>), code_lock:put_chars(<<"7">>), code_lock:enter() 这一系列动做开锁了。
这一小节包含了以前提到的大部分修改,用到了状态 enter 回调,用一个新的状态图来表述:
图 3.2:重写密码锁状态图
注意,图中没有说明 open 状态如何处理按钮事件。须要从其余地方找,由于没标明的事件不是被去掉了,而是在其余状态中进行处理了。图中也没有说明 code_length/0 须要在全部状态中处理。
回调模式:state_functions
使用 state functions:
-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_2). -export([start_link/1,stop/0]). -export([button/1,code_length/0]). -export([init/1,callback_mode/0,terminate/3,code_change/4]). -export([locked/3,open/3]). start_link(Code) -> gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). stop() -> gen_statem:stop(?NAME). button(Digit) -> gen_statem:cast(?NAME, {button,Digit}). code_length() -> gen_statem:call(?NAME, code_length). init(Code) -> process_flag(trap_exit, true), Data = #{code => Code}, {ok, locked, Data}. callback_mode() -> [state_functions,state_enter]. locked(enter, _OldState, #{code := Code} = Data) -> do_lock(), {keep_state, Data#{remaining => Code}}; locked( timeout, _, #{code := Code, remaining := Remaining} = Data) -> {keep_state, Data#{remaining := Code}}; locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete {next_state, open, Data}; [Digit|Rest] -> % Incomplete {keep_state, Data#{remaining := Rest}, 30000}; [_|_] -> % Wrong {keep_state, Data#{remaining := Code}} end; locked(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). open(enter, _OldState, _Data) -> do_unlock(), {keep_state_and_data, [{state_timeout,10000,lock}]}; open(state_timeout, lock, Data) -> {next_state, locked, Data}; open(cast, {button,_}, _) -> {keep_state_and_data, [postpone]}; open(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). handle_event({call,From}, code_length, #{code := Code}) -> {keep_state_and_data, [{reply,From,length(Code)}]}. do_lock() -> io:format("Locked~n", []). do_unlock() -> io:format("Open~n", []). terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}.
回调模式:handle_event_function
这部分描述了如何使用一个 handle_event/4 函数来替换上面的例子。前文提到的在第一层以事件做区分的方式在此例中不太合适,由于有状态 enter 调用,因此用第一层以状态做区分的方式:
... -export([handle_event/4]). ... callback_mode() -> [handle_event_function,state_enter]. %% State: locked handle_event( enter, _OldState, locked, #{code := Code} = Data) -> do_lock(), {keep_state, Data#{remaining => Code}}; handle_event( timeout, _, locked, #{code := Code, remaining := Remaining} = Data) -> {keep_state, Data#{remaining := Code}}; handle_event( cast, {button,Digit}, locked, #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete {next_state, open, Data}; [Digit|Rest] -> % Incomplete {keep_state, Data#{remaining := Rest}, 30000}; [_|_] -> % Wrong {keep_state, Data#{remaining := Code}} end; %% %% State: open handle_event(enter, _OldState, open, _Data) -> do_unlock(), {keep_state_and_data, [{state_timeout,10000,lock}]}; handle_event(state_timeout, lock, open, Data) -> {next_state, locked, Data}; handle_event(cast, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; %% %% Any state handle_event({call,From}, code_length, _State, #{code := Code}) -> {keep_state_and_data, [{reply,From,length(Code)}]}. ...
真正的密码锁中把按钮事件从 locked 状态延迟到 open 状态感受会很奇怪,它只是用来举例说明事件延缓。
目前实现的服务器,会在终止时的错误日志中输出全部的内部状态。包含了门锁密码和剩下须要按的按钮。
这个信息属于敏感信息,你可能不想由于一些不可预料的事情在错误日志中输出这些。
还有可能内部状态数据太多,在错误日志中包含了太多没用的数据,因此须要进行筛选。
你能够经过实现函数 Module:format_status/2 来格式化错误日志中经过 sys:get_status/1,2 得到的内部状态,例如:
... -export([init/1,terminate/3,code_change/4,format_status/2]). ... format_status(Opt, [_PDict,State,Data]) -> StateData = {State, maps:filter( fun (code, _) -> false; (remaining, _) -> false; (_, _) -> true end, Data)}, case Opt of terminate -> StateData; normal -> [{data,[{"State",StateData}]}] end.
实现 Module:format_status/2 并非强制的。若是不实现,默认的实现方式就相似上面这个例子,除了默认不会筛选 Data(即 StateData = {State,Data}),例子中由于有敏感信息必须进行筛选。
回调模式 handle_event_function 支持使用非 atom 的状态(详见回调模式),好比一个复合状态多是一个 tuple。
你可能想在状态变化的时候取消状态超时,或者和延缓事件配合使用控制事件处理,这时候就要用到复合状态。咱们引入可配置的锁门按钮来完善前面的例子(这就是此问题中的状态),这个按钮能够在 open 状态立马锁门,且能够经过 set_lock_button/1 这个接口来设置锁门按钮。
假设咱们在开门的状态调用 set_lock_button,而且此前已经延缓了一个按钮事件(不是旧的锁门按钮,译者补充:是新的锁门按钮)。说这个按钮按得太早不算是锁门按钮,合理。然而门锁状态变为 locked 时,你就会惊奇地发现一个锁门按钮事件触发了。
咱们用 gen_statem:call 来实现 button/1 函数,仍在 open 状态延缓它全部的按钮事件。在 open 状态调用 button/1,状态变为 locked 以前它不会返回,由于 locked 状态时事件才会被处理而且回复。
若是另外一个进程在 button/1 挂起,有人调用 set_lock_button/1 来改变锁门按钮,被挂起的 button 调用会马上生效,门被锁住。所以,咱们把当前的门锁按钮做为状态的一部分,这样当咱们改变门锁按钮时,状态会改变,全部的延缓事件会从新触发。
咱们定义状态为 {StateName,LockButton},其中 StateName 和以前同样,而 LockButton 则表示当前的锁门按钮:
-module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_3). -export([start_link/2,stop/0]). -export([button/1,code_length/0,set_lock_button/1]). -export([init/1,callback_mode/0,terminate/3,code_change/4,format_status/2]). -export([handle_event/4]). start_link(Code, LockButton) -> gen_statem:start_link( {local,?NAME}, ?MODULE, {Code,LockButton}, []). stop() -> gen_statem:stop(?NAME). button(Digit) -> gen_statem:call(?NAME, {button,Digit}). code_length() -> gen_statem:call(?NAME, code_length). set_lock_button(LockButton) -> gen_statem:call(?NAME, {set_lock_button,LockButton}). init({Code,LockButton}) -> process_flag(trap_exit, true), Data = #{code => Code, remaining => undefined}, {ok, {locked,LockButton}, Data}. callback_mode() -> [handle_event_function,state_enter]. handle_event( {call,From}, {set_lock_button,NewLockButton}, {StateName,OldLockButton}, Data) -> {next_state, {StateName,NewLockButton}, Data, [{reply,From,OldLockButton}]}; handle_event( {call,From}, code_length, {_StateName,_LockButton}, #{code := Code}) -> {keep_state_and_data, [{reply,From,length(Code)}]}; %% %% State: locked handle_event( EventType, EventContent, {locked,LockButton}, #{code := Code, remaining := Remaining} = Data) -> case {EventType, EventContent} of {enter, _OldState} -> do_lock(), {keep_state, Data#{remaining := Code}}; {timeout, _} -> {keep_state, Data#{remaining := Code}}; {{call,From}, {button,Digit}} -> case Remaining of [Digit] -> % Complete {next_state, {open,LockButton}, Data, [{reply,From,ok}]}; [Digit|Rest] -> % Incomplete {keep_state, Data#{remaining := Rest, 30000}, [{reply,From,ok}]}; [_|_] -> % Wrong {keep_state, Data#{remaining := Code}, [{reply,From,ok}]} end end; %% %% State: open handle_event( EventType, EventContent, {open,LockButton}, Data) -> case {EventType, EventContent} of {enter, _OldState} -> do_unlock(), {keep_state_and_data, [{state_timeout,10000,lock}]}; {state_timeout, lock} -> {next_state, {locked,LockButton}, Data}; {{call,From}, {button,Digit}} -> if Digit =:= LockButton -> {next_state, {locked,LockButton}, Data, [{reply,From,locked}]}; true -> {keep_state_and_data, [postpone]} end end. do_lock() -> io:format("Locked~n", []). do_unlock() -> io:format("Open~n", []). terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}. format_status(Opt, [_PDict,State,Data]) -> StateData = {State, maps:filter( fun (code, _) -> false; (remaining, _) -> false; (_, _) -> true end, Data)}, case Opt of terminate -> StateData; normal -> [{data,[{"State",StateData}]}] end.
对现实中的锁来讲,button/1 在状态变为 locked 前被挂起不合理。可是做为一个 API,还好。
(译者补充:此挂起跟前文的挂起不一样,前文的挂起仅意味着 receive 阻塞。)
若是一个节点中有不少个 server,而且他们在生命周期中某些时候会空闲,那么这些 server 的堆内存会形成浪费,经过 proc_lib:hibernate/3 来挂起 server 会把它的内存占用降到最低。
注意:挂起一个进程代价很高,详见 erlang:hibernate/3 。不要在每一个事件以后都挂起它。
此例中咱们能够在 {open,_} 状态挂起,由于正常来讲只有在一段时间后它才会收到状态超时,迁移至 locked 状态:
... %% State: open handle_event( EventType, EventContent, {open,LockButton}, Data) -> case {EventType, EventContent} of {enter, _OldState} -> do_unlock(), {keep_state_and_data, [{state_timeout,10000,lock},hibernate]}; ...
最后一行的动做列表中 hibernate 是惟一的修改。若是任何事件在 {open,_} 状态到达,咱们不用再从新挂起,接收事件后 server 会一直处于活跃状态。
若是要从新挂起,咱们须要在更多的地方插入 hibernate 来改变。例如,跟状态无关的 set_lock_button 和 code_length 操做,在 {open,_} 状态可让他 hibernate,可是这样会让代码很乱。
另外一个不经常使用的方法是使用事件超时,在一段时间的不活跃后触发挂起。
本例可能不值得使用挂起来下降堆内存。只有在运行中产生了垃圾的 server 才会从挂起中受益,从这个层面说,上面的是个很差的例子。
此章可结合 gen_event(3)(包含所有接口函数和回调函数的详述)教程一块儿看。
在 OTP 中,一个事件管理器(event manager)是一个能够接收事件的指定的对象。事件(event)多是要记录日志的错误、警告、信息等等。
事件管理器中能够安装(install)0个、1个或更多的事件处理器(event handler)。当事件管理器收到一个事件通知,这个事件被全部安装好的事件处理器处理。例如,一个处理错误的事件管理器可能内置一个默认的处理器,把错误写到终端。若是某段时间须要把错误信息写到文件,用户能够添加另外一个处理器来处理。不须要再写入文件时,则能够删除这个处理器。
事件管理器是一个进程,而事件处理器则是一个回调模块。
事件管理器本质上就是维护一个 {Module, State} 列表,其中 Module 是一个事件处理器,State 则是处理器的内部状态。
将错误信息写到终端的事件处理器的回调模块可能长这样:
-module(terminal_logger). -behaviour(gen_event). -export([init/1, handle_event/2, terminate/2]). init(_Args) -> {ok, []}. handle_event(ErrorMsg, State) -> io:format("***Error*** ~p~n", [ErrorMsg]), {ok, State}. terminate(_Args, _State) -> ok.
将错误信息写到文件的事件处理器的回调模块可能长这样:
-module(file_logger). -behaviour(gen_event). -export([init/1, handle_event/2, terminate/2]). init(File) -> {ok, Fd} = file:open(File, read), {ok, Fd}. handle_event(ErrorMsg, Fd) -> io:format(Fd, "***Error*** ~p~n", [ErrorMsg]), {ok, Fd}. terminate(_Args, Fd) -> file:close(Fd).
下一小节分析这些代码。
调用下面的函数来开启一个前例中说的处理错误的事件管理器:
gen_event:start_link({local, error_man})
这个函数建立并链接一个新进程(事件管理器 event manager)。
参数 {local, error_man} 指定了事件管理器的名字,事件管理器在本地注册为 error_man。
若是名字参数被忽略,事件管理器不会被注册,则必须用到它的进程 pid。名字还能够用{global, Name},这样的话会调用 global:register_name/2 来注册事件管理器。
若是 gen_event 是一个监控树的一部分,supervisor 启动 gen_event 时必定要使用 gen_event:start_link。还有一个函数是 gen_event:start ,这个函数会启动一个独立的 gen_event,也就是说它不会成为监控树的一部分。
下例代表了在 shell 中,如何开启一个事件管理器,并为它添加一个事件处理器:
1> gen_event:start({local, error_man}). {ok,<0.31.0>} 2> gen_event:add_handler(error_man, terminal_logger, []). ok
这个函数会发送一个消息给事件处理器 error_man,告诉它须要添加一个事件处理器 terminal_logger。事件管理器会调用函数 terminal_logger:init([]) (init 的参数 [] 是 add_handler 的第三个参数)。正常的话 init 会返回 {ok, State},State就是事件处理器的内部状态。
init(_Args) ->
{ok, []}.
此例中 init 不须要任何输入,所以忽略了它的参数。terminal_logger 中不须要用到内部状态,file_logger 能够用内部状态来保存文件描述符。
init(File) -> {ok, Fd} = file:open(File, read), {ok, Fd}.
3> gen_event:notify(error_man, no_reply). ***Error*** no_reply ok
其中 error_man 是事件处理器的注册名,no_reply 是事件。
这个事件会以消息的形式发送给事件处理器。接收事件时,事件管理器会按照安装的顺序,依次调用每一个事件处理器的 handle_event(Event, State)。handle_event 正常会返回元组 {ok,State1},其中 State1 是事件处理器的新的内部状态。
terminal_logger 中:
handle_event(ErrorMsg, State) -> io:format("***Error*** ~p~n", [ErrorMsg]), {ok, State}.
file_logger 中:
handle_event(ErrorMsg, Fd) -> io:format(Fd, "***Error*** ~p~n", [ErrorMsg]), {ok, Fd}.
4> gen_event:delete_handler(error_man, terminal_logger, []).
ok
这个函数会发送一条消息给注册名为 error_man 的事件管理器,告诉它要删除处理器 terminal_logger。此时管理器会调用 terminal_logger:terminate([], State),其中 [] 是 delete_handler 的第三个参数。terminate 中应该作与 init 相反的事情,作一些清理工做。它的返回值会被忽略。
terminal_logger 不须要作清理:
terminate(_Args, _State) ->
ok.
file_logger 须要关闭 init 中开启的文件描述符:
terminate(_Args, Fd) ->
file:close(Fd).
当事件管理器被终止,它会调用每一个处理器的 terminate/2,和删除处理器时同样。
在监控树中
若是管理器是监控树的一部分,则不须要终止函数。管理器自动的被它的监控者终止,具体怎么终止经过 终止策略 来决定。
独立的事件管理器
事件管理器能够经过调用如下函数终止:
> gen_event:stop(error_man).
ok
若是想要处理事件以外的其余消息,须要实现回调函数 handle_info(Info, StateName, StateData)。好比说 exit 消息,当 gen_event 与其余进程(非它的监控者)链接,而且被设置为捕捉 exit 信号。
handle_info({'EXIT', Pid, Reason}, State) ->
..code to handle exits here..
{ok, NewState}.
code_change 函数也须要实现。
code_change(OldVsn, State, Extra) -> ..code to convert state (and more) during code change {ok, NewState}
这部分可与 stdblib 中的 supervisor(3) 教程(包含了全部细节)一块儿阅读。
监控者(supervisor)要负责开启、终止和监控它的子进程。监控者的基本理念就是经过必要时的重启,来保证子进程一直活着。
子进程规格说明指定了要启动和监控的子进程。子进程根据规格列表依次启动,终止顺序和启动顺序相反。
下面的例子是启动 gen_server 子进程的监控树:
-module(ch_sup). -behaviour(supervisor). -export([start_link/0]). -export([init/1]). start_link() -> supervisor:start_link(ch_sup, []). init(_Args) -> SupFlags = #{strategy => one_for_one, intensity => 1, period => 5}, ChildSpecs = [#{id => ch3, start => {ch3, start_link, []}, restart => permanent, shutdown => brutal_kill, type => worker, modules => [cg3]}], {ok, {SupFlags, ChildSpecs}}.
返回值中的 SupFlags 即 supervisor flag,详见下一小节。
ChildSpecs 是子进程规格列表。
下面是 supervisor flag 的类型定义:
sup_flags() = #{strategy => strategy(), % optional intensity => non_neg_integer(), % optional period => pos_integer()} % optional strategy() = one_for_all | one_for_one | rest_for_one | simple_one_for_one
重启策略是由 init 返回的 map 中的 strategy 来指定的:
SupFlags = #{strategy => Strategy, ...}
strategy 是可选参数,若是没有指定,默认为 one_for_one。
若是子进程终止,只有终止的子进程会被重启。
图5.1 one_for_one 监控树
若是一个子进程终止,其余子进程都会被终止,而后全部子进程被重启。
图5.2 one_for_all 监控树
若是一个子进程终止,启动顺序在此子进程以后的子进程们都会被终止。而后这些终止的进程(包括本身终止的那位)被重启。
详见 simple-one-for-one supervisors(译者补充:本原则中也有说起simple_one_for_one)
supervisor 内置了一个机制来限制给定时间间隔内的重启次数。由 init 函数返回的 supervisor flag 中的 intensity 和 period 字段来指定:
SupFlags = #{intensity => MaxR, period => MaxT, ...}
若是 MaxT 秒内重启了 MaxR 次,监控者会终止全部的子进程,而后退出。此时 supervisor 退出的理由是 shutdown。
当 supervisor 终止时,它的上一级 supervisor 会做出一些处理,重启它,或者跟着退出。
这个重启机制的目的是防止进程反复由于同一缘由终止和重启。
intensity 和 period 都是可选参数,若是没有指定,它们缺省值分别为1和5。
缺省值为5秒重启1次。这个配置对大部分系统(即使是很深的监控树)来讲都是保险的,但你可能想为某些特殊的应用场景作出调整。
首先,intensity 决定了你能忍受多少次突发重启。例如,你只能接受5~10次的重启尝试(尽管下一秒它可能会重启成功)。
其次,若是崩溃持续发生,可是没有频繁到让 supervisor 放弃,你须要考虑持续的失败率。好比说你把 intensity 设置为10,而 period 为1,supervisor 会容许子进程在1秒内重启10次,在人工干预前它会持续往日志中写入 crash 报告。
此时你须要把 period 设置得足够大,让 supervisor 在你能接受的比值下运行。例如,你将 intensity 设置为5,period 为30s,会让它在一段时间内容许平均6s的重启间隔,这样你的日志就不会太快被填满,你能够观察错误,而后做出修复。
这些选择取决于你的问题做用域。若是你不会实时监测或者不能快速解决问题(例如在嵌入式系统中),你可能想1分钟最多重启一次,把问题交给更高层去自动清理错误。或者有时候,可能高失败率时仍然尝试重启是更好的选择,你能够设置成一秒1-2次重启。
避免一些常见的错误:
例如,若是最上层容许10次重启,第二层也容许10次,下层崩溃的子进程会被重启100次,这太多了。最上层容许3次重启可能更好。
下面是子进程规格(child specification)的类型定义:
child_spec() = #{id => child_id(), % mandatory start => mfargs(), % mandatory restart => restart(), % optional shutdown => shutdown(), % optional type => worker(), % optional modules => modules()} % optional child_id() = term() mfargs() = {M :: module(), F :: atom(), A :: [term()]} modules() = [module()] | dynamic restart() = permanent | transient | temporary shutdown() = brutal_kill | timeout() worker() = worker | supervisor
id 是必填项
有时 id 会被称为 name,如今通常都用 identifier 或者 id,但为了向后兼容,有时也能看到 name,例如在错误信息中。
它应该(或者最终应该)调用下面这些函数:
start 是必填项。
restart 是可选项,缺省值为 permanent。
警告:当子进程是 worker 时慎用 infinity。由于这种状况下,监控树的退出取决于子进程的退出,必需要安全地实现子进程,确保它的清理过程一定会返回。
shutdown 是可选项,若是子进程是 worker,默认为 5000;若是子进程是监控树,默认为 infinity。
type 是可选项,缺省值为 worker。
这个字段在发布管理的升级和降级中会用到,详见 Release Handling。
modules 是可选项,缺省值为 [M],其中 M 来自子进程的启动参数 {M,F,A} 。
例:前例中 ch3 的子进程规格以下:
#{id => ch3, start => {ch3, start_link, []}, restart => permanent, shutdown => brutal_kill, type => worker, modules => [ch3]}
或者简化一下,取默认值:
#{id => ch3, start => {ch3, start_link, []} shutdown => brutal_kill}
例:上文的 gen_event 子进程规格以下:
#{id => error_man, start => {gen_event, start_link, [{local, error_man}]}, modules => dynamic}
这两个都是注册进程,都被指望一直能访问到。因此他们被指定为 permanent 。
ch3 在终止前不须要作任何清理工做,因此不须要指定终止时间,shudown 值设置为 brutal_kill 就好了。而 error_man 须要时间去清理,因此设置为5000毫秒(默认值)。
例:启动另外一个 supervisor 的子进程规格:
#{id => sup, start => {sup, start_link, []}, restart => transient, type => supervisor} % will cause default shutdown=>infinity (type为supervisor会致使shutdown的默认值为infinity)
前例中,supervisor 经过调用 ch_sup:start_link() 来启动:
start_link() ->
supervisor:start_link(ch_sup, []).
ch_sup:start_link 函数调用 supervisor:start_link/2,生成并链接了一个新进程(supervisor)。
此例中 supervisor 没有被注册,所以必须用到它的 pid。能够经过调用 supervisor:start_link({local, Name}, Module, Args) 或 supervisor:start_link({global, Name}, Module, Args) 来指定它的名字。
这个新的 supervisor 进程会调用 init 回调 ch_sup:init([])。init 函数应该返回 {ok, {SupFlags, ChildSpecs}}。
init(_Args) -> SupFlags = #{}, ChildSpecs = [#{id => ch3, start => {ch3, start_link, []}, shutdown => brutal_kill}], {ok, {SupFlags, ChildSpecs}}.
而后 supervisor 会根据子进程规格列表,启动全部的子进程。此例中只有一个子进程,ch3 。
supervisor:start_link 是同步调用,在全部子进程启动以前它不会返回。
除了静态的监控树外,还能够动态地添加子进程到监控树中:
supervisor:start_child(Sup, ChildSpec)
Sup 是 supervisor 的 pid 或注册名。ChildSpec 是子进程规格。
使用 start_child/2 添加的子进程跟其余子进程行为同样,除了一点:若是 supervisor 终止并被重启,全部动态添加的进程都会丢失。
调用下面的函数,静态或动态的子进程,都会根据规格终止:
supervisor:terminate_child(Sup, Id)
一个终止的子进程的规格可经过下面的函数删除:
supervisor:delete_child(Sup, Id)
Sup 是 supervisor 的 pid 或注册名。Id 是子进程规格中的 id 项。
删除静态的子进程规格会致使它跟动态子进程同样,在 supervisor 重启时丢失。
重启策略 simple_one_for_one 是简化的 one_for_one,全部的子进程是相同过程的实例,被动态地添加到监控树中。
下面是一个 simple_one_for_one 的 supervisor 回调模块:
-module(simple_sup). -behaviour(supervisor). -export([start_link/0]). -export([init/1]). start_link() -> supervisor:start_link(simple_sup, []). init(_Args) -> SupFlags = #{strategy => simple_one_for_one, intensity => 0, period => 1}, ChildSpecs = [#{id => call, start => {call, start_link, []}, shutdown => brutal_kill}], {ok, {SupFlags, ChildSpecs}}.
启动时,supervisor 没有启动任何子进程。全部的子进程是经过调用以下函数动态添加的:
supervisor:start_child(Pid, [id1])
子进程会经过调用 apply(call, start_link, []++[id1]) 来启动,即:
call:start_link(id1)
simple_one_for_one 监程的子进程经过下面的方式来终止:
supervisor:terminate_child(Sup, Pid)
Sup 是 supervisor 的 pid 或注册名。Pid 是子进程的 pid。
因为 simple_one_for_one 的监程可能有大量的子进程,因此它是异步终止它们的。就是说子进程平行地作清理工做,终止顺序不可预测。
因为 supervisor 是监控树的一部分,它会自动地被它的 supervisor 终止。当被要求终止时,它会根据 shutdown 配置按照与启动相反的顺序(译者补充:除了 simple_one_for_one 模式)终止全部的子进程,而后退出。
sys 模块包含一些函数,能够简单地 debug 用 behaviour 实现的进程。还有一些函数能够和 proc_lib 模块的函数一块儿,用来实现特殊的进程,这些特殊的进程不采用标准的 behaviour,可是知足 OTP 设计原则。这些函数还能够用来实现用户自定义(非标准)的 behaviour。
sys 和 proc_lib 模块都属于 STDLIB 应用。
sys 模块包含一些函数,能够简单地 debug 用 behaviour 实现的进程。用 gen_statem Behaviour 中的例子 code_lock 举例:
Erlang/OTP 20 [DEVELOPMENT] [erts-9.0] [source-5ace45e] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] Eshell V9.0 (abort with ^G) 1> code_lock:start_link([1,2,3,4]). Lock {ok,<0.63.0>} 2> sys:statistics(code_lock, true). ok 3> sys:trace(code_lock, true). ok 4> code_lock:button(1). *DBG* code_lock receive cast {button,1} in state locked ok *DBG* code_lock consume cast {button,1} in state locked 5> code_lock:button(2). *DBG* code_lock receive cast {button,2} in state locked ok *DBG* code_lock consume cast {button,2} in state locked 6> code_lock:button(3). *DBG* code_lock receive cast {button,3} in state locked ok *DBG* code_lock consume cast {button,3} in state locked 7> code_lock:button(4). *DBG* code_lock receive cast {button,4} in state locked ok Unlock *DBG* code_lock consume cast {button,4} in state locked *DBG* code_lock receive state_timeout lock in state open Lock *DBG* code_lock consume state_timeout lock in state open 8> sys:statistics(code_lock, get). {ok,[{start_time,{{2017,4,21},{16,8,7}}}, {current_time,{{2017,4,21},{16,9,42}}}, {reductions,2973}, {messages_in,5}, {messages_out,0}]} 9> sys:statistics(code_lock, false). ok 10> sys:trace(code_lock, false). ok 11> sys:get_status(code_lock). {status,<0.63.0>, {module,gen_statem}, [[{'$initial_call',{code_lock,init,1}}, {'$ancestors',[<0.61.0>]}], running,<0.61.0>,[], [{header,"Status for state machine code_lock"}, {data,[{"Status",running}, {"Parent",<0.61.0>}, {"Logged Events",[]}, {"Postponed",[]}]}, {data,[{"State", {locked,#{code => [1,2,3,4],remaining => [1,2,3,4]}}}]}]]}
此小节讲述怎么不使用标准 behaviour 来写一个程序,使它知足 OTP 设计原则。这样一个进程须要知足:
系统消息是在监控树中用到的、有特殊意义的消息。典型的系统消息有追踪输出的请求、挂起或恢复进程的请求(release handling 发布管理中用到)。使用标准 behaviour 实现的进程能自动处理这些消息。
概述里面的简单服务器,使用 sys 和 proc_lib 来实现以使其可归入监控树中:
-module(ch4). -export([start_link/0]). -export([alloc/0, free/1]). -export([init/1]). -export([system_continue/3, system_terminate/4, write_debug/3, system_get_state/1, system_replace_state/2]). start_link() -> proc_lib:start_link(ch4, init, [self()]). alloc() -> ch4 ! {self(), alloc}, receive {ch4, Res} -> Res end. free(Ch) -> ch4 ! {free, Ch}, ok. init(Parent) -> register(ch4, self()), Chs = channels(), Deb = sys:debug_options([]), proc_lib:init_ack(Parent, {ok, self()}), loop(Chs, Parent, Deb). loop(Chs, Parent, Deb) -> receive {From, alloc} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, alloc, From}), {Ch, Chs2} = alloc(Chs), From ! {ch4, Ch}, Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3, ch4, {out, {ch4, Ch}, From}), loop(Chs2, Parent, Deb3); {free, Ch} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, {free, Ch}}), Chs2 = free(Ch, Chs), loop(Chs2, Parent, Deb2); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ch4, Deb, Chs) end. system_continue(Parent, Deb, Chs) -> loop(Chs, Parent, Deb). system_terminate(Reason, _Parent, _Deb, _Chs) -> exit(Reason). system_get_state(Chs) -> {ok, Chs}. system_replace_state(StateFun, Chs) -> NChs = StateFun(Chs), {ok, NChs, NChs}. write_debug(Dev, Event, Name) -> io:format(Dev, "~p event = ~p~n", [Name, Event]).
sys 模块中的简易 debug 也可用于 ch4:
% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> ch4:start_link(). {ok,<0.30.0>} 2> sys:statistics(ch4, true). ok 3> sys:trace(ch4, true). ok 4> ch4:alloc(). ch4 event = {in,alloc,<0.25.0>} ch4 event = {out,{ch4,ch1},<0.25.0>} ch1 5> ch4:free(ch1). ch4 event = {in,{free,ch1}} ok 6> sys:statistics(ch4, get). {ok,[{start_time,{{2003,6,13},{9,47,5}}}, {current_time,{{2003,6,13},{9,47,56}}}, {reductions,109}, {messages_in,2}, {messages_out,1}]} 7> sys:statistics(ch4, false). ok 8> sys:trace(ch4, false). ok 9> sys:get_status(ch4). {status,<0.30.0>, {module,ch4}, [[{'$ancestors',[<0.25.0>]},{'$initial_call',{ch4,init,[<0.25.0>]}}], running,<0.25.0>,[], [ch1,ch2,ch3]]}
proc_lib 中的一些函数可用来启动进程。有几个函数可选,如:异步启动 spawn_link/3,4 和同步启动 start_link/3,4,5 。
使用这些函数启动的进程会存储一些信息(好比高层级进程 ancestor 和初始化回调 initial call),这些信息在监控树中会被用到。
若是进程以除 normal 或 shutdown 以外的理由终止,会生成一个 crash 报告。能够在 SASL 的用户手册中了解更多 crash 报告的内容。
此例中,使用了同步启动。进程经过 ch4:start_link() 来启动:
start_link() ->
proc_lib:start_link(ch4, init, [self()]).
ch4:start_link 调用了函数 proc_lib:start_link 。这个函数的参数为模块名、函数名和参数列表,它建立并链接到一个新进程。新进程执行给定的函数来启动,ch4:init(Pid),其中 Pid 是第一个进程的 pid,即父进程。
全部的初始化(包括名字注册)都在 init 中完成。新进程须要通知父进程它的启动:
init(Parent) ->
...
proc_lib:init_ack(Parent, {ok, self()}),
loop(...).
proc_lib:start_link 是同步函数,在 proc_lib:init_ack 被调用前不会返回。
要支持 sys 的 debug 工具,须要 debug 结构。Deb 经过 sys:debug_options/1 来初始生成:
init(Parent) -> ... Deb = sys:debug_options([]), ... loop(Chs, Parent, Deb).
sys:debug_options/1 的参数为一个选项列表。此例中列表为空,即初始时没有 debug 被启用。可用选项详见 sys 模块的用户手册。
而后,对于每一个要记录或追踪的系统事件,下面的函数会被调用:
sys:handle_debug(Deb, Func, Info, Event) => Deb1
其中:
handle_debug 返回一个更新的 debug 结构 Deb1。
此例中,handle_debug 会在每次输入和输出信息时被调用。格式化函数 Func 即 ch4:write_debug/3,它调用 io:format/3 打印消息:
loop(Chs, Parent, Deb) -> receive {From, alloc} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, alloc, From}), {Ch, Chs2} = alloc(Chs), From ! {ch4, Ch}, Deb3 = sys:handle_debug(Deb2, fun ch4:write_debug/3, ch4, {out, {ch4, Ch}, From}), loop(Chs2, Parent, Deb3); {free, Ch} -> Deb2 = sys:handle_debug(Deb, fun ch4:write_debug/3, ch4, {in, {free, Ch}}), Chs2 = free(Ch, Chs), loop(Chs2, Parent, Deb2); ... end. write_debug(Dev, Event, Name) -> io:format(Dev, "~p event = ~p~n", [Name, Event]).
收到的系统消息形如:
{system, From, Request}
这些消息的内容和意义,进程不须要理解,而是直接调用下面的函数:
sys:handle_system_msg(Request, From, Parent, Module, Deb, State)
这个函数不会返回。它处理了系统消息以后,若是要继续执行,会调用:
Module:system_continue(Parent, Deb, State)
若是进程终止,调用:
Module:system_terminate(Reason, Parent, Deb, State)
监控树中的进程应以父进程相同的理由退出。
若是进程要返回它的状态,handle_system_msg 会调用:
Module:system_get_state(State)
若是进程要调用函数 StateFun 替换它的状态,handle_system_msg 会调用:
Module:system_replace_state(StateFun, State)
此例中对应代码:
loop(Chs, Parent, Deb) -> receive ... {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ch4, Deb, Chs) end. system_continue(Parent, Deb, Chs) -> loop(Chs, Parent, Deb). system_terminate(Reason, Parent, Deb, Chs) -> exit(Reason). system_get_state(Chs) -> {ok, Chs, Chs}. system_replace_state(StateFun, Chs) -> NChs = StateFun(Chs), {ok, NChs, NChs}.
若是这个特殊的进程设置为捕捉 exit 信号,而且父进程终止,它的预期行为是以一样的理由终止:
init(...) -> ..., process_flag(trap_exit, true), ..., loop(...). loop(...) -> receive ... {'EXIT', Parent, Reason} -> ..maybe some cleaning up here.. exit(Reason); ... end.
要实现自定义 behaviour,代码跟特殊进程差很少,除了要调用回调模块里的函数来处理特殊的任务。
若是想要编译器像对 OTP 的 behaviour 同样,给缺乏的回调函数报警告,须要在 behaviour 模块增长 -callback 属性来描述预期的回调:
-callback Name1(Arg1_1, Arg1_2, ..., Arg1_N1) -> Res1. -callback Name2(Arg2_1, Arg2_2, ..., Arg2_N2) -> Res2. ... -callback NameM(ArgM_1, ArgM_2, ..., ArgM_NM) -> ResM.
NameX 是预期的回调名。ArgX_Y 和 ResX 是 Types and Function Specifications 中所描述的类型。-callback 属性支持 -spec 的全部语法。
-optional_callbacks 属性能够用来指定可选的回调:
-optional_callbacks([OptName1/OptArity1, ..., OptNameK/OptArityK]).
其中每一个 OptName/OptArity 指定了一个回调函数的名字和参数个数。-optional_callbacks 应与 -callback 一块儿使用,它不能与下文的 behaviour_info() 结合使用。
注意:咱们推荐使用 -callback 而不是 behaviour_info() 函数。由于工具能够用额外的类型信息来生成文档和找出矛盾。
你也能够实现并导出 behaviour_info() 来替代 -callback 和 -optional_callbacks 属性:
behaviour_info(callbacks) ->
[{Name1, Arity1},...,{NameN, ArityN}].
其中每一个 {Name, Arity} 指定了回调函数的名字和参数个数。使用 -callback 属性会自动生成这个函数。
当编译器在模块 Mod 中遇到属性 -behaviour(Behaviour),它会调用 Behaviour:behaviour_info(callbacks),而且与 Mod 实际导出的函数集相比较,在缺乏回调函数的时候发布一个警告。
例:
%% User-defined behaviour module -module(simple_server). -export([start_link/2, init/3, ...]). -callback init(State :: term()) -> 'ok'. -callback handle_req(Req :: term(), State :: term()) -> {'ok', Reply :: term()}. -callback terminate() -> 'ok'. -callback format_state(State :: term()) -> term(). -optional_callbacks([format_state/1]). %% Alternatively you may define: %% %% -export([behaviour_info/1]). %% behaviour_info(callbacks) -> %% [{init,1}, %% {handle_req,2}, %% {terminate,0}]. start_link(Name, Module) -> proc_lib:start_link(?MODULE, init, [self(), Name, Module]). init(Parent, Name, Module) -> register(Name, self()), ..., Dbg = sys:debug_options([]), proc_lib:init_ack(Parent, {ok, self()}), loop(Parent, Module, Deb, ...). ...
在回调模块中:
-module(db). -behaviour(simple_server). -export([init/1, handle_req/2, terminate/0]). ...
behaviour 模块中 -callback 属性指定的协议,在回调模块中能够添加 -spec 属性来优化。-callback 指定的协议通常都比较宽泛,因此 -spec 会很是有用。有协议的回调模块:
-module(db). -behaviour(simple_server). -export([init/1, handle_req/2, terminate/0]). -record(state, {field1 :: [atom()], field2 :: integer()}). -type state() :: #state{}. -type request() :: {'store', term(), term()}; {'lookup', term()}. ... -spec handle_req(request(), state()) -> {'ok', term()}. ...
每一个 -spec 协议都是对应的 -callback 协议的子类型。
此部分可与 Kernel 手册中的 app 和 application 部分一块儿阅读。
若是你编码实现了一些特定的功能,你可能想把它封装成一个应用,能够做为一个总体启动和终止,在其余系统中能够重用等。
要作到这一点,须要建立一个应用回调模块,描述怎么启动和终止这个应用。
而后还须要一个应用规格说明(application specification),把它放在应用资源文件中。这个文件指定了组成应用的模块列表以及回调模块名。
若是你使用 Erlang/OTP 的代码打包工具 systools(详见 Releases),每一个应用的代码都放在不一样的目录下,并遵循预约义的目录结构 。
在下面两个回调函数中,指定了怎么启动和终止应用(即监控树):
start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State}
stop(State)
打包前文 Supervisor Behaviour 的监控树为一个应用,应用回调模块以下:
-module(ch_app). -behaviour(application). -export([start/2, stop/1]). start(_Type, _Args) -> ch_sup:start_link(). stop(_State) -> ok.
库应用不须要启动和终止,因此不须要应用回调模块。
应用的规格说明用来配置一个应用,它放在应用资源文件中,简称 .app 文件:
{application, Application, [Opt1,...,OptN]}.
库应用的最简短的 .app 文件长这样(libapp 应用):
{application, libapp, []}.
有监控树的应用最简短的 .app 文件长这样(ch_app 应用):
{application, ch_app,
[{mod, {ch_app,[]}}]}.
mod 定义了应用的回调模块(ch_app)和启动参数([]),应用启动时会调用:
ch_app:start(normal, [])
应用终止后会调用:
ch_app:stop([])
当使用 Erlang/OTP 的代码打包工具 systools(详见 Releases),还要指定 description、vsn、modules、registered 和 applications:
{application, ch_app, [{description, "Channel allocator"}, {vsn, "1"}, {modules, [ch_app, ch_sup, ch3]}, {registered, [ch3]}, {applications, [kernel, stdlib, sasl]}, {mod, {ch_app,[]}} ]}.
description - 简短的描述,字符串,默认为 ""。
vsn - 版本号,字符串,默认为 ""。
modules - 应用引入的全部模块,在生成启动脚本和 tar 文件的时候 systools 会用到此列表。默认为 [] 。
registered - 应用中全部注册的进程名。systools 会用它来检测应用间的名字冲突。默认为 [] 。
注意:应用资源文件的语法和内容,详见Kernel中的app手册
使用 systools 来打包代码,每一个应用的代码会放在单独的目录下:lib/Application-Vsn,其中 Vsn 是版本号。
即使不用 systools 打包,因为 Erlang 是根据 OTP 原则打包,它会有一个特定的目录结构。若是应用存在多个版本,code server(详见 code(3) )会自动使用版本号最高的目录的代码。
只要发布环境的目录结构遵循规定,开发目录结构怎么样都行,但仍是建议在开发环境中使用相同的目录结构。目录名中的版本号要略掉,由于版本是发布步骤的一部分。
有些子目录是必须的。有些子目录是可选的,应用须要才有。还有些子目录是推荐有的,也就是说建议您按下面说的使用它。例如,文档 doc 和测试 test 目录是建议在应用中包含的,以成为一个合格的 OTP 应用。
─ ${application} ├── doc │ ├── internal │ ├── examples │ └── src ├── include ├── priv ├── src │ └── ${application}.app.src └── test
开发环境可能还须要其余文件夹。例如,若是有其余语言的源码,好比说 C 语言写的 NIF,应该把它们放在其余目录。按照惯例,应该以语言名为前缀命名目录,好比说 C 语言用 c_src,Java 用 java_src,Go 用 go_src 。后缀 _src 意味着这个文件夹里的文件是编译和应用步骤中的一部分。最终构建好的文件应放在 priv/lib 或 priv/bin 目录下。
priv 目录存放应用运行时须要的资源。可执行文件应放在 priv/bin 目录,动态连接应放在 priv/bin 目录。其余资源能够随意放在 priv 目录下,不过最好用结构化的方式组织。
生成 erlang 代码的其余语言代码,好比 ASN.1 和 Mibs,应该放在顶层目录或 src 目录的子目录中,子目录以语言名命名(如 asn1 和 mibs)。构建文件应放在相应的语言目录下,好比 erlang 对应 src 目录,java 对应 java_src 目录。
开发环境的 .app 文件可能放在 ebin 目录下,不过建议在构建时再把它放过去。惯常作法是使用 .app.src 文件,存放在 src 目录。.app.src 文件和 .app 文件基本上是同样的,只是某些字段会在构建阶段被替换,好比应用版本号。
目录名不该该用大写字母。
建议删掉空目录。
应用的发布版必须遵循特定的目录结构。
─ ${application}-${version} ├── bin ├── doc │ ├── html │ ├── man[1-9] │ ├── pdf │ ├── internal │ └── examples ├── ebin │ └── ${application}.app ├── include ├── priv │ ├── lib │ └── bin └── src
src 目录可用于 debug,但不是必须有的。include 目录只有在应用有公开的 include 文件时会用到。
推荐你们以上面的方式发布帮助文档(doc/man...),通常 HTML 和 PDF 会以其余方式发布。
建议删掉空目录。
当 erlang 运行时系统启动,Kernel 应用会启动不少进程,其中一个进程是应用控制器(application controller)进程,注册名为 application_controller 。
应用的全部操做都是经过控制器来协调的。它使用了 application 模块的一些函数,详见 application 模块的文档。它控制应用的加载、卸载、启动和终止。
应用启动前,必定要先加载它。控制器会读取并存储 .app 文件中的信息:
1> application:load(ch_app). ok 2> application:loaded_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"}, {stdlib,"ERTS CXC 138 10","1.11.4.3"}, {ch_app,"Channel allocator","1"}]
终止或者未启动的应用能够被卸载。卸载时,应用的信息会从控制器的内部数据库中清除:
3> application:unload(ch_app). ok 4> application:loaded_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"}, {stdlib,"ERTS CXC 138 10","1.11.4.3"}]
注意:加载或卸载应用不会加载或卸载应用的代码。代码加载是以平时的方式处理的。
启动应用:
5> application:start(ch_app). ok 6> application:which_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"}, {stdlib,"ERTS CXC 138 10","1.11.4.3"}, {ch_app,"Channel allocator","1"}]
若是应用没被加载,控制器会先调用 application:load/1 来加载它。它校验 applications 的值,确保这个配置中的全部应用在此应用运行前都已经启动了。
而后控制器为应用建立一个 application master 。这个 master 是应用中全部进程的组长。master 经过调用应用回调函数 start/2 来启动应用,应用回调由 mod 配置指定。
调用下面的函数,应用会被终止,但不会被卸载:
7> application:stop(ch_app). ok
master 经过 shutdown 顶层 supervisor 来终止应用。顶层 supervisor 通知它全部的子进程终止,层层下推,整个监控树会以与启动相反的顺序终止。而后 master 会调用回调函数 stop/1(mod 配置指定的应用回调模块)。
能够经过配置参数来配置应用。配置参数就是 .app 文件中的 env 字段对应的一个 {Par,Val} 列表:
{application, ch_app, [{description, "Channel allocator"}, {vsn, "1"}, {modules, [ch_app, ch_sup, ch3]}, {registered, [ch3]}, {applications, [kernel, stdlib, sasl]}, {mod, {ch_app,[]}}, {env, [{file, "/usr/local/log"}]} ]}.
其中 Par 必须是一个 atom,Val 能够是任意类型。能够调用 application:get_env(App, Par) 来获取配置参数,还有一组相似函数,详见 Kernel 模块的 application 手册。
例:
% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"/usr/local/log"}
.app 文件中的配置值会被系统配置文件中的配置覆盖。配置文件包含了相关应用的配置参数:
[{Application1, [{Par11,Val11},...]}, ..., {ApplicationN, [{ParN1,ValN1},...]}].
系统配置文件名为 Name.config,erlang 启动时可经过命令行参数 -config Name 来指定配置文件。详见 Kernel 模块的 config 文档。
例:
文件 test.config 内容以下:
[{ch_app, [{file, "testlog"}]}].
file 的值会覆盖 .app 文件中 file 对应的值:
% erl -config test Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"testlog"}
若是使用 release handling ,只能使用一个系统配置文件:sys.config 。
.app 文件和系统配置文件中的值都会被命令行中指定的值覆盖:
% erl -ApplName Par1 Val1 ... ParN ValN
例:
% erl -ch_app file '"testlog"' Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"testlog"}
启动类型在应用启动时指定:
application:start(Application, Type)
application:start(Application) 至关于 application:start(Application, temporary) 。Type 还能够是 permanent 和 transient:
经过调用 application:stop/1 能够显式地终止一个应用,无论启动类型是什么,其余应用都不会被影响。
transient 模式基本没什么用,由于当监控树退出,终止理由会是 shutdown 而不是 normal 。
应用能够 include(译做包含) 其余应用。被包含的应用(included application)有本身的应用目录和 .app 文件,不过它是另外一个应用的监控树的一部分。
应用不能被多个应用包含。
被包含的应用能够包含其余应用。
没有被任何应用包含的应用被称为原初应用(primary application)。
图8.1 原初应用和被包含的应用
应用控制器会在加载原初应用时,自动加载被包含的应用,可是不会启动它们。被包含的应用顶层 supervisor 必须由包含它的应用的 supervisor 启动。
也就是说运行时,被包含的应用其实是原初应用的一部分,被包含应用中的进程会认为本身归属于原初应用。
要包含哪些应用,是在 .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"}]} ]}.
被包含应用的监控树,是包含它的应用的监控树的一部分。若是须要在两个应用间作同步,能够经过 start phase 来实现。
Start phase 是由 .app 文件中的 start_phases 字段指定的,它是一个 {Phase,PhaseArgs} 列表,其中 Phase 是一个 atom,PhaseArgs 能够是任何类型。
包含其余应用时,mod 字段必须为 {application_starter,[Module,StartArgs]}。其中 Module 是应用回调模块,StartArgs 是传递给 Module:start/2 的参数:
{application, prim_app, [{description, "Tree application"}, {vsn, "1"}, {modules, [prim_app_cb, prim_app_sup, prim_app_server]}, {registered, [prim_app_server]}, {included_applications, [incl_app]}, {start_phases, [{init,[]}, {go,[]}]}, {applications, [kernel, stdlib, sasl]}, {mod, {application_starter,[prim_app_cb,[]]}}, {env, [{file, "/usr/local/log"}]} ]}. {application, incl_app, [{description, "Included application"}, {vsn, "1"}, {modules, [incl_app_cb, incl_app_sup, incl_app_server]}, {registered, []}, {start_phases, [{go,[]}]}, {applications, [kernel, stdlib, sasl]}, {mod, {incl_app_cb,[]}} ]}.
启动包含了其余应用的原初应用,跟正常启动应用是同样的,也就是说:
而后,原初应用和被包含应用按照从上到下从左到右的顺序,master 依次为它们 start phase 。对每一个应用,master 按照原初应用中指定的 phase 顺序依次调用 Module:start_phase(Phase, Type, PhaseArgs) ,其中当前应用的 start_phases 中未指定的 phase 会被忽略。
被包含应用的 .app 文件须要以下内容:
启动上文定义的 prim_app 时,在 application:start(prim_app) 返回以前,应用控制器会调用下面的回调:
application:start(prim_app) => prim_app_cb:start(normal, []) => prim_app_cb:start_phase(init, normal, []) => prim_app_cb:start_phase(go, normal, []) => incl_app_cb:start_phase(go, normal, []) ok
在拥有多个节点的分布式系统中,有必要以分布式的方式来管理应用。若是某应用所在的节点崩溃,则在另外一个节点重启这个应用。
这样的应用被称为分布式应用。注意,分布式指的是应用的“管理”。若是从跨节点使用服务的角度来讲,全部的应用都能分布式。
分布式的应用能够在节点间迁移,因此须要寻址机制来确保无论它在哪一个节点都能被其余应用访问到。这个问题不在此讨论,可经过 Kernel 应用的 global 和 pg2 模块的某些功能来实现。
分布式的应用受两个东西控制,应用控制器(application_controller)和分布式应用控制进程(dist_ac)。这两个都是 Kernel 应用的一部分。因此分布式应用是经过配置 Kernel 应用来指定的,可使用下面的配置参数(详见 kernel 文档):
distributed = [{Application, [Timeout,] NodeDesc}]
为了正确地管理分布式应用,可运行应用的节点必须互相链接,协商应用在哪里启动。可在 Kernel 中使用下面的配置参数:
节点启动时会等待全部 sync_nodes_mandatory 和 sync_nodes_optional 中的节点启动。若是全部节点都启动了,或必须启动的节点启动了,sync_nodes_timeout 时长后全部的应用会被启动。若是有必须的节点没启动,当前节点会终止。
应用 myapp 在 cp1@cave 中运行。若是此节点终止,myapp 将在 cp2@cave 或 cp3@cave 节点上重启。cp1@cave 的系统配置 cp1.config 以下:
[{kernel, [{distributed, [{myapp, 5000, [cp1@cave, {cp2@cave, cp3@cave}]}]}, {sync_nodes_mandatory, [cp2@cave, cp3@cave]}, {sync_nodes_timeout, 5000} ] } ].
cp2@cave 和 cp3@cave 的系统配置也是同样的,除了必须启动的节点分别是 [cp1@cave, cp3@cave] 和 [cp1@cave, cp2@cave] 。
注意:全部节点的 distributed 和 sync_nodes_timeout 值必须一致,不然该系统行为不会被定义。
当全部涉及(必须启动)的节点被启动,在全部这些节点中调用 application:start(Application) 就能启动这个分布式应用。
能够用引导脚本(Releases)来自动启动应用。
应用将在参数 distributed 配置的节点列表中的第一个可用节点启动。和日常启动应用同样,建立了一个 application master,调用回调:
Module:start(normal, StartArgs)
例:
继续上一小节的例子,启动了三个节点,指定系统配置文件:
> erl -sname cp1 -config cp1 > erl -sname cp2 -config cp2 > erl -sname cp3 -config cp3
全部节点可用时,myapp 会被启动。全部节点中调用 application:start(myapp) 便可。此时它会在 cp1 中启动,以下图所示:
图9.1:应用 myapp - 状况 1
一样地,在全部的节点中调用 application:stop(Application) 将终止应用。
若是应用所在的节点终止,指定的超时时长后,应用将在 distributed 配置中指定的第一个可用节点中重启。这就是故障切换。
应用在新节点中和日常同样启动,application master 调用:
Module:start(normal, StartArgs)
有一个例外,若是应用指定了 start_phases(详见Included Applications),应用将这样重启:
Module:start({failover, Node}, StartArgs)
其中 Node 为终止的节点。
若是 cp1 终止,系统会等待 cp1 重启5秒,超时后在 cp2 和 cp3 中选择一个运行的应用最少的。若是 cp1 没有重启,且 cp2 运行的应用比 cp3 少,myapp 将会 cp2 节点重启。
图9.2:应用 myapp - 状况 2
假设 cp2 也崩溃了,而且5秒内没有重启。myapp 将在 cp3 重启。
图9.3:应用 myapp - 状况 3
若是一个在 distributed 配置中优先级较高的节点启动,应用会在新节点重启,在旧节点结束。这就是接管。
应用会经过以下方式启动:
Module:start({takeover, Node}, StartArgs)
其中 Node 表示旧节点。
若是 myapp 在 cp3 节点运行,此时 cp2 启动,应用不会被重启,由于 cp2 和 cp3 是没有前后顺序的。
图9.4:应用 myapp - 状况 4
但若是 cp1 也重启了,函数 application:takeover/2 会将 myapp 移动到 cp1,由于对 myapp 来讲 cp1 比 cp3 优先级高。此时节点 cp1 会调用 Module:start({takeover, cp3@cave}, StartArgs) 来启动应用。
图9.5:应用 myapp - 状况 5
此章应与 SASL 部分的 rel、systemtools、script 教程一块儿阅读。
当你写了一个或多个应用,你可能想用这些应用加 Erlang/OTP 应用的子集建立一个完整的系统。这就是 release 。
首先要建立一个 release 源文件,文件中指定了 release 所包含的应用。
此文件用于生成启动脚本和 release 包。可移动和安装到另外一个地址的系统被称为目标系统。系统原则(System Principles)中讲了如何用 release 包建立目标系统。
建立 release 源文件来描述一个 release,简称 .rel 文件。文件中指定了 release 的名字和版本号,它基于哪一个版本的 ERTS,以及它由哪些应用组成:
{release, {Name,Vsn}, {erts, EVsn},
[{Application1, AppVsn1},
...
{ApplicationN, AppVsnN}]}.
Name、Vsn、EVsn 和 AppVsn 都是字符串(string)。
文件名必须为 Rel.rel ,其中 Rel 是惟一的名字。
Application (atom) 和 AppVsn 是 release 中各应用的名字和版本号。基于 Erlang/OTP 的最小的 release 由 Kernel 和 STDLIB 应用组成,这两个应用必定要在应用列表中。
要升级 release 的话,还必须包含 SASL 应用。
例:Applications 章中的 ch_app 的 release 中有下面的 .app 文件:
{application, ch_app, [{description, "Channel allocator"}, {vsn, "1"}, {modules, [ch_app, ch_sup, ch3]}, {registered, [ch3]}, {applications, [kernel, stdlib, sasl]}, {mod, {ch_app,[]}} ]}.
.rel 文件必须包含 kernel、stdlib 和 sasl,由于 ch_app 要用到这些应用。文件名 ch_rel-1.rel :
{release, {"ch_rel", "A"}, {erts, "5.3"}, [{kernel, "2.9"}, {stdlib, "1.12"}, {sasl, "1.10"}, {ch_app, "1"}] }.
SASL 应用的 systools 模块包含了构建和检查 release 的工具。这些函数读取 .rel 和 .app 文件,执行语法和依赖检测。用 systools:make_script/1,2 来生成启动脚本(详见 System Principles):
1> systools:make_script("ch_rel-1", [local]).
ok
这个会建立启动脚本,可读版本 ch_rel-1.script 和运行时系统用到的二进制版本 ch_rel-1.boot。
这在本地测试生成启动脚本时有用处。
使用启动脚原本启动 Erlang/OTP 时,会自动加载和启动 .rel 文件中全部的应用:
% erl -boot ch_rel-1 Erlang (BEAM) emulator version 5.3 Eshell V5.3 (abort with ^G) 1> =PROGRESS REPORT==== 13-Jun-2003::12:01:15 === supervisor: {local,sasl_safe_sup} started: [{pid,<0.33.0>}, {name,alarm_handler}, {mfa,{alarm_handler,start_link,[]}}, {restart_type,permanent}, {shutdown,2000}, {child_type,worker}] ... =PROGRESS REPORT==== 13-Jun-2003::12:01:15 === application: sasl started_at: nonode@nohost ... =PROGRESS REPORT==== 13-Jun-2003::12:01:15 === application: ch_app started_at: nonode@nohost
systools:make_tar/1,2 函数以 .rel 文件做为输入,输出一个 zip 压缩的 tar 文件,文件中包含指定应用的代码,即 release 包:
1> systools:make_script("ch_rel-1"). ok 2> systools:make_tar("ch_rel-1"). ok
一个 release 包默认包含:
% tar tf ch_rel-1.tar lib/kernel-2.9/ebin/kernel.app lib/kernel-2.9/ebin/application.beam ... lib/stdlib-1.12/ebin/stdlib.app lib/stdlib-1.12/ebin/beam_lib.beam ... lib/sasl-1.10/ebin/sasl.app lib/sasl-1.10/ebin/sasl.beam ... lib/ch_app-1/ebin/ch_app.app lib/ch_app-1/ebin/ch_app.beam lib/ch_app-1/ebin/ch_sup.beam lib/ch_app-1/ebin/ch3.beam releases/A/start.boot releases/A/ch_rel-1.rel releases/ch_rel-1.rel
Release 包生成前,生成了一个新的启动脚本(不使用 local 选项)。在 release 包中,全部的应用目录都放在 lib 目录下。因为不知道 release 包会发布到哪里,因此不能写死绝对路径。
在 tar 文件中有两个同样的 rel 文件。最初这个文件只放在 releases 目录下,这样 release_handler 就能单独提取这个文件。解压 tar 文件后,release_handler 会自动把它拷贝到 releases/FIRST 目录。可是有时 tar 文件解包时没有 release_handler 参与(好比解压第一个目标系统),因此改成在 tar 文件中有两份,不须要再手动拷贝。
包里面还可能有 relup 文件和系统配置文件 sys.config,这些文件也会在 release 包中包含。详见 Release Handling 。
release_handler 从 release 包安装的代码目录结构以下:
$ROOT/lib/App1-AVsn1/ebin /priv /App2-AVsn2/ebin /priv ... /AppN-AVsnN/ebin /priv /erts-EVsn/bin /releases/Vsn /bin
应用不必定要放在 $ROOT/lib 目录。所以能够有多个安装目录,包含系统的不一样部分。例如,上面的例子能够拓展成:
$SECOND_ROOT/.../SApp1-SAVsn1/ebin /priv /SApp2-SAVsn2/ebin /priv ... /SAppN-SAVsnN/ebin /priv $THIRD_ROOT/TApp1-TAVsn1/ebin /priv /TApp2-TAVsn2/ebin /priv ... /TAppN-TAVsnN/ebin /priv
$SECOND_ROOT 和 $THIRD_ROOT 在调用 systools:make_script/2 函数时做为参数传入。
若是系统由无磁盘的或只读的客户端节点组成,$ROOT 目录中还会有一个 clients 目录。只读的节点就是节点在一个只读文件系统中。
每一个客户端节点在 clients 中有一个子目录。每一个子目录的名字是对应的节点名。一个客户端目录至少包含 bin 和 releases 两个子目录。这些目录用来存放 release 的信息,以及把当前 release 指派给客户端。$ROOT 目录以下所示:
$ROOT/... /clients/ClientName1/bin /releases/Vsn /ClientName2/bin /releases/Vsn ... /ClientNameN/bin /releases/Vsn
这个结构用于全部客户端都运行在同类型的 Erlang 虚拟机上。若是有不一样类型的 Erlang 虚拟机,或者在不一样的操做系统中,能够把 clients 分红每一个类型一个子目录。或者每一个类型设置一个 $ROOT。此时 $ROOT 目录相关的一些子目录都须要包含进来:
$ROOT/... /clients/Type1/lib /erts-EVsn /bin /ClientName1/bin /releases/Vsn /ClientName2/bin /releases/Vsn ... /ClientNameN/bin /releases/Vsn ... /TypeN/lib /erts-EVsn /bin ...
这个结构中,Type1 的客户端的根目录为 $ROOT/clients/Type1 。
Erlang 的一个重要特色就是能够在运行时改变模块代码,即 Erlang Reference Manual(参考手册)中说的代码替换。
基于这个特色,OTP 应用 SASL 提供在运行时升级和降级整个 release 的框架。这就是 release 管理。
这个框架包含:
包含 release 管理的基于 Erlang/OTP 的最小的系统,由 Kernel、STDLIB 和 SASL 应用组成。
步骤 1:按 Releases 章所述建立一个 release。
步骤 2:在目标环境中安装 release 。如何安装第一个目标系统,详见 System Principles 文档。
步骤 3:在开发环境中修改代码(好比错误修复)。
步骤 4:某个时间点,须要建立新版本 release 。更新相关的 .app 文件,建立 .rel 文件。
步骤 5:为每一个修改的应用,建立 .appup 文件(应用升级文件)。该文件描述了怎么在应用的新旧版本间升降级。
步骤 6:基于 .appup 文件,建立 relup 文件 (release 升级文件)。该文件描述了怎么在整个 release 的新旧版本间升降级。
步骤 7:建立一个新的 release 包,放到目标系统上。
步骤 8:使用 release handler 解包。
步骤 9:使用 release handler 安装新版 release 包。执行 relup 文件中的指令:添加、删除或从新加载模块,启动、终止或重启应用,等等。有时须要重启整个模拟器。
Appup Cookbook 章中有 .appup 文件的示例,包含了典型的运行时系统能够轻松处理的案例。然而有些状况下 release 管理会很复杂,例如:
因此建议代码作尽量小的改动,永远保持向后兼容。
为了正确地执行 release 管理,运行时系统必须知道当前运行哪一个 release 。必须能在运行时,改变重启时要用哪一个启动脚本和系统配置文件,使其崩溃时还能生效。因此,Erlang 必须以嵌入式系统方式启动,详见 Embedded System 文档。
为了系统重启顺利,系统启动时必须启动心跳监测,详见 ERTS 部分的 erl 手册和 Kernel 部分的 heart(3) 手册。
其余必要条件:
若是系统由多个节点组成,每一个节点能够拥有本身的 release 。release_handler 是一个本地注册的进程,升降级时只能在节点中调用。Release 管理指令 sync_nodes 能够用来同步多个节点的 release 管理进程,详见 SASL 的 appup(4) 手册。
OTP 支持一系列 Release 管理指令,在建立 appup 文件时会用到。release_handler 能理解其中一部分,低级指令。还有一些高级指令,是为了用户方便而设计的,调用 systools:make_relup 时会被转化成低级指令。
此节描述了最经常使用的指令。完整的指令列表可见 SASL 的 appup(4) 手册。
首先,给出一些定义:
对一个 OTP behaviour 实现的进程来讲,behaviour 模块就是它的驻地模块,回调模块就是功能模块。
若是模块作了简单的扩展,加载模块的新版本并移除旧版本就好了。这就是简单的代码替换,使用以下指令便可:
{load_module, Module}
若是有复杂的修改,好比改了 gen_server 的内部状态格式,简单的代码替换就不够了。必须作到:
这个就是同步代码替换,使用以下指令:
{update, Module, {advanced, Extra}}
{update, Module, supervisor}
当要改变上述 behaviour 的内部状态时,使用 {advanced,Extra} 。它会致使进程调用回调函数 code_change,传递 Extra 和一些其余信息做为参数。详见对应 behaviour 和 Appup Cookbook 。
改变监程的启动规格时使用 supervisor 参数。详见 Appup Cookbook 。
当模块更新时,release_handler 会遍历各应用的监控树,检查全部的子进程规格,找到用到该模块的进程:
{Id, StartFunc, Restart, Shutdown, Type, Modules}
进程用到了某模块,意思就是该模块在子进程规格的 Modules 列表中。
若是 Modules=dynamic,如事件管理器,则事件管理器会通知 release_handler 当前安装的事件处理器列表(gen_event),它会检测这个列表的模块名。
release_handler 经过 sys:suspend/1,2 、sys:change_code/4,5 和 sys:resume/1,2 来挂起、要求切换代码以及恢复进程。
使用下列指令引入新模块:
{add_module, Module}
这条指令加载了新模块,在嵌入模式运行 Erlang 时必须使用它。交互模式下能够不使用这条指令,由于代码服务器会自动搜寻和加载未加载的模块。
delete_module 与 add_module 相反,它能卸载模块:
{delete_module, Module}
当这条指令执行时,以 Module 为驻地模块的全部进程都会被杀死。用户必须保证在卸载模块前,全部涉及进程都终止,以免无谓的 supervisor 重启。
添加应用:
{add_application, Application}
添加一个应用,会先用 add_module 指令加载全部 .app 文件中 modules 字段所列模块,而后启动应用。
移除应用:
{remove_application, Application}
移除应用会终止应用,而且使用 delete_module 指令卸载模块,最后会从应用控制器卸载应用的规格信息。
重启应用:
{restart_application, Application}
重启应用会先终止应用再启动应用,至关于连续使用 remove_application 和 add_application 。
让 release_handler 调用任意函数:
{apply, {M, F, A}}
release_handler 会执行 apply(M, F, A) 。
这条指令用于改变模拟器版本,或者升级核心应用 Kernel、STDLIB 或 SASL 。若是由于某种缘由须要系统重启,则应该使用 restart_emulator 指令。
这条指令要求系统启动时必须启动心跳监测,详见 ERTS 部分的 erl 手册和 Kernel 部分的 heart(3) 手册。
restart_new_emulator 必须是 relup 文件的第一条指令,若是使用 systools:make_relup/3,4 生成 relup 文件,会默认放在最前面。
当 release_handler 执行这条命令,它会先生成一个临时的启动文件,文件指定新版本的模拟器和核心应用以及旧版本的其余应用。而后它调用 init:reboot()(详见 Kernel 的 init(3) 手册)关闭当前模拟器。全部进程优雅地终止,而后 heart 程序使用临时启动文件重启系统。重启后,会执行其余的 relup 指令,这个过程定义在临时启动文件中。
警告:这个机制会在启动时使用新版本的模拟器和核心应用,可是其余应用还是旧版本。因此要额外注意兼容问题。有时核心应用中会作不兼容的修改。若是可能,新旧代码先共存于一个 release,线上更新完成后再在此后的新 release 弃用旧代码。为了保证应用不会由于不兼容的修改而崩溃,应尽量早地中止调用弃用函数。
升级完成会写一条 info 报告。能够经过调用 release_handler:which_releases(current) ,检查它是否返回预期的新的 release 。
当新模拟器可操做时,必须持久化新的 release 版本。不然系统重启时仍会使用旧版。
在 UNIX 系统中,release_handler 会告诉 heart 程序使用哪条命令来重启系统。此时 heart 程序使用的环境变量 HEART_COMMAND 会被忽略,默认命令为 $ROOT/bin/start 。也能够经过使用 SASL 的配置参数 start_prg 来指定其余命令,详见 sasl(6) 手册。
这条命令不用于 ERTS 或核心应用的升级。在全部升级指令执行完后,能够用它来强制重启模拟器。
relup 文件只能有一个 restart_emulator 指令,且必须放在最后。若是使用 systools:make_relup/3,4 生成 relup 文件,会默认放在最后。
当 release_handler 执行这条命令,它会调用 init:reboot()(详见 Kernel 的 init(3) 手册)关闭当前模拟器。全部进程优雅地终止,而后 heart 程序使用新版 release 来重启系统。重启后不会执行其余升级指令。
建立应用升级文件来指定如何在当前版本和旧版本应用之间升降级,简称 .appup 文件。文件名为 Application.appup ,其中 Application 是应用名:
{Vsn,
[{UpFromVsn1, InstructionsU1},
...,
{UpFromVsnK, InstructionsUK}],
[{DownToVsn1, InstructionsD1},
...,
{DownToVsnK, InstructionsDK}]}.
.appup 文件的语法和内容,详见 SASL 的 appup(4) 手册。
Appup Cookbook 中有典型案例的 .appup 文件示例。
例:Releases 章中的例子。若是想在 ch3 中添加函数 available/0 ,返回可用 channel 的数量(修改的时候,在原目录的副本里改,这样初版仍然可用):
-module(ch3). -behaviour(gen_server). -export([start_link/0]). -export([alloc/0, free/1]). -export([available/0]). -export([init/1, handle_call/3, handle_cast/2]). start_link() -> gen_server:start_link({local, ch3}, ch3, [], []). alloc() -> gen_server:call(ch3, alloc). free(Ch) -> gen_server:cast(ch3, {free, Ch}). available() -> gen_server:call(ch3, available). init(_Args) -> {ok, channels()}. handle_call(alloc, _From, Chs) -> {Ch, Chs2} = alloc(Chs), {reply, Ch, Chs2}; handle_call(available, _From, Chs) -> N = available(Chs), {reply, N, Chs}. handle_cast({free, Ch}, Chs) -> Chs2 = free(Ch, Chs), {noreply, Chs2}.
建立新版 ch_app.app 文件,修改版本号:
{application, ch_app, [{description, "Channel allocator"}, {vsn, "2"}, {modules, [ch_app, ch_sup, ch3]}, {registered, [ch3]}, {applications, [kernel, stdlib, sasl]}, {mod, {ch_app,[]}} ]}.
要让 ch_app 从版本 "1" 升到 "2" 或从 "2" 降到 "1",只须要加载对应版本的 ch3 回调便可。在 ebin 目录建立 ch_app.appup 应用升级文件:
{"2", [{"1", [{load_module, ch3}]}], [{"1", [{load_module, ch3}]}] }.
要指定如何在 release 的版本间切换,要建立一个 release 升级文件,简称 relup 文件。
可使用 systools:make_relup/3,4 自动生成此文件,将相关版本的 .rel 文件、.app 文件和 .appup 文件做为输入。它不包含要增删哪些应用,哪些应用要升降级。这些指令会从 .appup 文件中获取,按正确的顺序转化成低级指令列表。
若是 relup 文件很简单,能够手动建立它。它只包含低级指令。
relup 文件的语法和内容详见 SASL 的 relup(4) 手册。
继续前小节的例子:已经有新版 "2" 的 ch_app 应用以及 .appup 文件。还需新版的 .rel 文件。文件名 ch_rel-2.rel ,release 版本从 "A" 变为 "B":
{release, {"ch_rel", "B"}, {erts, "5.3"}, [{kernel, "2.9"}, {stdlib, "1.12"}, {sasl, "1.10"}, {ch_app, "2"}] }.
生成 relup 文件:
1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"]).
ok
生成了一个 relup 文件,文件中有从版本 "A" ("ch_rel-1") 升级到版本 "B" ("ch_rel-2") 和从 "B" 降到 "A" 的指令。
新版和旧版的 .app 和 .rel 文件、.appup 文件和新的 .beam 文件都必须在代码路径中。代码路径可使用选项 path 来扩展:
1> systools:make_relup("ch_rel-2", ["ch_rel-1"], ["ch_rel-1"], [{path,["../ch_rel-1", "../ch_rel-1/lib/ch_app-1/ebin"]}]). ok
有了一个新版的 release,就能够建立 release 包并放到目标环境中去。
在运行时系统中安装新版 release 会用到 release handler 。它是 SASL 应用的一个进程,用于处理 release 包的解包、安装和移除。它经过 release_handler 模块通信。详见 SASL 的 release_handler(3) 手册。
假设有一个可操做的目标系统,安装根目录为 $ROOT,新版 release 包应拷贝到 $ROOT/releases 目录下。首先,解包。从包中提取文件:
release_handler:unpack_release(ReleaseName) => {ok, Vsn}
release_handler:install_release(Vsn) => {ok, FromVsn, []}
若是安装过程当中有错误发生,系统会使用旧版 release 重启。若是安装成功,后续系统会用新版本,不过若是系统中途有重启的话,仍是会使用旧版本。
必须把新安装的 release 持久化才能让它成为默认版本,让以前的版本变成旧版本:
release_handler:make_permanent(Vsn) => ok
系统在 $ROOT/releases/RELEASES 和 $ROOT/releases/start_erl.data 中保存版本信息。
从 Vsn 降级到 FromVsn 时,须再次调用 install_release:
release_handler:install_release(FromVsn) => {ok, Vsn, []}
安装了的可是还没持久化的 release 能够被移除。移除意味着 release 的信息会被从 $ROOT/releases/RELEASES 中移除。代码也会被移除,也就是说,新的应用目录和 $ROOT/releases/Vsn 目录都会被删掉。
release_handler:remove_release(Vsn) => ok
步骤 1)建立 Releases 中的版本 "A" 的目标系统。这回 sys.config 必须包含在 release 包中。若是不须要任何配置,这个文件中为一个空列表:
[].
步骤 2)启动系统。现实中会以嵌入式系统启动。不过,使用 erl 和正确的启动脚本和配置就足以用来举例说明:
% cd $ROOT % bin/erl -boot $ROOT/releases/A/start -config $ROOT/releases/A/sys ...
步骤 3)在另外一个 Erlang shell,生成启动脚本,并建立版本 "B" 的 release 包。记得包含 sys.config(可能有变化)和 relup 文件,详见 Release 升级文件。
1> systools:make_script("ch_rel-2"). ok 2> systools:make_tar("ch_rel-2"). ok
新的 release 包如今包含版本 "2" 的 ch_app 和 relup 文件:
% tar tf ch_rel-2.tar lib/kernel-2.9/ebin/kernel.app lib/kernel-2.9/ebin/application.beam ... lib/stdlib-1.12/ebin/stdlib.app lib/stdlib-1.12/ebin/beam_lib.beam ... lib/sasl-1.10/ebin/sasl.app lib/sasl-1.10/ebin/sasl.beam ... lib/ch_app-2/ebin/ch_app.app lib/ch_app-2/ebin/ch_app.beam lib/ch_app-2/ebin/ch_sup.beam lib/ch_app-2/ebin/ch3.beam releases/B/start.boot releases/B/relup releases/B/sys.config releases/B/ch_rel-2.rel releases/ch_rel-2.rel
步骤 4)拷贝 release 包 ch_rel-2.tar.gz 到 $ROOT/releases 目录。
步骤 5)在运行的目标系统中,解包:
1> release_handler:unpack_release("ch_rel-2").
{ok,"B"}
新版本应用 ch_app-2 被安装在 $ROOT/lib 目录,在 ch_app-1 附近。kernel、stdlib 和 sasl 目录不受影响,由于它们没有改变。
$ROOT/releases 下建立了一个新目录 B,其中包含了 ch_rel-2.rel、start.boot、sys.config 和 relup 。
步骤 6)检查 ch3:available/0 是否可用:
2> ch3:available().
** exception error: undefined function ch3:available/0
步骤 7)安装新 release 。$ROOT/releases/B/relup 中的指令会一一被执行,新版 ch3 被加载进来。函数 ch3:available/0 如今可用了:
3> release_handler:install_release("B"). {ok,"A",[]} 4> ch3:available(). 3 5> code:which(ch3). ".../lib/ch_app-2/ebin/ch3.beam" 6> code:which(ch_sup). ".../lib/ch_app-1/ebin/ch_sup.beam"
ch_app 中的进程代码不变,例如,supervisor 还在执行 ch_app-1 的代码。
步骤 8)若是目标系统如今重启,它会从新使用 "A" 版本。要在重启时使用 "B" 版本,必须持久化:
7> release_handler:make_permanent("B").
ok
当新版 release 安装,全部加载的应用规格会自动更新。
注意:新的应用规格从 release 包中的启动脚本中获取。因此启动脚本必须和 release 包从同一个 .rel 文件中生成。
确切地说,应用配置参数会根据下面的内容自动更新(优先级递增):
也就是说被其余系统配置文件设置的值,以及使用 application:set_env/3 设置的值都会被无视。
当安装好的 release 被设置为永久时,系统进程 init 会指向新的 sys.config 文件。
安装后,应用控制器会比较全部运行中的应用的新旧配置参数,并调用回调:
Module:config_change(Changed, New, Removed)
这个函数是可选的,在实现应用回调模块时能够省略。
此章包含典型案例的升降级 .appup 文件的例子。
若是功能模块被修改,例如新加了一个函数或修复了一个 bug,简单的代码替换就够了:
{"2", [{"1", [{load_module, m}]}], [{"1", [{load_module, m}]}] }.
若是系统彻底根据 OTP 设计原则来实现,除系统进程和特殊进程外的全部进程,都会驻扎在某个 behavior 中,supervisor、gen_server、gen_fsm、gen_statem 或 gen_event 。这些都属于 STDLIB 应用,升降级通常来讲须要模拟器重启。
所以 OTP 没有支持修改驻地模块,除了一些特殊进程。
回调模块属于功能模块,代码扩展只须要简单的代码替换就行。
例:前文 Relase Handling 中的例子,在 ch3 中添加一个函数,ch_app.appup 内容以下:
{"2", [{"1", [{load_module, ch3}]}], [{"1", [{load_module, ch3}]}] }.
OPT 还支持修改 behaviour 进程的内部状态,详见下一小节。
这种状况下,简单的代码替换不能解决问题。在切换到新版回调模块前,进程必须使用 code_change 回调显示地修改它的状态。此时要用到同步代码替换(译者补充:同步即须要等待进程做出一些反应)。
例:前文 gen_server Behaviour 中的 gen_server ch3,内部状态为 Chs,表示可用的 channel 。假设你想增长一个计数器 N,记录 alloc 请求次数。状态的格式必须变为 {Chs,N} 。
.appup 文件内容以下:
{"2", [{"1", [{update, ch3, {advanced, []}}]}], [{"1", [{update, ch3, {advanced, []}}]}] }.
update 指令的第三个参数是一个元组 {advanced,Extra} ,意思是在加载新版模块以前,受影响的进程要先修改状态。修改状态是经过让进程调用 code_change 回调来完成的(详见 STDLIB 的 gen_server(3) 手册)。Extra(此例中是 [])会被传递到 code_change 函数:
-module(ch3). ... -export([code_change/3]). ... code_change({down, _Vsn}, {Chs, N}, _Extra) -> {ok, Chs}; code_change(_Vsn, Chs, _Extra) -> {ok, {Chs, 0}}.
code_change 第一个参数,若是降级则为 {down,Vsn},升级则为 Vsn 。Vsn 是模块的“原”版,即升级前的版本。
若是模块有 vsn 属性的话,版本即该属性的值。ch3 没有这个属性,因此此时版本号为 beam 文件的校验和(大整数),此处它不重要被忽略。
ch3 的其余回调也要修改,还要加其余接口函数,不过此处不赘述。
在模块中增长了一个接口函数,好比前文 Release Handling 中的,在 ch3 中增长 available/0 。
假设在另外一个模块 m1 会调用此函数。在 release 升级过程当中,若是先加载新版 m1,在 ch3 加载前 m1 调用 ch3:available/0 会引起一个 runtime error 。
因此升级时 ch3 必须在 m1 以前加载,降级时则相反。即 m1 依赖于 ch3 。在 release 处理指令中,用 DepMods 元素来表示:
{load_module, Module, DepMods}
{update, Module, {advanced, Extra}, DepMods}
DepMods 是模块列表,表示 Module 所依赖的模块。
例:myapp 应用的模块 m1 依赖于 ch_app 应用的模块 ch3,从 "1" 升级到 "2",或从 "2" 降级到 "1" 时:
myapp.appup: {"2", [{"1", [{load_module, m1, [ch3]}]}], [{"1", [{load_module, m1, [ch3]}]}] }. ch_app.appup: {"2", [{"1", [{load_module, ch3}]}], [{"1", [{load_module, ch3}]}] }.
若是 m1 和 ch_app 属于同一个应用,.appup 文件以下:
{"2", [{"1", [{load_module, ch3}, {load_module, m1, [ch3]}]}], [{"1", [{load_module, ch3}, {load_module, m1, [ch3]}]}] }.
降级时也是 m1 依赖于 ch3 。systools 能区分升降级,生成正确的 relup 文件,升级时先加载 ch3 再 m1,降级时先加载 m1 再 ch3 。
这种状况下,简单的代码替换不能解决问题。加载特殊进程的新版驻地模块时,进程必须调用它的 loop 函数的全名,来切换至新代码。此时,必须用同步代码替换。
注意:用户自定义的驻地模块,必须在特殊进程的子进程规格的 Modules 列表中。不然 release_handler 会找不到该进程。
例:前文 sys and proc_lib 中的例子。经过 supervisor 启动时,子进程规格以下:
{ch4, {ch4, start_link, []},
permanent, brutal_kill, worker, [ch4]}
若是 ch4 是应用 sp_app 的一部分,从版本 "1" 升级到版本 "2" 时,要加载该模块的新版本,sp_app.appup 内容以下:
{"2", [{"1", [{update, ch4, {advanced, []}}]}], [{"1", [{update, ch4, {advanced, []}}]}] }.
update 指令必须包含元组 {advanced,Extra} 。这条指令让特殊进程调用回调 system_code_change/4,这个回调必需要实现。Extra(此例中为 [] ),会被传递给 system_code_change/4 :
-module(ch4). ... -export([system_code_change/4]). ... system_code_change(Chs, _Module, _OldVsn, _Extra) -> {ok, Chs}.
此例中,只用到了第一个参数,函数仅返回内部状态。若是代码只是被扩展,这样就 ok 了。若是内部状态改变(相似 12.4 小节),要在这个函数中进行改变,并返回 {ok,Chs2} 。
supervisor behaviour 支持修改内部状态,也就是修改重启策略、最大重启频率以及子进程规格。
能够添加或删除子进程,不过不是自动处理的。必须在 .appup 中指定。
因为 supervisor 内部状态有改动,必须使用同步代码替换。须要一个特殊的 update 指令。
首先,加载新版回调模块(升或降)。而后检测 init/1 的新的返回值,并据此修改内部状态。
supervisor 的升级指令以下:
{update, Module, supervisor}
例:把 ch_sup 的重启策略,从 one_for_one 变为 one_for_all,要改 ch_sup.erl 中的回调函数 init/1 :
-module(ch_sup). ... init(_Args) -> {ok, {#{strategy => one_for_all, ...}, ...}}.
文件 ch_app.appup :
{"2", [{"1", [{update, ch_sup, supervisor}]}], [{"1", [{update, ch_sup, supervisor}]}] }.
修改已存在的子进程规格,指令和 .appup 文件与前面的修改属性同样:
{"2", [{"1", [{update, ch_sup, supervisor}]}], [{"1", [{update, ch_sup, supervisor}]}] }.
这些修改不会影响已存在的子进程。例如,修改启动函数,只会影响子进程重启。
子进程规格的 id 不能修改。
修改子进程规格的 Modules 字段,会影响 release_handler 进程自身,由于这个字段用于在同步代码替换中,确认哪些进程收到影响。
如前文所说,修改子进程规格,不影响现有子进程。新的规格会自动添加,可是不会删除废弃规格。子进程不会自动重启或终止,必须使用 apply 指令来操做。
例:假设从 "1" 升到 "2" 时, ch_sup 增长了一个子进程 m1。降级时 m1 会被删除:
{"2", [{"1", [{update, ch_sup, supervisor}, {apply, {supervisor, restart_child, [ch_sup, m1]}} ]}], [{"1", [{apply, {supervisor, terminate_child, [ch_sup, m1]}}, {apply, {supervisor, delete_child, [ch_sup, m1]}}, {update, ch_sup, supervisor} ]}] }.
指令的顺序很重要。
supervisor 必须被注册为 ch_sup 才能让脚本生效。若是没有注册,不能从脚本中直接访问它。此时必须写一个帮助函数来寻找 supervisor 的 pid,并调用 supervisor:restart_child 。而后在脚本中使用 apply 指令调用该帮助函数。
若是模块 m1 在应用 ch_app 的版本 "2" 引入,它必须在升级时加载、降级时删除:
{"2", [{"1", [{add_module, m1}, {update, ch_sup, supervisor}, {apply, {supervisor, restart_child, [ch_sup, m1]}} ]}], [{"1", [{apply, {supervisor, terminate_child, [ch_sup, m1]}}, {apply, {supervisor, delete_child, [ch_sup, m1]}}, {update, ch_sup, supervisor}, {delete_module, m1} ]}] }.
如前文所述,指令的顺序很重要。升级时,必须在启动新进程以前,加载 m一、改变 supervisor 的子进程规格。降级时,子进程必须在规格改变和模块被删除前终止。
例:应用 ch_app 增长了一个新的功能模块:
{"2", [{"1", [{add_module, m}]}], [{"1", [{delete_module, m}]}]
一个根据 OTP 设计原则组织的系统中,全部的进程都是某 supervisor 的子进程,详见增长和删除子进程。
增长或移除应用时,不须要 .appup 文件。生成 relup 文件时,会比较 .rel 文件,并自动添加 add_application 和 remove_application 指令。
当修改太复杂时(如监控树层级重构),能够重启应用。
例:增长和删除子进程中的例子,ch_sup 增长了一个子进程 m1,还能够经过重启整个应用来更新 supervisor :
{"2", [{"1", [{restart_application, ch_app}]}], [{"1", [{restart_application, ch_app}]}] }.
在执行 relup 脚本前,在安装 release 时,应用规格就自动更新了。所以,不须要在 .appup 中增长指令:
{"2", [{"1", []}], [{"1", []}] }.
能够经过修改 .app 文件中的 env 字段,来修改应用配置。
另外,还能够修改 sys.config 文件来修改应用配置参数。
增长、移除、重启应用的 release 处理指令,只适用于原初应用。被包含应用没有相应的指令。可是由于实际上,被包含应用的最上层 supervisor 是包含它的应用的 supervisor 的子进程,咱们能够手动建立 relup 文件。
例:假设一个 release 包含了应用 prim_app,它的监控树中有一个监程 prim_sup 。
在新版本 release 中,应用 ch_app 被包含进了 prim_app ,也就是说它的最上层监程 ch_sup 是 prim_sup 的子进程。
工做流以下:
步骤 1)修改 prim_sup 的代码:
init(...) ->
{ok, {...supervisor flags...,
[...,
{ch_sup, {ch_sup,start_link,[]},
permanent,infinity,supervisor,[ch_sup]},
...]}}.
步骤 2)修改 prim_app 的 .app 文件:
{application, prim_app, [..., {vsn, "2"}, ..., {included_applications, [ch_app]}, ... ]}.
步骤 3)建立新的 .rel 文件,包含 ch_app:
{release, ..., [..., {prim_app, "2"}, {ch_app, "1"}]}.
被包含的应用能够经过两种方式重启。下面会说。
步骤 4a)一种方式,是重启整个 prim_app 应用。在 prim_app 的 .appup 文件中使用 restart_application 指令。
然而,若是这样作,relup 文件不止包含了重启(移除和添加)prim_app 的指令,它还会有启动(以及降级时移除)ch_app 的指令。由于新的 .rel 文件中有 ch_app,而旧的 .rel 文件没有。
因此,应该手动建立正确的 relup 文件,重写或在自动生成的基础上写都行。用加载/卸载 ch_app 的指令,替换启动/中止的指令:
{"B", [{"A", [], [{load_object_code,{ch_app,"1",[ch_sup,ch3]}}, {load_object_code,{prim_app,"2",[prim_app,prim_sup]}}, point_of_no_return, {apply,{application,stop,[prim_app]}}, {remove,{prim_app,brutal_purge,brutal_purge}}, {remove,{prim_sup,brutal_purge,brutal_purge}}, {purge,[prim_app,prim_sup]}, {load,{prim_app,brutal_purge,brutal_purge}}, {load,{prim_sup,brutal_purge,brutal_purge}}, {load,{ch_sup,brutal_purge,brutal_purge}}, {load,{ch3,brutal_purge,brutal_purge}}, {apply,{application,load,[ch_app]}}, {apply,{application,start,[prim_app,permanent]}}]}], [{"A", [], [{load_object_code,{prim_app,"1",[prim_app,prim_sup]}}, point_of_no_return, {apply,{application,stop,[prim_app]}}, {apply,{application,unload,[ch_app]}}, {remove,{ch_sup,brutal_purge,brutal_purge}}, {remove,{ch3,brutal_purge,brutal_purge}}, {purge,[ch_sup,ch3]}, {remove,{prim_app,brutal_purge,brutal_purge}}, {remove,{prim_sup,brutal_purge,brutal_purge}}, {purge,[prim_app,prim_sup]}, {load,{prim_app,brutal_purge,brutal_purge}}, {load,{prim_sup,brutal_purge,brutal_purge}}, {apply,{application,start,[prim_app,permanent]}}]}] }.
步骤 4b)另外一种方式,是结合为 prim_sup 添加或删除子进程的指令,以及加载和卸载 ch_app 代码和应用规格的指令。
这种方式也须要手动建立 relup 文件。重写或在自动生成的基础上写都行。先加载 ch_app 的代码和应用规格,而后再更新 prim_sup 。降级时先更新 prim_sup 再卸载 ch_app 的代码和应用规格。
{"B", [{"A", [], [{load_object_code,{ch_app,"1",[ch_sup,ch3]}}, {load_object_code,{prim_app,"2",[prim_sup]}}, point_of_no_return, {load,{ch_sup,brutal_purge,brutal_purge}}, {load,{ch3,brutal_purge,brutal_purge}}, {apply,{application,load,[ch_app]}}, {suspend,[prim_sup]}, {load,{prim_sup,brutal_purge,brutal_purge}}, {code_change,up,[{prim_sup,[]}]}, {resume,[prim_sup]}, {apply,{supervisor,restart_child,[prim_sup,ch_sup]}}]}], [{"A", [], [{load_object_code,{prim_app,"1",[prim_sup]}}, point_of_no_return, {apply,{supervisor,terminate_child,[prim_sup,ch_sup]}}, {apply,{supervisor,delete_child,[prim_sup,ch_sup]}}, {suspend,[prim_sup]}, {load,{prim_sup,brutal_purge,brutal_purge}}, {code_change,down,[{prim_sup,[]}]}, {resume,[prim_sup]}, {remove,{ch_sup,brutal_purge,brutal_purge}}, {remove,{ch3,brutal_purge,brutal_purge}}, {purge,[ch_sup,ch3]}, {apply,{application,unload,[ch_app]}}]}] }.
修改其余语言写的代码,好比接口程序,是依赖于应用的,OTP 没有提供特别的支持。
例:修改 port 程序,假设控制这个接口的 Erlang 进程是注册为 portc 的 gen_server,经过回调 init/1 来开启接口:
init(...) -> ..., PortPrg = filename:join(code:priv_dir(App), "portc"), Port = open_port({spawn,PortPrg}, [...]), ..., {ok, #state{port=Port, ...}}.
要更新接口程序,gen_server 的代码必须有 code_change 回调,用来关闭接口和开启新接口(若是有须要,还可让 gen_server 先从旧接口请求必要的数据,而后传递给新接口):
code_change(_OldVsn, State, port) -> State#state.port ! close, receive {Port,close} -> true end, PortPrg = filename:join(code:priv_dir(App), "portc"), Port = open_port({spawn,PortPrg}, [...]), {ok, #state{port=Port, ...}}.
更新 .app 文件的版本号,并建立 .appup 文件:
["2", [{"1", [{update, portc, {advanced,port}}]}], [{"1", [{update, portc, {advanced,port}}]}] ].
确保 C 程序所在的 priv 目录被包含在新的 release 包中:
1> systools:make_tar("my_release", [{dirs,[priv]}]).
...
两条重启模拟器的升级指令:
当 ERTS、Kernel、STDLIB 或 SASL 要升级时会用到。用 systools:make_relup/3,4 生成 relup 文件会自动添加这条指令。它会在全部其余指令以前执行。详见前文的 restart_new_emulator(低级指令)。
在全部其余指令执行完后须要重启模拟器时会用到。详见前文的 restart_emulator 。
若是只须要重启模拟器,不须要其余升级指令,能够手动建立一个 relup 文件:
{"B", [{"A", [], [restart_emulator]}], [{"A", [], [restart_emulator]}] }.
此时,release 管理框架会自动打包、解包、更新路径等,且不须要指定 .appup 文件。
从 OTP R15 开始,模拟器升级会在加载代码和运行其余应用升级指令前,使用新版的核心应用(Kernel、STDLIB 和 SASL)重启模拟器来完成模拟器升级。这要求要升级的 release 必须是 OTP R15 或更晚版本。
若是 release 是早期版本,systools:make_relup 会生成一个向后兼容的 relup 文件。全部升级指令在模拟器重启前执行,新的应用代码会被加载到旧模拟器中。若是新代码是用新模拟器编译的,而新模拟器下的 beam 文件的格式有变化,可能致使加载 beam 文件失败。用旧模拟器编译新代码,能够解决这个问题。