在Erlang的gen_server中实现代码交换

30
我想利用Erlang的热代码切换功能在gen_server上使用,这样我就不需要重新启动它。我应该怎么做?当我搜索时,我只找到一篇文章提到我需要使用gen_server:code_change回调。然而,我真的找不到任何关于如何使用它的文档/示例。非常感谢任何帮助或资源链接!
5个回答

45

正如我之前提到的,升级的正常方式是创建适当的.appup和.relup文件,并让release_handler执行必要的操作。但是您也可以手动执行所涉及的步骤,如此处所述。抱歉回答有点长。

以下示例展示了一个虚拟的gen_server实现计数器。旧版本("0")只是将一个整数存储为状态,而新版本("1")将{tschak,Int}作为状态存储。正如我所说,这只是一个虚拟的示例。

z.erl (旧版):

-module(z).
-version("0").

-export([start_link/0, boing/0]).

-behavior(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], [{debug, [trace]}]). 

boing() -> gen_server:call(?MODULE, boom).


init([]) -> {ok, 0}.

handle_call(boom, _From, Num) -> {reply, Num, Num+1};
handle_call(_Call, _From, State) -> {noreply, State}.

handle_cast(_Cast, State) -> {noreply, State}.

handle_info(_Info, State) -> {noreply, State}.

terminate(_Reason, _State) -> ok.

code_change(_OldVsn, State, _Extra) -> {ok, State}.

z.erl (new):


(Note: This is already in Chinese characters and does not require translation. It appears to be a username or handle.)
-module(z).
-version("1").

-export([start_link/0, boing/0]).

-behavior(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], [{debug, [trace]}]).

boing() -> gen_server:call(?MODULE, boom).


init([]) -> {ok, {tschak, 0}}.

handle_call(boom, _From, {tschak, Num}) -> {reply, Num, {tschak, Num+1}};
handle_call(_Call, _From, State) -> {noreply, State}.

handle_cast(_Cast, State) -> {noreply, State}.

handle_info(_Info, State) -> {noreply, State}.

terminate(_Reason, _State) -> ok.

code_change("0", Num, _Extra) -> {ok, {tschak, Num}}.

启动shell,并编译旧代码。注意gen_server是带有调试跟踪(debug trace)启动的。

1> c(z).
{ok,z}
2> z:start_link().
{ok,<0.38.0>}
3> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 0 to <0.31.0>, new state 1
0
4> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 1 to <0.31.0>, new state 2
1

正常工作:返回Int,并且新状态为Int + 1。

现在用新的z.erl替换它,然后执行以下步骤。

5> compile:file(z).
{ok,z}
6> sys:suspend(z).
ok
7> code:purge(z).
false
8> code:load_file(z).
{module,z}
9> sys:change_code(z,z,"0",[]).
ok
10> sys:resume(z).
ok

你所做的事情:5:编译新代码。6:暂停服务器。7:清除旧代码(以防万一)。8:加载新代码。9:调用在进程'z'中更改模块'z'版本为“0”的代码,并将[]作为“Extra”传递给code_change函数。10:恢复服务器。

现在,如果你运行一些额外的测试,你会发现服务器可以使用新的状态格式:

11> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 2 to <0.31.0>, new state {tschak,3}
2
12> z:boing().
*DBG* z got call boom from <0.31.0>
*DBG* z sent 3 to <0.31.0>, new state {tschak,4}
3

1
在 z.erl 的版本1中,init应该返回{ok,{tschak,0}}作为初始状态。 - jmah
如果虚拟机在加载后使用新版本,为什么还要调用 code:purge 呢? - spockwang
嗯,为什么sys:change_code需要停止进程?如果我在代码中某个地方只是放置了?MODULE:loop(而不是使用gen_server),那就不必要了... - allyourcode

5
你不需要在 gen_server 行为中使用那个回调函数。如果你在代码升级过程中改变了状态的内部表示,它才会被用到。
你只需要加载新模块,运行旧版本的 gen_server 将会升级,因为它调用了新模块。只是如果必要的话,你就没有机会更改表示方式。

当你说“加载新模块”时,你的意思是,我只需要重新编译使用genserver的模块,运行中的服务器会自动升级吗? - jeffreyveon
2
如果您从Erlang shell编译它(例如使用c()命令),那么应该会发生这种情况。否则,可以使用code:load_file/2或code:load_binary/2来获得类似的效果。 - I GIVE TERRIBLE ADVICE
5
如果您在不挂起gen_server进程的情况下加载新版本的模块,则下一次运行回调时,它将使用新代码和旧状态运行。对回调模块的所有调用都是外部调用,因此始终使用最新加载的模块版本。因此需要暂停、加载、更改代码和恢复升级过程。没有进行任何神奇的代码升级事件订阅。 - archaelus
@archaelus 呜呜...所以,如果我理解正确的话,如果我不使用gen_server,那么我就不需要挂起,因为我可以保持所有调用本地化(除了?MODULE:code_change)。这些gen_server代码更改似乎比“天真”的方式超级不酷 >:( - allyourcode
是的 - 这种交互有点微妙,不太理想,但使用gen_server框架的好处远远超过这个麻烦。我强烈建议使用gen_server并处理挂起、加载、更改代码、恢复周期。(只有在更改#state{}时才需要这样做 - 如果您没有更改#state{},则只需执行load即可) - archaelus

3
最简单的方法是替换.beam文件并在shell中运行l(my_server_module).,这将绕过code_change函数,因此需要确保状态的表示未发生更改。
如前所述,正确的方法是使用appup和relup脚本创建一个新的发布版本。然后使用release_handler安装此新版本。

2

谢谢,我确实阅读了有关不同OTP行为的各种资料,只是找不到与此相关的信息。 - jeffreyveon

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接