Search code examples
servertimeouterlangerlang-otperlang-shell

Timeout issue facing on creating new shortcode Erlang


I was trying to implement an emoji server and write codes for it. But when I test it in the terminal it is showing timeout error. For checking the code please check the alias function for it:

-spec alias(Pid :: pid(), Short1 :: shortcode(), Short2 :: shortcode()) -> ok | {error, Reason :: term()}.
alias(Pid, Short1, Short2) when is_pid(Pid), is_list(Short1), is_list(Short2) ->
    Pid ! {alias, Short1, Short2},
    receive
        {ok, Short2} -> ok;
        {error, Reason} -> {error, Reason}
    after 1000 -> {error, timeout}
    end.

emoji_loop(State = #emoji_state{}) ->
    receive
        {alias, Short1, Short2} ->
            io:format("Received alias: Short1 = ~p, Short2 = ~p~n", [Short1, Short2]),
            case dict:find(Short1, State#emoji_state.shortcodes) of
                {ok, _} ->
                    case dict:find(Short2, State#emoji_state.aliases) of
                        {ok, _} -> emoji_loop(State);
                        error ->
                            NewAliases = dict:store(Short2, Short1, State#emoji_state.aliases),
                            emoji_loop(State#emoji_state{aliases = NewAliases})
                    end;
                error -> emoji_loop(State)
            end;
    end.

I have added io:format to debug the code and it seems my new_shortcode function has the issue, but I could not able to find out the issue.

Here is the input that I entered into the terminal:

{ok, Pid} = emoji:start([{"happy", <<"😄">>}, {"sad", <<"😢">>}]).

and then received the output like this:

{ok,<0.92.0>}

Then again entered another input

emoji:new_shortcode(Pid, "dollar", <<"💵"/utf8>>).

and received a message like this:

Received new_shortcode: Short = "dollar", Emo = <<240,159,146,181>>

with timeout error {error,timeout}.

How can I run the create shortcode function perfectly?

Thank you for the clarification again but still I am struggling with the stop function. Here is my code for stop function in the loop.

{stop, Pid} -> 
        if
            Pid =:= self() ->
                exit(Pid);
            true ->
                emoji_loop(State)
        end

In this stop function I need to stop the server when write the command emoji:stop(Pid). After that the server's commands for emoji will not work and give errors. Here is the stop main function where pass the Pid as you instructed.

-spec stop(Pid :: pid()) -> ok | {error, Reason :: term()}.
stop(Pid) when is_pid(Pid) ->
    Pid ! {stop, self()},
    receive
        ok -> ok;
        {error, Reason} -> {error, Reason}
    after 5000 ->
        {error, timeout}
    end.

Solution

  • Looking at the new_shortcode/3 function, we see that it sends a message to Pid and then expects a response, but it fails with {error, timeout} because it never gets that response:

    new_shortcode(Pid, Short, Emo) when is_pid(Pid), is_binary(Emo), is_list(Short) ->
        Pid ! {new_shortcode, Short, Emo},
        receive
            {ok, Short} -> ok;
            {error, Reason} -> {error, Reason}
        after 1000 -> {error, timeout}
        end.
    

    The Pid here is the process running the emoji_loop/1 function, which receives commands in the form of tuples and carries them out by changing the loop state. But what the emoji_loop/1 function doesn't do is send a message back to the process sending the command, which is why a function like new_shortcode/3 times out waiting for a reply.

    To fix it, send the caller's pid to emoji_loop/1 so it can reply. This means changing new_shortcode/1 to add self() into the command tuple it sends to emoji_loop/1:

    new_shortcode(Pid, Short, Emo) when is_pid(Pid), is_binary(Emo), is_list(Short) ->
        Pid ! {new_shortcode, Short, Emo, self()},
        receive
            {ok, Short} -> ok;
            {error, Reason} -> {error, Reason}
        after 1000 -> {error, timeout}
        end.
    

    We then have to change emoji_loop/1 to send a reply back to the caller. For example, change the handling of new_shortcode like this:

            {new_shortcode, Short, Emo, Pid} ->
                io:format("Received new_shortcode: Short = ~p, Emo = ~p~n", [Short, Emo]),
                NewShortCodes = case dict:find(Short, State#emoji_state.shortcodes) of
                    {ok, _} -> State#emoji_state.shortcodes;
                    error ->
                        dict:store(Short, Emo, State#emoji_state.shortcodes)
                end,
                Pid ! {ok, Short},
                emoji_loop(State#emoji_state{shortcodes = NewShortCodes});
    

    With this change and a similar change for lookup, we can see it works as expected:

    3> {ok, Pid} = emoji:start([{"happy", <<"😄">>}, {"sad", <<"😢">>}]).
    {ok,<0.138.0>}
    4> emoji:new_shortcode(Pid, "dollar", <<"💵"/utf8>>).
    Received new_shortcode: Short = "dollar", Emo = <<240,159,146,181>>
    ok
    5> emoji:lookup(Pid, "dollar").
    Received lookup: Short = "dollar"
    {ok,<<240,159,146,181>>}
    

    We have to make similar changes to all command functions that expect replies from the emoji_loop/1 process.

    I strongly suggest you to look into using a gen_server rather than rolling your own server, as it easily handles everything you're trying to do here and also helps with error handling and with process management and cleanup.

    Update: the question was revised to add a specific question about emoji:stop/1, so I'm updating my answer to address it:

    The original stop handler in emoji_loop/1 simply exited:

        stop ->
            exit(normal)
    

    To address the timeout problem, you've modified it to be:

        {stop, Pid} -> 
            if
                Pid =:= self() ->
                    exit(Pid);
                true ->
                    emoji_loop(State)
            end
    

    But there are two problems with this change:

    1. Pid will never be self() because emoji_loop/1 doesn't send itself messages, so the first clause of the if just never happens.
    2. Having emoji_loop/1 call itself recursively as in the true clause means that the loop process keeps running rather than stopping as requested.

    To fix these issues, you have to fix the stop handler just the same as the new_shortcode handler was fixed above: pass the caller's pid with the command, and have the command handler send a reply back to the caller. If we go back to the original stop handler and make these changes, we get:

        {stop, Pid} ->
            Pid ! ok,
            exit(normal)
    

    And the stop/1 function has to be changed to send its pid with the command:

    stop(Pid) when is_pid(Pid) ->
        Pid ! {stop, self()},
        receive
            ok -> ok;
            {error, Reason} -> {error, Reason}
        after 1000 -> {error, timeout}
        end.
    

    We can test stop/1 to verify that it actually stops the loop process:

    2> {ok, Pid} = emoji:start([{"happy", <<"😄">>}, {"sad", <<"😢">>}]).
    {ok,<0.117.0>}
    3> is_process_alive(Pid).
    true
    4> emoji:stop(Pid).
    ok.
    5> is_process_alive(Pid).
    false