12 Appup 食谱
本节包含 .appup 文件的示例,用于在运行时进行升级/降级操作的典型情况。
12.1 更改功能模块
如果更改了功能模块,例如,添加了新函数或修复了错误,则只需简单的代码替换即可,例如
{"2", [{"1", [{load_module, m}]}], [{"1", [{load_module, m}]}] }.
12.2 更改驻留模块
在根据 OTP 设计原则实现的系统中,除系统进程和特殊进程外,所有进程都驻留在以下行为之一中:supervisor、gen_server、gen_fsm、gen_statem 或 gen_event。这些属于 STDLIB 应用程序,升级/降级通常需要重启仿真器。
因此,OTP 不支持更改驻留模块,除非是在 特殊进程 的情况下。
12.3 更改回调模块
回调模块是一个功能模块,对于代码扩展,只需简单的代码替换即可。
示例: 当在 ch3 中添加函数时,如 版本处理 中的示例所述,ch_app.appup 如下所示
{"2", [{"1", [{load_module, ch3}]}], [{"1", [{load_module, ch3}]}] }.
OTP 还支持更改行为进程的内部状态,请参阅 更改内部状态。
12.4 更改内部状态
在这种情况下,简单的代码替换是不够的。进程必须在切换到回调模块的新版本之前,使用回调函数 code_change 显式地转换其状态。因此,使用同步代码替换。
示例: 考虑来自 gen_server 行为 的 gen_server ch3。内部状态是一个项 Chs,它表示可用的通道。假设你想要添加一个计数器 N,它跟踪到目前为止的 alloc 请求数量。这意味着格式必须更改为 {Chs,N}。
.appup 文件可以如下所示
{"2", [{"1", [{update, ch3, {advanced, []}}]}], [{"1", [{update, ch3, {advanced, []}}]}] }.
update 指令的第三个元素是一个元组 {advanced,Extra},它表示受影响的进程在加载模块的新版本之前要进行状态转换。这是由进程调用回调函数 code_change 来完成的(请参阅 STDLIB 中的 gen_server(3) 手册页)。项 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 从模块的“原始”版本中获取,即你正在升级的版本或降级的版本。
版本由模块属性 vsn(如果有)定义。ch3 中没有这样的属性,因此在本例中,版本是 beam 文件的校验和(一个巨大的整数),这是一个不重要的值,将被忽略。
ch3 的其他回调函数也必须修改,并且可能需要添加新的接口函数,但这里没有显示。
12.5 模块依赖关系
假设通过添加接口函数来扩展模块,如 版本处理 中的示例所示,其中向 ch3 添加了函数 available/0。
如果在模块 m1 中添加了对该函数的调用,那么在版本升级期间,如果先加载了 m1 的新版本,并在加载 ch3 的新版本之前调用了 ch3:available/0,就会发生运行时错误。
因此,在升级的情况下,ch3 必须在 m1 之前加载,反之亦然,在降级的情况下。可以说 m1 **依赖于** ch3。在版本处理指令中,这由 DepMods 元素表示
{load_module, Module, DepMods} {update, Module, {advanced, Extra}, DepMods}
DepMods 是模块列表,Module 依赖于这些模块。
示例: 当从“1”升级到“2”或从“2”降级到“1”时,应用程序 myapp 中的模块 m1 依赖于 ch3
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 更改特殊进程的代码
在这种情况下,简单的代码替换是不够的。当加载特殊进程的驻留模块的新版本时,进程必须对其循环函数进行完全限定的调用以切换到新代码。因此,必须使用同步代码替换。
用户定义的驻留模块的名称必须列在特殊进程的子进程规范的 Modules 部分中。否则,版本处理程序无法找到该进程。
示例: 考虑 sys 和 proc_lib 中的示例 ch4。当由监督者启动时,子进程规范可以如下所示
{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 的描述所示。
在本例中,除第一个参数外,所有参数都被忽略,该函数只是简单地返回内部状态。如果代码只是扩展了,这已经足够了。如果内部状态发生了更改(类似于 更改内部状态 中的示例),则在该函数中完成此操作,并返回 {ok,Chs2}。
12.7 更改监督者
监督者行为支持更改内部状态,即更改重启策略和最大重启频率属性,以及更改现有的子进程规范。
可以添加或删除子进程,但这不是自动处理的。必须在 .appup 文件中给出指令。
更改属性
由于监督者要更改其内部状态,因此需要同步代码替换。但是,必须使用特殊的 update 指令。
首先,必须加载回调模块的新版本,无论是在升级还是降级的情况下。然后,可以检查 init/1 的新返回值,并相应地更改内部状态。
以下 upgrade 指令用于监督者
{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字段可能会影响发布处理过程本身,因为此字段用于识别在进行同步代码替换时哪些进程受到影响。
添加和删除子进程
如前所述,更改子规范不会影响现有的子进程。新的子规范会自动添加,但不会自动删除。子进程不会自动启动或终止,这必须使用apply指令完成。
示例:假设在将ch_app从“1”升级到“2”时,要将一个新的子进程m1添加到ch_sup。这意味着在从“2”降级到“1”时,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} ]}] }.
指令的顺序很重要。
为了使脚本正常工作,主管必须注册为ch_sup。如果主管未注册,则无法从脚本直接访问它。相反,必须编写一个帮助函数,该函数查找主管的pid并调用supervisor:restart_child等。然后,此函数将使用apply指令从脚本中调用。
如果模块m1是在ch_app的版本“2”中引入的,那么它在升级时也必须加载,在降级时也必须删除。
{"2", [{"1", [{add_module, m1}, {update, ch_sup, supervisor}, {apply, {supervisor, restart_child, [ch_sup, m1]}} ]}], [{"1", [{apply, {supervisor, terminate_child, [ch_sup, m1]}}, {apply, {supervisor, delete_child, [ch_sup, m1]}}, {update, ch_sup, supervisor}, {delete_module, m1} ]}] }.
如前所述,指令的顺序很重要。在升级时,必须先加载m1并更改主管子规范,然后才能启动新的子进程。在降级时,必须先终止子进程,然后再更改子规范并删除模块。
12.8 添加或删除模块
示例:将一个新的功能模块m添加到ch_app
{"2", [{"1", [{add_module, m}]}], [{"1", [{delete_module, m}]}]
12.9 启动或终止进程
在根据 OTP 设计原则构建的系统中,任何进程都是属于主管的子进程,请参阅更改主管中的添加和删除子进程。
12.10 添加或删除应用程序
在添加或删除应用程序时,不需要.appup文件。在生成relup时,会比较.rel文件,并自动添加add_application和remove_application指令。
12.11 重新启动应用程序
当更改过于复杂而无法在不重新启动进程的情况下进行时,重新启动应用程序很有用,例如,如果主管层次结构已重构。
示例:在将子进程m1添加到ch_sup时,就像更改主管中的添加和删除子进程一样,更新主管的替代方法是重新启动整个应用程序。
{"2", [{"1", [{restart_application, ch_app}]}], [{"1", [{restart_application, ch_app}]}] }.
12.12 更改应用程序规范
安装发布时,应用程序规范会在评估relup脚本之前自动更新。因此,.appup文件中不需要任何指令。
{"2", [{"1", []}], [{"1", []}] }.
12.13 更改应用程序配置
通过更新.app文件中的env键来更改应用程序配置,是更改应用程序规范的一种情况,请参见上一节。
或者,可以在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)编辑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(以及在降级情况下停止它)的指令。这是因为ch_app包含在新的.rel文件中,但不包含在旧的.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 进程是一个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}}]}] ].
确保包含 C 程序的priv目录包含在新的发布包中
1> systools:make_tar("my_release", [{dirs,[priv]}]).
...
12.16 模拟器重启和升级
两个升级指令会重启模拟器
-
restart_new_emulator
在升级 ERTS、Kernel、STDLIB 或 SASL 时使用。当systools:make_relup/3,4生成relup文件时,会自动添加此指令。它在所有其他升级指令之前执行。有关此指令的更多信息,请参阅发布处理指令中的restart_new_emulator(低级)。
-
restart_emulator
在执行所有其他升级指令后,当需要重启模拟器时使用。有关此指令的更多信息,请参阅发布处理指令中的restart_emulator(低级)。
如果需要重启模拟器并且不需要升级指令,也就是说,如果重启本身足以让升级后的应用程序开始运行新版本,则可以手动创建一个简单的relup文件。
{"B", [{"A", [], [restart_emulator]}], [{"A", [], [restart_emulator]}] }.
在这种情况下,可以不指定.appup文件,而使用具有自动打包和解包发布包、自动路径更新等的发布处理程序框架。
12.17 从 OTP R15 之前版本升级模拟器
从 OTP R15 开始,通过在加载代码并运行其他应用程序的升级指令之前,使用核心应用程序(Kernel、STDLIB 和 SASL)的新版本重启模拟器来执行模拟器升级。要使此操作有效,要从其升级的发布版本必须包含 OTP R15 或更高版本。
对于要从其升级的发布版本包含早期模拟器版本的情况,systools:make_relup会创建一个向后兼容的 relup 文件。这意味着所有升级指令都在重启模拟器之前执行。因此,新的应用程序代码将加载到旧模拟器中。如果使用新模拟器编译了新代码,则可能会出现 beam 格式已更改且无法加载 beam 文件的情况。要解决此问题,请使用旧模拟器编译新代码。