Erlang:如何处理长时间运行的init回调?

Str*_*s3D 6 erlang erlang-otp gen-server erlang-supervisor

我有一个gen_server当开始尝试在监督树中的主管下启动一定数量的子进程(通常是10-20)时.gen_server的init回调调用supervisor:start_child/2所需的每个子进程.调用supervisor:start_child/2是同步的,因此在子进程启动之前它不会返回.所有子进程也是gen_servers,因此在init回调返回之前,start_link调用不会返回.在init回调中,对第三方系统进行了调用,这可能需要一段时间才能响应(我发现这个问题,当第三方系统的调用在60秒后超时).与此同时,init调用已被阻止,这意味着supervisor:start_child/2它也被阻止.所以调用的gen_server进程supervisor:start_child/2一直没有响应.在等待start_child函数返回时调用gen_server超时.因为这可以容易地持续60秒或更长时间.我想改变它,因为我的应用程序在等待时暂停在一种半启动状态.

解决此问题的最佳方法是什么?

我能想到的唯一解决方案是将与第三方系统交互的代码从init回调移到handle_cast回调中.这将使init回调更快.缺点是我需要gen_server:cast/2在所有子进程启动后调用.

有没有更好的方法呢?

mpm*_*mpm 8

我见过的一种方法是使用超时init/1handle_info/2.

init(Args) ->
  {ok, {timeout_init, Args} = _State, 0 = _Timeout}.


...


handle_info( timeout, {timeout_init, Args}) ->
   %% do your inicialization
   {noreply, ActualServerState};  % this time no need for timeout 

handle_info( .... 
Run Code Online (Sandbox Code Playgroud)

几乎所有结果都可以使用额外的超时参数返回,这基本上是等待另一条消息的时间.它给出了时间传递handle_info/2被调用,带有timeout原子和服务器状态.在我们的例子中,当超时等于0时,即使在gen_server:start完成之前也应该发生超时.handle_info甚至在我们能够将我们的服务器的pid返回给其他任何人之前应该调用的含义.所以这timeout_init应该是第一次调用我们的服务器,并给我们一些保证,我们完成初始化,然后再处理其他任何事情.

如果您不喜欢这种方法(实际上不可读),您可能会尝试向自己发送消息 init/1

init(Args) ->
   self() ! {finish_init, Args},
   {ok, no_state_yet}.

...


handle_info({finish_init, Args} = _Message, no_state_yet) ->
   %% finish whateva 
   {noreply, ActualServerState};

handle_info(  ... % other clauses 
Run Code Online (Sandbox Code Playgroud)

同样,您要确保完成初始化的消息尽快发送到此服务器,这对于在某个原子下注册的gen_servers非常重要.


编辑 经过一些OTP源代码的仔细研究.

当您通过它的pid与服务器通信时,这种方法已经足够了.主要是因为init/1函数返回后返回了pid .但是在gen_..开始时start/4或者start_link/4我们在同一名称下自动注册进程的情况下,它有点不同.你可能会遇到一种竞争条件,我想更详细地解释一下.

如果进程是寄存器,则通常简化所有调用并转换为服务器,如:

count() ->
   gen_server:cast(?SERVER, count).
Run Code Online (Sandbox Code Playgroud)

?SERVER通常模块名称(原子)在哪里可以正常工作,直到这个名称下是一些注册(和活动)过程.当然,在引擎盖下这cast是标准的Erlang的消息发送!.什么神奇之处,几乎你在做同样的initself() ! {finish ....

但在我们的例子中,我们假设还有一件事.不只是注册部分,而且我们的服务器完成了它的初始化.当然,既然我们正在处理消息框,那么事情需要多长时间并不重要,但重要的是我们收到的消息.所以确切地说,我们希望在收到finish_init消息之前收到count消息.

不幸的是这种情况可能发生 这是因为调用回调之前gen,OTP中已经注册了 init/1.因此理论上,当一个进程调用start将进入注册部分的函数时,另一个进程可以找到我们的服务器并发送count消息,之后init/1就会使用finish_init消息调用该函数.机会很小(非常非常小),但它仍然可能发生.

有三种解决方案.

首先是什么都不做.在这种竞争条件的情况下,handle_cast将失败(由于函数子句,因为我们的状态是not_state_yet原子),并且主管将重新启动整个事情.

第二种情况是忽略这种不良信息/状态事件.这很容易实现

   ... ;
handle_cast( _, State) -> 
   {noreply, State}.
Run Code Online (Sandbox Code Playgroud)

作为你的最后一句.不幸的是,大多数使用模板的人使用这种不幸的(恕我直言)模式.

在这两者中你可能会丢失一条count消息.如果这确实是一个问题,你仍然可以尝试通过将最后一个子句更改为来修复它

   ... ;
handle_cast(Message, no_state_yet) -> 
   gen_server:cast( ?SERVER, Message),
   {noreply, no_state_yet}.
Run Code Online (Sandbox Code Playgroud)

但这有其他明显的优点,我宁愿"让它失败"的方法.

第三种选择是稍后注册过程.而不是使用start/4和要求自动注册,使用start/3,接收pid,并自己注册.

start(Args) ->
   {ok, Pid} = gen_server:start(?MODULE, Args, []),
   register(?SERVER, Pid),
   {ok, Pid}.
Run Code Online (Sandbox Code Playgroud)

这样我们就可以finish_init在注册之前发送消息,然后在任何其他人发送消息之前发送count消息.

但是这种方法有它自己的缺点,主要是注册本身,它可能以几种不同的方式失败.人们可以随时检查OTP如何处理,并复制此代码.但这是另一个故事.

所以最终这一切都取决于你需要什么,甚至取决于你在生产中会遇到什么问题.重要的是要知道可能会发生什么坏事,但我个人不会尝试解决任何问题,直到我真的遇到这样的竞争条件.