12  Appup 食谱

12 Appup 食谱

本节包含 .appup 文件的示例,用于在运行时进行升级/降级操作的典型情况。

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

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

在根据 OTP 设计原则实现的系统中,除系统进程和特殊进程外,所有进程都驻留在以下行为之一中:supervisorgen_servergen_fsmgen_statemgen_event。这些属于 STDLIB 应用程序,升级/降级通常需要重启仿真器。

因此,OTP 不支持更改驻留模块,除非是在 特殊进程 的情况下。

回调模块是一个功能模块,对于代码扩展,只需简单的代码替换即可。

示例: 当在 ch3 中添加函数时,如 版本处理 中的示例所述,ch_app.appup 如下所示

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

OTP 还支持更改行为进程的内部状态,请参阅 更改内部状态

在这种情况下,简单的代码替换是不够的。进程必须在切换到回调模块的新版本之前,使用回调函数 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 的其他回调函数也必须修改,并且可能需要添加新的接口函数,但这里没有显示。

假设通过添加接口函数来扩展模块,如 版本处理 中的示例所示,其中向 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}]}]
}.

如果 m1ch3 属于同一个应用程序,则 .appup 文件可以如下所示

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

降级时,m1 也依赖于 ch3systools 知道升级和降级的区别,并生成正确的 relup,其中在升级时,ch3m1 之前加载,但在降级时,m1ch3 之前加载。

在这种情况下,简单的代码替换是不够的。当加载特殊进程的驻留模块的新版本时,进程必须对其循环函数进行完全限定的调用以切换到新代码。因此,必须使用同步代码替换。

注意

用户定义的驻留模块的名称必须列在特殊进程的子进程规范的 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}

监督者行为支持更改内部状态,即更改重启策略和最大重启频率属性,以及更改现有的子进程规范。

可以添加或删除子进程,但这不是自动处理的。必须在 .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并更改主管子规范,然后才能启动新的子进程。在降级时,必须先终止子进程,然后再更改子规范并删除模块。

示例:将一个新的功能模块m添加到ch_app

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

在根据 OTP 设计原则构建的系统中,任何进程都是属于主管的子进程,请参阅更改主管中的添加和删除子进程

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

当更改过于复杂而无法在不重新启动进程的情况下进行时,重新启动应用程序很有用,例如,如果主管层次结构已重构。

示例:在将子进程m1添加到ch_sup时,就像更改主管中的添加和删除子进程一样,更新主管的替代方法是重新启动整个应用程序。

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

安装发布时,应用程序规范会在评估relup脚本之前自动更新。因此,.appup文件中不需要任何指令。

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

通过更新.app文件中的env键来更改应用程序配置,是更改应用程序规范的一种情况,请参见上一节。

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

用于添加、删除和重新启动应用程序的发布处理指令仅适用于主应用程序。对于包含的应用程序,没有相应的指令。但是,由于包含的应用程序实际上是一个具有最顶层主管的监督树,它作为包含应用程序中主管的子进程启动,因此可以手动创建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]}}]}]
}.

更改用 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]}]).
...

两个升级指令会重启模拟器

  • 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文件,而使用具有自动打包和解包发布包、自动路径更新等的发布处理程序框架。

从 OTP R15 开始,通过在加载代码并运行其他应用程序的升级指令之前,使用核心应用程序(Kernel、STDLIB 和 SASL)的新版本重启模拟器来执行模拟器升级。要使此操作有效,要从其升级的发布版本必须包含 OTP R15 或更高版本。

对于要从其升级的发布版本包含早期模拟器版本的情况,systools:make_relup会创建一个向后兼容的 relup 文件。这意味着所有升级指令都在重启模拟器之前执行。因此,新的应用程序代码将加载到旧模拟器中。如果使用新模拟器编译了新代码,则可能会出现 beam 格式已更改且无法加载 beam 文件的情况。要解决此问题,请使用旧模拟器编译新代码。