繁体   English   中英

Elixir:Genserver.call无法初始化handle_call

[英]Elixir: Genserver.call not initiaing handle_call

我正在实现Gossip Algorithm ,其中多个参与者同时并行传播八卦。 当每个Actor听了八卦达10次后,系统将停止。

现在,我有一个方案,在将八卦发送给接收方演员之前,我正在检查接收方演员的侦听计数。 如果侦听计数已为10,则八卦将不会发送给接收方演员。 我正在使用同步调用来获取侦听计数。

def get_message(server, msg) do
    GenServer.call(server, {:get_message, msg})
end

def handle_call({:get_message, msg}, _from, state) do
    listen_count = hd(state) 
    {:reply, listen_count, state}
end

该程序在开始时运行良好,但是过了一段时间, Genserver.call停止,并显示超时错误,如下所示。 经过一些调试后,我意识到Genserver.call处于休眠状态,无法启动相应的handle_call方法。 使用同步调用时是否会出现这种现象? 由于所有参与者都是独立的,因此Genserver.call方法不应在不等待彼此响应的情况下独立运行。

02:28:05.634 [error] GenServer #PID<0.81.0> terminating
    ** (stop) exited in: GenServer.call(#PID<0.79.0>, {:get_message, []}, 5000)
    ** (EXIT) time out
    (elixir) lib/gen_server.ex:774: GenServer.call/3

编辑:在iex shell中运行时,以下代码可以重现该错误。

defmodule RumourActor do
use GenServer

def start_link(opts) do
    {:ok, pid} = GenServer.start_link(__MODULE__,opts)
    {pid}
end

def set_message(server, msg, recipient) do      
    GenServer.cast(server, {:set_message, msg, server, recipient})
end

def get_message(server, msg) do
    GenServer.call(server, :get_message)
end

def init(opts) do
    state=opts
    {:ok,state}
end

def handle_cast({:set_message, msg, server, recipient},state) do
  :timer.sleep(5000)
  c = RumourActor.get_message(recipient, [])
  IO.inspect c
  {:noreply,state}
end

def handle_call(:get_message, _from, state) do
    count = tl(state)
    {:reply, count, state}
end
end

打开iex shell并在模块上方加载。 使用以下两个步骤开始:

a = RumourActor.start_link(["", 3])
b = RumourActor.start_link(["", 5])

通过调用Dogbert在注释中提到的死锁条件来产生错误。 运行时没有太多时差。

cb = RumourActor.set_message(elem(a,0), [], elem(b,0))
ca = RumourActor.set_message(elem(b,0), [], elem(a,0))

等待5秒钟。 将会出现错误。

闲话协议是一种处理异步,未知,未配置(随机)网络的方法,该网络可能会遭受间歇性中断和分区,并且不存在任何领导者或默认结构。 (请注意,这种情况在现实世界中有点不寻常,并且带外控制始终以某种方式强加给系统。)

考虑到这一点,让我们将其更改为一个异步系统(使用cast ),以便我们遵循“闲聊”八卦风格通信概念的精神。

我们需要消息摘要来计算收到给定消息的次数,摘要已经收到并且已经超过幻数的消息(因此,如果延迟太晚,我们就不会重新发送消息),并且我们系统中注册的进程的列表,以便我们知道向谁广播:

(以下示例位于Erlang中,因为自从我停止使用Elixir语法以来,我就一直在使用Elixir语法。)

-module(rumor).

-record(s,
        {peers  = []         :: [pid()],
         digest = #{}        :: #{message_id(), non_neg_integer()},
         dead   = sets:new() :: sets:set(message_id())}).

-type message_id() :: zuuid:uuid().

在这里,我正在使用UUID ,但是它可能是任何东西。 对于测试用例来说, Erlang引用将是很好的选择,但是由于八卦在Erlang集群中没有用,并且引用在原始系统之外也不安全,我只是跳到一个假设,它是针对网络系统的。

我们将需要一个接口函数,该函数允许我们告诉一个过程将新消息注入系统。 我们还将需要一个接口功能,一旦它已经存在于系统中,它将在两个进程之间发送一条消息。 然后,我们将需要一个内部函数,该函数将消息广播到所有已知的(订阅的)对等方。 嗯,这意味着我们需要一个欢迎界面,以便对等进程可以相互通知它们的存在。

我们还将希望有一种方法使流程自我告知,以便随着时间的推移不断广播。 设置重传间隔的时间实际上不是一个简单的决定-它与网络拓扑,延迟,可变性等有关(您实际上可能偶尔ping对等端并根据时延开发启发式方法,丢弃对等端似乎没有反应,依此类推-但我们不会在这里陷入疯狂)。 在这里,我将其设置为1秒钟,因为对于人类观察系统来说,这是一个易于理解的间隔。

请注意,以下所有内容都是异步的。

接口...

insert(Pid, Message) ->
    gen_server:cast(Pid, {insert, Message}).

relay(Pid, ID, Message) ->
    gen_server:cast(Pid, {relay, ID, Message}).

greet(Pid) ->
    gen_server:cast(Pid, {greet, self()}).

make_introduction(Pid, PeerPid) ->
    gen_server:cast(Pid, {make_introduction, PeerPid}).

最后的功能将作为系统测试人员的方式,使其中一个进程在某个目标Pid上调用greet/1 ,以便他们开始构建对等网络。 在现实世界中,通常会发生一些稍微不同的事情。

在我们的gen_server回调中,用于接收演员表,我们将得到:

handle_cast({insert, Message}, State) ->
    NewState = do_insert(Message, State);
    {noreply, NewState};
handle_cast({relay, ID, Message}, State) ->
    NewState = do_relay(ID, Message, State),
    {noreply, NewState};
handle_cast({greet, Peer}, State) ->
    NewState = do_greet(Peer, State),
    {noreply, NewState};
handle_cast({make_introduction, Peer}, State) ->
    NewState = do_make_introduction(Peer, State),
    {noreply, NewState}.

很简单的东西。

上面我提到过,我们需要一种方法让这个东西告诉自己在延迟后重新发送。 为此,我们将在使用erlang:send_after/3延迟后向“ redo_relay”发送一条裸消息,因此我们将需要handle_info / 2来处理它:

handle_info({redo_relay, ID, Message}, State) ->
    NewState = do_relay(ID, Message, State),
    {noreply, NewState}.

消息位的实现是很有趣的部分,但是这都不是很难的。 原谅下面的do_relay/3可能更简洁,但我是在我头顶上方的浏览器中编写的,所以...

do_insert(Message, State = #s{peers = Peers, digest = Digest}) ->
    MessageID = zuuid:v1(),
    NewDigest = maps:put(MessageID, 1, Digest),
    ok = broadcast(Message, Peers),
    ok = schedule_resend(MessageID, Message),
    State#s{digest = NewDigest}.

do_relay(ID,
         Message,
         State = #s{peers = Peers, digest = Digest, dead = Dead}) ->
    case maps:find(ID, Digest) of
        {ok, Count} when Count >= 10 ->
            NewDigest = maps:remove(ID, Digest),
            NewDead = sets:add_element(ID, Dead),
            ok = broadcast(Message, Peers),
            State#s{digest = NewDigest, dead = NewDead};
        {ok, Count} ->
            NewDigest = maps:put(ID, Count + 1),
            ok = broadcast(ID, Message, Peers),
            ok = schedule_resend(ID, Message),
            State#s{digest = NewDigest};
        error ->
            case set:is_element(ID, Dead) of
                true ->
                    State;
                false ->
                    NewDigest = maps:put(ID, 1),
                    ok = broadcast(Message, Peers),
                    ok = schedule_resend(ID, Message),
                    State#s{digest = NewDigest}
            end
    end.

broadcast(ID, Message, Peers) ->
    Forward = fun(P) -> relay(P, ID, Message),
    lists:foreach(Forward, Peers).

schedule_resend(ID, Message) ->
    _ = erlang:send_after(1000, self(), {redo_relay, ID, Message}),
    ok.

现在我们需要社交活动...

do_greet(Peer, State = #s{peers = Peers}) ->
    case lists:member(Peer, Peers) of
        false -> State#s{peers = [Peer | Peers]};
        true  -> State
    end.

do_make_introduction(Peer, State = #s{peers = Peers}) ->
    ok = greet(Peer),
    do_greet(Peer, State).

那么,那里所有可怕的非类型化的东西都做了什么呢?

它避免了任何死锁的可能性。 死锁在对等系统中是如此致命的原因是,每当您有两个相同的进程(或参与者,或任何类似对象)同步通信时,您就创建了一个潜在死锁的教科书。

现在,当A现在有死锁时, A的同步消息都指向BB的同步消息又指向A 没有办法创建相同的进程,这些进程彼此同步调用而不产生潜在的死锁。 在大规模并发系统中,几乎可以肯定会发生的任何事情最终都会发生,因此您迟早会遇到这种情况。

八卦之所以被认为是异步的,是有原因的:它是一种处理草率,不可靠,低效的网络拓扑的草率,不可靠,低效的方法。 尝试进行呼叫而不是强制转换,不仅无法实现八卦风格的消息中继的目的,而且还会使您陷入不可能的死锁区域,从而使协议的性质从异步更改为同步。

Genser.call默认超时为5000毫秒。 因此,可能发生的情况是,参与者的消息队列中充满了数百万条消息,并且在到达call主叫参与者已超时。

您可以使用try...catch处理超时:

try do
  c = RumourActor.get_message(recipient, [])
catch
  :exit, reason ->
    # handle timeout

现在,被调用的 actor最终将获得call消息并做出响应,这将作为意外消息出现在第一个进程中。 您需要使用handle_info处理。 因此,一种方法是忽略catch块中的错误,并从handle_info发送谣言。

另外,如果有很多进程在等待5秒钟后才继续前进,则会大大降低性能。 可以故意减少超时并处理handle_info的答复。 这将减少到使用cast和处理来自其他进程的答复。

您的阻止呼叫需要分为两个非阻止呼叫。 因此,如果A对B进行阻塞呼叫,而不是等待答复,则A可以要求B在给定地址(A的地址)上发送其状态,然后继续前进。 然后,A将单独处理该消息,并在必要时回复。

 A.fun1():
   body of A before blocking call
   result = blockingcall()
   do things based on result

需要分为:

 A.send():
   body of A before blocking call
   nonblockingcall(A.receive) #A.receive is where B should send results
   do other things

 A.receive(result):
   do things based on result

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM