erlang 为什么gen_server:reply/2在某些示例中有效,而在其他示例中会导致超时

klh5stk1  于 2023-01-28  发布在  Erlang
关注(0)|答案(2)|浏览(196)

在我的代码中,让gen_server:reply在某些情况下(但不是所有情况下)工作有问题,尽管在我看来,代码在结构上从它工作的领域和不工作的领域来看是相似的。我不知道这是由于一些概念上的误解还是gen_server:reply/的不完整。
我已经创建了如下所示的MRE代码(带有EUnit测试,并且都准备好即插即用)。我发现测试函数setup_test()成功,而函数setup_test_move_rs_both_receive()没有成功。后者为mve_rps_game:move/2函数创建了一个“挂起”/超时。
为什么会这样?我该如何应对?

-module(mve_rps_game).
-behaviour(gen_server).
-include_lib("eunit/include/eunit.hrl").

-export([init/1, handle_cast/2, handle_call/3, start/0, setup_game/2, move/2, test_all/0]). 

%------------------------Internal functions to game coordinator module

make_move(Choice, OtherRef, PlayersMap, CurMoveKey, OtherMoveKey) ->
    case maps:get(OtherMoveKey, PlayersMap, badkey) of
        badkey ->            
            PlayersMap2 = maps:put(CurMoveKey, Choice, PlayersMap),
            {noreply, PlayersMap2};
        OtherChoice ->
            io:format(user, "~n Choice, OtherChoice, CurPid, OtherRef: ~w ~w ~w ~w~n",[Choice, OtherChoice, self(), OtherRef]),
            gen_server:reply(OtherRef, Choice),
            {reply, OtherChoice, PlayersMap}           
    end.
%-----------------Init game-coordinator and its handle_call functions

init(_Args) ->    
    PlayersMap = #{}, 
    {ok,PlayersMap}.

handle_call({move, Choice}, From, PlayersMap = #{start:= {P1Ref, P2Ref}}) ->    
    {P1id, _} = P1Ref,
    {P2id, _} = P2Ref,
    {CurId, _} = From,
    case CurId of
        P1id ->            
            make_move(Choice, P2Ref, PlayersMap, p1_move, p2_move);
        P2id ->
            make_move(Choice, P1Ref, PlayersMap, p2_move, p1_move);
        _Other ->
            {reply, {error, not_a_player}, PlayersMap}
    end;

handle_call({set_up, Name}, From, PlayersMap) ->
    case maps:is_key(prev_player, PlayersMap) of
        %Adds req number of rounds as a key containing player name and ref for postponed reply
        false ->           
            PlayersMap2 = maps:put(prev_player,{Name, From}, PlayersMap),
            {noreply, PlayersMap2};
        
        %Sends message back to previous caller and current caller to setup game
        true -> 
            case maps:get(prev_player, PlayersMap, badkey) of                
                {OtherPlayer, OtherRef} ->  
                    gen_server:reply(OtherRef, {ok, Name}),                    
                    PlayersMap2 = maps:remove(prev_player, PlayersMap),                    
                    %Make key start to indicate that game is going on, and with References to two players. 
                    PlayersMap3 = PlayersMap2#{start => {From, OtherRef}}, 
                    {reply, {ok, OtherPlayer}, PlayersMap3};
                _ -> 
                    {reply, error, PlayersMap}
            end        
    end.

handle_cast(_Msg, State) ->
    {noreply, State}.

%------------- CALLS to Rps game -------------------
start() ->
    {ok, Pid} = gen_server:start(?MODULE,[], []),
    {ok, Pid}.

setup_game(Coordinator, Name) -> 
    gen_server:call(Coordinator, {set_up, Name}, infinity).

move(Coordinator, Choice) ->     
    gen_server:call(Coordinator, {move, Choice}, infinity).

%--------------- EUnit Test section ------------

setup_test() ->
    {"Queue up for a game",
     fun() ->
             {ok, CoordinatorPid} = mve_rps_game:start(),
             Caller = self(),
             spawn(fun() -> State = mve_rps_game:setup_game(CoordinatorPid, "player1"),
                            Caller ! State end),
             timer:sleep(1000),             
             {ok, OtherPlayer} = mve_rps_game:setup_game(CoordinatorPid, "player2"), 
             ProcessState = 
                receive             
                    State -> State
                end,
            ?assertMatch(ProcessState, {ok, "player2"}),
            ?assertMatch({ok, OtherPlayer}, {ok, "player1"})            
     end}.

queue_up_test_move_rs_both_receive() ->
    {"Testing that both players recieve answer in rock to scissor",
    fun() ->
        {ok, CoordinatorPid} = mve_rps_game:start(),
        Caller = self(),
        spawn(fun() ->
            {ok, _OtherPlayer} = mve_rps_game:setup_game(CoordinatorPid, "player1"),
            State = mve_rps_game:move(CoordinatorPid, rock),
            Caller ! State
        end),
        timer:sleep(1000),
        {ok, _OtherPlayer} = mve_rps_game:setup_game(CoordinatorPid, "player2"),
        Result2 = mve_rps_game:move(CoordinatorPid, scissor),
        io:format(user, "Result2: ~w~n",[Result2]),    
        ?assertMatch(Result2, rock),
        ProcessState = receive
            State -> State
        end,
        ?assertMatch(ProcessState, win)    
        end}.

test_all() ->
    eunit:test(
      [
        setup_test()
        ,queue_up_test_move_rs_both_receive()
      ], [verbose]).
wgmfuz8q

wgmfuz8q1#

好吧,我很好地理解了,我甚至在我的其他帖子中也没有离这里看到的make_ref()的使用和概念太远,所以问题是基于pid()gen_server中的reference()From在概念上的不完整。
该代码导致超时的原因是gen_server:reply(OtherRef, Choice)使用了一个引用,该引用在以前的gen_server:call/3中保存在gen_server s状态下。因此,它试图回复一个已经应答的呼叫,而新呼叫没有回复,因为新呼叫的引用/标记没有存储。

lmyy7pcs

lmyy7pcs2#

我没有找到解决您的问题的方法,但我可以为您指出代码中出错的地方...
当从handle_call/3的第一个子句调用函数make_move/5时,您传递的是p1_move, p2_movep2_move, p1_move作为最后一个参数,但这些键从未添加到PlayersMap中,因此maps:get/3将始终返回badkey
在这种情况下,您不会回复客户端(即,您返回{noreply, PlayersMap2},并且您不会在任何地方保留来自handle_call/3From......因此,本质上,您不会响应任何对mve_rps_game:move/2的调用。该函数使用infinity作为其gen_server:call/3的超时值,因此......最终测试本身超时。
我不完全确定修复 * 应该 * 是什么,但您可能应该从检查如何/何时在make_move/5(或handle_call/3的第一个子句)中响应客户端开始。

编辑:发现问题

(For此变更的解释,请检查以下备注)
解决此问题的方法是跟踪每个用户收到的最后一封邮件,而不仅仅是第一封邮件。进行以下更改,您将能够解决超时问题。您的测试仍将失败,但原因不同...

case CurId of
        P1id ->            
            make_move(Choice, P2Ref, PlayersMap#{start := {From, P2Ref}}, p1_move, p2_move);
        P2id ->
            make_move(Choice, P1Ref, PlayersMap#{start := {P1Ref, From}}, p2_move, p1_move);
        _Other ->
            {reply, {error, not_a_player}, PlayersMap}
    end;

主要的变化是PlayersMap内的start的调整。事实上,那个项目应该被称为last_message或类似的东西,但我会留给你。

相关问题