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

时间:2014-11-07 20:16:36

标签: erlang otp supervisor gen-server

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

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

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

有更好的方法吗?

1 个答案:

答案 0 :(得分: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( .... 

几乎所有结果都可以通过额外的超时参数返回,这基本上是等待另一条消息的时间。它给出时间通过调用handle_info/2timeout原子和服务器状态。在我们的例子中,当超时等于0时,甚至在gen_server:start完成之前就应该发生超时。这意味着即使在我们能够将服务器的pid返回给其他任何人之前,也应该调用handle_info。因此,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 

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


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

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

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

count() ->
   gen_server:cast(?SERVER, count).

其中?SERVER通常是模块名称(原子),哪个可以正常工作,直到此名称下是一些注册(和活动)进程。当然,这个cast是标准的Erlang与!一起发送的消息。没有什么神奇之处,几乎与你init self() ! {finish ...中的一样。

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

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

这有三种解决方案。

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

第二种情况是忽略这种不良消息/状态事件。

很容易实现
   ... ;
handle_cast( _, State) -> 
   {noreply, State}.

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

在这两种情况下,您可能会丢失一条count消息。如果这确实是一个问题,您仍然可以尝试通过将最后一个子句更改为

来修复它
   ... ;
handle_cast(Message, no_state_yet) -> 
   gen_server:cast( ?SERVER, Message),
   {noreply, no_state_yet}.

但这有其他明显的优点,我更喜欢"让它失败"方法

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

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

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

但是这种方法有其自身的缺点,主要是注册本身,可能会以几种不同的方式失败。可以随时检查OTP handles that的方式,并复制此代码。但这是另一个故事。

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