Unsupervised gen_server doesn't call terminate when it receives exit signal
Asked Answered
S

2

7

gen_server documentation on Module:terminate callback says:

Even if the gen_server process is not part of a supervision tree, this function is called if it receives an 'EXIT' message from its parent. Reason is the same as in the 'EXIT' message.

Here is my handle_info and terminate function:

handle_info(UnknownMessage, State) ->
    io:format("Got unknown message: ~p~n", [UnknownMessage]),
    {noreply, State}.

terminate(Reason, State) ->
    io:format("Terminating with reason: ~p~n", [Reason]).

I start this server using gen_server:start. I assume when I call erlang:exit(Pid, fuckoff), it should call terminate callback function. But it shows:

Got unknown message: {'EXIT',<0.33.0>,fuckoff}

Which means it is calling handle_info. But when I call gen_server:stop, everything works as mentioned in documentation. I'm calling my gen_server from shell. Would you please clarify this?

[UPDATE]

Here is source code of decode_msg function inside gen_server. If it receives any 'EXIT' message it should call terminate function:

decode_msg(Msg, Parent, Name, State, Mod, Time, Debug, Hib) ->
    case Msg of
    {system, From, Req} ->
        sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug,
                  [Name, State, Mod, Time], Hib);
    {'EXIT', Parent, Reason} ->
        terminate(Reason, Name, Msg, Mod, State, Debug);
    _Msg when Debug =:= [] ->
        handle_msg(Msg, Parent, Name, State, Mod);
    _Msg ->
        Debug1 = sys:handle_debug(Debug, fun print_event/3,
                      Name, {in, Msg}),
        handle_msg(Msg, Parent, Name, State, Mod, Debug1)
end.

In my case it doesn't call terminate function.

[UPDATE]

When I start gen_server using gen_server:start_link(), sending an exit signal using erlang:exit(Pid, Reason) will result in calling terminate call back function which is an expected behaviour. It seems there is a difference in interpreting an exit signal whether a process is linked to its parent or not.

Seroka answered 10/9, 2016 at 21:6 Comment(2)
Did you set the trap_exit flag of process to true somewhere in the code?Jasmin
Yes. Inside init function.Seroka
J
3

Short answer:

If you call the exit/2 function from inside the gen_server actor, it behaves as expected based on the documentation and the terminate/2 callback will be called.

Long answer:

When you send the exit message from the shell, the Parent value of exit tuple is set to the shell process id, on the other hand when you start the gen_server process form shell its Parent value is set to its own process id, not the shell process id, therefore when it gets the exit message it doesn't match the second clause of the receive block in decode_msg/8 function so the terminate/6 function is not called and finally the next clause is matched which is calling handle_msg/5 function.

Recommendation:

For getting the terminate/3 callback called even by sending an exit message to the gen_server process, you can trap the exit message in handle_info/2 and then return with stop tuple as follows:

init([]) ->
    process_flag(trap_exit, true),
    {ok, #state{}}.

handle_info({'EXIT', _From, Reason}, State) ->
    io:format("Got exit message: ~p~n", []),
    {stop, Reason, State}.

terminate(Reason, State) ->
    io:format("Terminating with reason: ~p~n", [Reason]),
    %% do cleanup ...
    ok.
Jasmin answered 25/9, 2016 at 8:41 Comment(3)
I got it. I can see that in observer. Why the parent is set to its own process id when I start a process in shell? Any special reason?Seroka
@MajidAzimi: Based on the code, the gen:get_parent/0 which is responsible for finding the parent of gen_server looks for the first item of $ancestors key in the process dictionary. As the gen_server process is an OTP process which is started with proc_lib, it should have the calling actor process ID in the $ancestors dictionary, which is the shell process ID in this case and set it as parent, but it doesn't work as expected. I need more time to find whether it's a bug or something else.Jasmin
As far as I can check, when I start an OTP process using gen_*:start the parent pid is set to newly created process itself. When starting with gen_*:start_link it is set to the first element of $ancestors. It doesn't matter if the actual parent is shell or other gen_*. In both case $ancestors contains all parents up to the root. gen_*:start_link is using first element of $ancestors to get the parent(which is the expected behaviour) but for some reason gen_*:start uses its own pid as parent.Seroka
S
0

When you start your gen_server, its a simple process, so, erlang:exit/1 or erlang:exit/2 work as expected.

  • If Pid is not trapping exits, Pid itself exits with exit reason Reason.
  • If Pid is trapping exits, the exit signal is transformed into a message {'EXIT', From, Reason} and delivered to the message queue of Pid.

So, currently your code trap 'EXIT' signal because this one is send like any other message into the mailbox and match handle_info/2 wildcard pattern.

If you want more information about that, you can read gen_server source code and see how it works. You can also find your problem described in this code.

Subinfeudation answered 10/9, 2016 at 21:39 Comment(2)
Let me ask the question this way: gen_server:stop also sends exit signal. It will be converted to 'EXIT' message. How gen_server differentiate this message from my 'EXIT' message (which comes from exit)Seroka
gen_server:handle_info/2 is currently used to trap every signal (even special one). So, if you want to differentiate 'EXIT' signal from other signal, your definition of handle_info need to be different (modifying pattern matching, adding guards...). To be clear, 'EXIT' signal is more like a convention than a standard in Erlang, because its only a simple message.Subinfeudation

© 2022 - 2024 — McMap. All rights reserved.