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

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

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

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

有没有更好的方法来做到这一点?


我见过的一种方法是使用timeout 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/2被调用,具有timeout原子和服务器状态。 在我们的例子中,如果超时时间等于0,超时应该在gen_server:start结束之前发生。 这意味着即使在我们能够将服务器的pid返回给其他任何人之前,也应该调用handle_info 。 所以这个timeout_init应该首先调用我们的服务器,并给我们一些保证,我们完成初始化,然后再处理其他事情。

如果你不喜欢这种方法(不太可读),你可以尝试在init/1发送消息给self

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通常是模块名称(atom),并且在这个名字下工作得很好直到有一些已注册(并且活着)的进程。 当然,这个cast是标准的Erlang发送的信息! 。 没什么神奇的,几乎和你在init使用self() ! {finish ... self() ! {finish ...

但在我们的案例中,我们假设还有一件事。 不仅仅是注册部分,而且我们的服务器完成了初始化。 当然,由于我们正在处理消息框,所花费的时间并不重要,但重要的是我们收到的是什么消息。 确切地说,我们希望在收到count消息之前收到finish_init消息。

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

有三种解决方案。

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

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

   ... ;
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}.

通过这种方式,我们在注册之前以及任何其他人可以发送和count消息之前发送finish_init消息。

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

所以最终都取决于你需要什么,甚至是你在生产中遇到什么问题。 了解可能发生什么坏事很重要,但我个人不会试图解决它,直到我实际上遭受这种竞争状况。

链接地址: http://www.djcxy.com/p/38189.html

上一篇: Erlang: how to deal with long running init callback?

下一篇: What OTP pattern to use for gen