非常教程

Erlang 20参考手册

指南:设计原则 | Guide: Design principles

12. Appup Cookbook

本部分包含.appup用于在运行时完成升级/降级典型情况的文件示例。

12.1更改功能模块

例如,如果功能模块已更改,如果添加了新功能或修正了错误,则只需更换简单的代码即可,例如:

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

12.2更换Residence模块

在根据OTP设计原理实现的系统中,所有的过程,除了系统进程和特殊工艺,驻留在行为之一supervisorgen_servergen_fsmgen_statemgen_event行为之一。这些属于STDLIB应用程序,升级/降级通常需要重新启动仿真器。

OTP因此不提供对改变居住模块的支持,除非是special processes

12.3更改回调模块

回调模块是一个功能模块,对于代码扩展,简单的代码替换就足够了。

例子:当添加一个函数时ch3,如例子中所述Release Handlingch_app.appup看起来如下所示:

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

OTP还支持更改行为过程的内部状态,请参阅Changing Internal State

12.4内部状态的变化

在这种情况下,简单的代码替换是不够的。code_change在切换到新版本的回调模块之前,进程必须使用回调函数显式地转换其状态。因此,使用了同步代码替换。

例如:考虑gen_server ch3gen_server Behaviour。内部状态是Chs表示可用频道的术语。假设你想添加一个计数器N,它跟踪alloc到目前为止的请求数量。这意味着格式必须更改为{Chs,N}

.appup文件可以如下所示:

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

update指令的第三个元素是一个元组{advanced,Extra},它表示受影响的进程在加载模块的新版本之前将进行状态转换。这是通过调用回调函数的进程完成的code_change(请参阅gen_server(3)STDLIB中的手册页)。Extra在这种情况下[],该术语按原样传递给函数:

-module(ch3).
...
-export([code_change/3]).
...
code_change({down, _Vsn}, {Chs, N}, _Extra) ->
    {ok, Chs};
code_change(_Vsn, Chs, _Extra) ->
    {ok, {Chs, 0}}.

第一个参数是{down,Vsn}如果降级,或者Vsn升级。该术语Vsn是从模块的“original”版本获取的,也就是您要升级或降级到的版本。

版本由模块属性定义vsn,如果有的话。没有这样的属性ch3,所以在这种情况下,版本是beam文件的校验和(一个巨大的整数),一个无趣的值,它被忽略。

其他回调函数ch3也必须修改,并且可能需要添加一个新的接口函数,但这里没有显示。

12.5模块相关性

假设一个模块是通过添加的接口功能,如在该示例中延伸Release Handling,其中一个功能available/0被添加到ch3

如果一个调用被添加到这个函数中,比如在模块中m1,如果在新版本m1被加载ch3:available/0之前先加载新版本并且调用新版本,则在版本升级期间可能发生运行时错误ch3

因此,ch3必须m1在升级之前,在升级情况下,相反在降级情况下加载。m1据说是依赖于 ch3。在发布处理指令中,这由DepMods元素表示:

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

DepMods是一个模块列表,其中Module是依赖的。

例如:m1应用程序中的模块myapp依赖于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并且ch3属于同一个应用程序,则该.appup文件可以如下所示:

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

m1依赖于ch3也是降级的时候。systools知道升降之间的区别,并生成正确的relup,在哪里ch3之前加载m1当升级时,但是m1之前加载ch3当降级的时候。

12.6特殊程序的更改代码

在这种情况下,简单的代码替换是不够的。当加载特殊过程的新版居住模块时,过程必须对其循环功能进行完全合格的调用才能切换到新代码。因此,必须使用同步代码替换。

注意

用户定义的居住模块的name(s)必须Modules在特殊过程的子规范的部分中列出。否则释放处理程序无法找到该进程。

例如:考虑例如ch4sys and proc_lib。当由主管启动时,子规格可以如下所示:

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

如果ch4是应用程序的一部分。sp_app当从本应用程序的版本“1”升级到“2”时,将加载该模块的新版本,sp_app.appup如下所示:

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

update指令必须包含元组{advanced,Extra}。该指令使特殊进程调用回调函数system_code_change/4,该函数是用户必须执行的。Extra在这种情况下[],该术语按原样传递给system_code_change/4

-module(ch4).
...
-export([system_code_change/4]).
...

system_code_change(Chs, _Module, _OldVsn, _Extra) ->
    {ok, Chs}.
  • 第一个参数是内部状态State,从函数传递sys:handle_system_msg(Request, From, Parent, Module, Deb, State),并在收到系统消息时由特殊进程调用。在ch4,内部状态是可用频道的集合Chs
  • 第二个参数是模块的名称(ch4)。
  • 第三个参数是Vsn{down,Vsn},如gen_server:code_change/3在中所描述的Changing Internal State

在这种情况下,除了第一个参数以外的所有参数都被忽略,函数只是简单地返回内部状态。如果代码只被扩展,这就够了。如果改变内部状态(类似于中的例子Changing Internal State),则在此函数中完成并{ok,Chs2}返回。

12.7更换主管

管理员行为支持更改内部状态,即更改重新启动策略和最大重新启动频率属性,以及更改现有子规范。

子进程可以添加或删除,但不会自动处理。说明必须在.appup文件中给出。

变化性质

由于主管要更改其内部状态,因此需要同步代码替换。但是,update必须使用特殊说明。

首先,在升级和降级的情况下,必须加载新版本的回调模块。然后init/1可以检查新的返回值并相应地改变内部状态。

upgrade监督人员使用以下说明:

{update, Module, supervisor}

示例:要将ch_sup(从Supervisor Behaviour)的重新启动策略更改one_for_oneone_for_all,请init/1ch_sup.erl以下位置更改回调函数:

-module(ch_sup).
...

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

档案ch_app.appup

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

更改子规格

.appup当更改现有的子规范时,该指令以及文件与前面所述的更改属性时相同:

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

这些更改不会影响现有的子进程。例如,更改启动函数仅指定子进程如何重新启动,如果稍后需要的话。

子规格的id不能改变。

更改Modules子规范的字段可能会影响发布处理过程本身,因为此字段用于标识执行同步代码替换时哪些进程受到影响。

添加和删除子进程

如前所述,更改子规范不影响现有子进程。新的子规范会自动添加,但不会删除。子进程不会自动启动或终止,必须使用apply指示。

例子:假设一个新的子进程m1将添加到ch_sup升级时ch_app从“1”到“2”。这意味着m1在将“2”降级为“1”时删除:

{"2",
 [{"1",
   [{update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor}
   ]}]
}.

指令的顺序很重要。

主管必须注册为ch_sup才能让脚本生效。如果未注册监控器,则无法从脚本直接访问它。而是一个帮助函数,它查找主管的PID并调用supervisor:restart_child等等,必须写下来。然后使用apply指令。

如果模块m1的版本“2”中介绍了ch_app,升级时也必须加载,降级时必须删除:

{"2",
 [{"1",
   [{add_module, m1},
    {update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor},
    {delete_module, m1}
   ]}]
}.

如前所述,说明的顺序很重要。在m1启动新子进程之前,升级时必须加载,并且更改了管理员子规格。降级时,必须终止子进程,然后才能更改子规范并删除该模块。

12.8添加或删除模块

例子:一种新的功能模块m被添加到ch_app*

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

12.9启动或终止进程

在根据OTP设计原则构建的系统中,任何过程都是属于主管的子过程,请参阅Adding and Deleting Child Processes更改主管。

12.10添加或删除应用程序

添加或删除应用程序时,不需要.appup文件。生成时relup,将.rel比较文件并自动添加add_applicationremove_application指令。

12.11重新启动应用程序

如果更改过于复杂而无法在不重新启动进程的情况下重新启动应用程序,例如,如果监督程序层次结构已重新构建,则重新启动应用程序非常有用。

示例:添加子项m1ch_sup,如Adding and Deleting Child Processes更改管理员中所述,更新管理员的替代方法是重新启动整个应用程序:

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

12.12更改应用程序规范

安装发行版时,应用程序规范在评估relup脚本之前会自动更新。因此,.appup文件中不需要说明:

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

12.13更改应用程序配置

通过更新文件中的env密钥来更改应用程序配置.app是一个更改应用程序规范的实例,请参阅上一节。

或者,可以在中添加或更新应用程序配置参数sys.config

12.14 更改包含的应用程序

用于添加,删除和重新启动应用程序的版本处理说明仅适用于主要应用程序。对于包含的应用程序没有相应的说明。但是,由于包含的应用程序实际上是一个具有最高管理者的监督树,作为包含应用程序中的主管的子进程启动,relup因此可以手动创建文件。

示例:假设有一个包含应用程序的版本prim_app,其中的监督器prim_sup在其监督树中。

在该版本的新版本中,该应用程序ch_app将被包含进来prim_app。也就是说,它的最高管理者ch_sup将作为一个子进程开始prim_sup

工作流程如下:

步骤1)编辑代码prim_sup

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

步骤2)编辑.app文件prim_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使用restart_application指令.appup文件中的。

但是,如果完成并relup生成文件,则不仅会包含重新启动(即删除和添加)prim_app指令,还会包含启动ch_app(并在降级时停止)的说明。这是因为ch_app包含在新.rel文件中,而不是旧文件中。

相反,一个正确的relup文件可以手动创建,可以从头开始创建,也可以编辑生成的版本。启动/停止指令ch_app由装载/卸载应用程序的指令取代:

{"B",
 [{"A",
   [],
   [{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
    {load_object_code,{prim_app,"2",[prim_app,prim_sup]}},
    point_of_no_return,
    {apply,{application,stop,[prim_app]}},
    {remove,{prim_app,brutal_purge,brutal_purge}},
    {remove,{prim_sup,brutal_purge,brutal_purge}},
    {purge,[prim_app,prim_sup]},
    {load,{prim_app,brutal_purge,brutal_purge}},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {load,{ch_sup,brutal_purge,brutal_purge}},
    {load,{ch3,brutal_purge,brutal_purge}},
    {apply,{application,load,[ch_app]}},
    {apply,{application,start,[prim_app,permanent]}}]}],
 [{"A",
   [],
   [{load_object_code,{prim_app,"1",[prim_app,prim_sup]}},
    point_of_no_return,
    {apply,{application,stop,[prim_app]}},
    {apply,{application,unload,[ch_app]}},
    {remove,{ch_sup,brutal_purge,brutal_purge}},
    {remove,{ch3,brutal_purge,brutal_purge}},
    {purge,[ch_sup,ch3]},
    {remove,{prim_app,brutal_purge,brutal_purge}},
    {remove,{prim_sup,brutal_purge,brutal_purge}},
    {purge,[prim_app,prim_sup]},
    {load,{prim_app,brutal_purge,brutal_purge}},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {apply,{application,start,[prim_app,permanent]}}]}]
}.

主管变更

步骤4b)启动包含的应用程序的另一种方式(或在降级的情况下停止它)是通过组合用于向/从prim_sup加载和卸载子进程的指令与加载/卸载所有ch_app代码及其应用程序规范的指令进行组合。

同样,该relup文件是手动创建的。无论是从头开始还是编辑生成的版本。首先加载所有代码ch_app,并在prim_sup更新之前加载应用程序规范。降级时,prim_sup首先要更新代码,ch_app然后卸载其应用程序规范的代码。

{"B",
 [{"A",
   [],
   [{load_object_code,{ch_app,"1",[ch_sup,ch3]}},
    {load_object_code,{prim_app,"2",[prim_sup]}},
    point_of_no_return,
    {load,{ch_sup,brutal_purge,brutal_purge}},
    {load,{ch3,brutal_purge,brutal_purge}},
    {apply,{application,load,[ch_app]}},
    {suspend,[prim_sup]},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {code_change,up,[{prim_sup,[]}]},
    {resume,[prim_sup]},
    {apply,{supervisor,restart_child,[prim_sup,ch_sup]}}]}],
 [{"A",
   [],
   [{load_object_code,{prim_app,"1",[prim_sup]}},
    point_of_no_return,
    {apply,{supervisor,terminate_child,[prim_sup,ch_sup]}},
    {apply,{supervisor,delete_child,[prim_sup,ch_sup]}},
    {suspend,[prim_sup]},
    {load,{prim_sup,brutal_purge,brutal_purge}},
    {code_change,down,[{prim_sup,[]}]},
    {resume,[prim_sup]},
    {remove,{ch_sup,brutal_purge,brutal_purge}},
    {remove,{ch3,brutal_purge,brutal_purge}},
    {purge,[ch_sup,ch3]},
    {apply,{application,unload,[ch_app]}}]}]
}.

12.15更改非Erlang代码

使用另一种编程语言编写的程序(而不是Erlang)的代码更改(例如,端口程序)取决于应用程序,OTP不提供特别的支持。

示例:在更改端口程序的代码时,假定控制端口的Erlang进程为a gen_server portc,并且该端口在回调函数中打开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}}]}]
].

确保privC程序所在的目录包含在新版本软件包中:

1> systools:make_tar("my_release", [{dirs,[priv]}]).
...

12.16模拟器重启和升级

两条升级指令重启模拟器:

  • restart_new_emulator 在升级ERTS,Kernel,STDLIB或SASL时使用。当由relup文件生成时它会自动添加systools:make_relup/3,4。它在所有其他升级说明之前执行。有关此指令的更多信息,请参见中的restart_new_emulator(低级)Release Handling Instructions
  • restart_emulator

在所有其他升级指令执行后需要重新启动仿真器时使用。有关此指令的更多信息,请参见中的restart_emulator(低级)Release Handling Instructions

如果需要重新启动模拟器并且不需要升级指令,也就是说,如果重新启动本身足够让升级后的应用程序开始运行新版本,relup则可以手动创建一个简单的文件:

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

在这种情况下,可以使用具有发行包自动打包和解包,自动路径更新等的发布处理程序框架,而无需指定.appup文件。

12.17仿真器从Pre OTP R15升级

从OTP R15开始,通过在加载代码和运行其他应用程序的升级指令之前,使用新版本的核心应用程序(Kernel,STDLIB和SASL)重新启动仿真器来执行仿真器升级。为此,升级版本必须包含OTP R15或更高版本。

对于升级版本包含较早的仿真器版本的情况,systools:make_relup创建一个向后兼容的relup文件。这意味着所有升级指令都在重新启动仿真器之前执行。新的应用程序代码因此被加载到旧的模拟器中。如果新代码使用新仿真器进行编译,则可能会出现波束格式发生变化并且无法加载波束文件的情况。为了克服这个问题,用旧的模拟器编译新的代码。

Erlang 20

Erlang 是一种通用的面向并发的编程语言,可应付大规模开发活动的程序设计语言和运行环境。

主页 https://www.erlang.org/
源码 https://github.com/erlang/otp
版本 20
发布版本 20.1