无法在GenServer

时间:2017-07-28 06:52:11

标签: elixir otp

我正在尝试将一个进程用作同步机制,它可以接收我们的订单消息,但仍能正常运行。我已经设法使用普通进程实现我的问题的简化版本,但是我无法与GenServer实现相同的效果。

简化版本是这样的:

defmodule Fun do
  def start_link do
    spawn(fn -> loop(:initiated) end)
  end

  def loop(state) do
    receive do
      :join when state == :initiated ->
        IO.inspect("Handling join")
        loop(:initiated)

      :finish when state == :initiated ->
        IO.inspect("Finishing")
        loop(:finishing)

      :cleanup when state == :finishing ->
        IO.inspect("Cleaning up...")
    end
  end
end

上述内容仅在:join:finish时处理state:initiated条消息,仅执行:cleanup并在:finish时退出已收到。在这里,我试图利用邮件卡在邮箱中,直到它们匹配为止。

它的工作原理如下:

iex(1)> pid = Fun.start_link
#PID<0.140.0>
iex(2)> send(pid, :join)
"Handling join"
:join
iex(3)> send(pid, :join)
"Handling join"
:join
iex(4)> send(pid, :cleanup) # No `IO.inspect` at this point
:cleanup
iex(5)> send(pid, :finish) # Two `IO.inspect`s once `:finish` received
"Finishing"
:finish
"Cleaning up..."

我试图用GenServer重现相同的行为:

defmodule FunServer do
  use GenServer

  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    {:ok, :initiated}
  end

  def handle_info(:join, msg) when msg == :initiated do
    IO.inspect("Handling join [From GenServer]")
    {:noreply, :initiated}
  end

  def handle_info(:finish, msg) when msg == :initiated do
    IO.inspect("Finishing [From GenServer]")
    {:noreply, :finishing}
  end

  def handle_info(:cleanup, msg) when msg == :finishing do
    IO.inspect("Cleaning up [From GenServer]")
    {:stop, :normal, msg}
  end
end

鉴于我使用此GenServer将我的应用程序配置为:temporary worker,它的工作原理如下:

iex(1)> send(FunServer, :join)
"Handling join [From GenServer]"
:join
iex(2)> send(FunServer, :cleanup)
:cleanup
iex(3)>
07:11:17.383 [error] GenServer FunServer terminating
** (FunctionClauseError) no function clause matching in 
FunServer.handle_info/2
    (what_the_beam) lib/fun_server.ex:22: FunServer.handle_info(:cleanup, :initiated)
    (stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
    (stdlib) gen_server.erl:686: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: :cleanup
State: :initiated

我尝试过使用handle_cast回调,以及不同格式的参数,例如:

handle_info(:cleanup, :finishing)

handle_cast(:cleanup, :finishing)

而不是where msg == :finishing,但它们都不适合我。

我正在使用Elixir 1.5和Erlang 20。

3 个答案:

答案 0 :(得分:2)

在原始代码中,当状态为receive时,:cleanup会忽略:initiated消息。这是一个不好的做法,可能导致瓶颈,因为任何此类消息将永远留在进程的收件箱中,耗尽内存并减慢未来的receive块,因为receive(通常)花费时间与进程收件箱中的邮件数量成比例。

GenServer通过强制您按照收到的顺序处理邮件来正确处理此案例。为了忽略消息,您需要显式添加不执行任何操作的handle_info。您可以简单地添加此子句,当状态为:cleanup时,将忽略:initiated

def handle_info(:cleanup, :initiated), do: {:noreply, :initiated}

您也可以通过在所有现有handle_info之后添加此子句来忽略任何其他消息:

def handle_info(_message, state), do: {:noreply, state}
  

我尝试过使用handle_cast回调,以及不同格式的参数,例如:

handle_info(:cleanup, :finishing)
     

...

如果您仔细阅读错误消息中的堆栈跟踪,问题是:gen_server试图调用FunServer.handle_info(:cleanup, :initiated),但handle_info中没有定义任何条款来处理它。 (:cleanup, :finishing)没有问题。

答案 1 :(得分:0)

您遇到的问题称为选择性接收常规erlang进程。这是Joe Armstrong的书中的引用:

    receive works as follows:
    ...
    2. Take the first message in the mailbox and try to match it 
    against Pattern1, Pattern2, and so on. If the match succeeds, 
    the message is removed from the mailbox, and the expressions 
    following the pattern are evaluated.
    3. If none of the patterns in the receive statement matches the 
    first message in the mailbox, then the first message is removed 
    from the mailbox and put into a “save queue.” The second message
    in the mailbox is then tried. This procedure is repeated until a 
    matching message is found or until all the messages in the mail- 
    box have been examined.
    4. If none of the messages in the mailbox matches, then the process 
    is suspended and will be rescheduled for execution the next time 
    a new message is put in the mailbox. Note that when a new message 
    arrives, the messages in the save queue are not rematched; only 
    the new message is matched.
    5. As soon as a message has been matched, then all messages that 
    have been put into the save queue are reentered into the mailbox 
    in the order in which they arrived at the process. If a timer 
    was set, it is cleared.
    6. If the timer elapses when we are waiting for a message, then 
    evaluate the expressions ExpressionsTimeout and put any saved 
    messages back into the mailbox in the order in which they 
    arrived at the process.

gen_server不是这样工作的。它需要你的回调模块来匹配一条消息,或者你发现有一个错误抛出,因为gen_server实现没有找到一种分派方法。如果您希望gen_server实现与上面概述的接收逻辑相匹配,则必须手动执行。一种简单的方法是在某种列表中累积您在给定状态下无法匹配的所有消息,然后在每次成功匹配后将它们重新发送到self()。为此,你的状态不再仅仅是一个简单的原子,因为你需要自己组织保存的队列。

而BTW之前在erlang的背景下提出了同样的问题。响应者有一个与我描述的一对一的建议。因此,如果您需要具体代码,请链接到该问题:How do you do selective receives in gen_servers?

答案 2 :(得分:0)

使用:gen_statem(或Elixir包装器库,在十六进制上找到gen_state_machine)可以更轻松地完成您要实现的目标。您可以使用&#34;推迟&#34;模拟选择性接收。功能,它将把消息放回内部缓冲区,直到机器的状态发生变化,此时推迟的消息将按接收顺序处理。

还有其他一些好的技巧,比如能够生成&#34;内部&#34;在处理任何其他内容之前放置在邮箱头部的邮件。由于您的示例是一个非常明确的FSM案例,我建议您使用该路由,而不是在GenServer中重新创建它。