12. Appup Cookbook
12 Appup Cookbook
本部分包含.appup
用于在运行时完成升级/降级典型情况的文件示例。
12.1更改功能模块
例如,如果功能模块已更改,如果添加了新功能或修正了错误,则只需更换简单的代码即可,例如:
{"2",
[{"1", [{load_module, m}]}],
[{"1", [{load_module, m}]}]
}.
12.2更换Residence模块
在根据OTP设计原理实现的系统中,所有的过程,除了系统进程和特殊工艺,驻留在行为之一supervisor
,gen_server
,gen_fsm
,gen_statem
或gen_event
行为之一。这些属于STDLIB应用程序,升级/降级通常需要重新启动仿真器。
OTP因此不提供对改变居住模块的支持,除非是special processes
。
12.3更改回调模块
回调模块是一个功能模块,对于代码扩展,简单的代码替换就足够了。
例子:
当添加一个函数时ch3
,如例子中所述Release Handling
,ch_app.appup
看起来如下所示:
{"2",
[{"1", [{load_module, ch3}]}],
[{"1", [{load_module, ch3}]}]
}.
OTP还支持更改行为过程的内部状态,请参阅Changing Internal State
。
12.4内部状态的变化
在这种情况下,简单的代码替换是不够的。code_change
在切换到新版本的回调模块之前,进程必须使用回调函数显式地转换其状态。因此,使用了同步代码替换。
例如:
考虑gen_server
ch3
从gen_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
在特殊过程的子规范的部分中列出。否则释放处理程序无法找到该进程。
例如:
考虑例如ch4
在sys 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_one
为one_for_all
,请init/1
在ch_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_application
和remove_application
指令。
12.11重新启动应用程序
如果更改过于复杂而无法在不重新启动进程的情况下重新启动应用程序,例如,如果监督程序层次结构已重新构建,则重新启动应用程序非常有用。
示例:
添加子项m1
时ch_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}}]}]
].
确保priv
C程序所在的目录包含在新版本软件包中:
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文件。这意味着所有升级指令都在重新启动仿真器之前执行。新的应用程序代码因此被加载到旧的模拟器中。如果新代码使用新仿真器进行编译,则可能会出现波束格式发生变化并且无法加载波束文件的情况。为了克服这个问题,用旧的模拟器编译新的代码。