Skip to content

Instantly share code, notes, and snippets.

@jadeallenx
Last active April 21, 2023 17:13
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save jadeallenx/806fe5506132260574af33e99dadd499 to your computer and use it in GitHub Desktop.
Save jadeallenx/806fe5506132260574af33e99dadd499 to your computer and use it in GitHub Desktop.
When does terminate/2 get called in a gen_server?

When does terminate/2 get called in a gen_server?

This is what the official documentation says about the terminate/2 callback for a gen_server:

This function is called by a gen_server when it is about to terminate. It should be the opposite of Module:init/1 and do any necessary cleaning up. When it returns, the gen_server terminates with Reason. The return value is ignored.

Reason is a term denoting the stop reason and State is the internal state of the gen_server.

Reason depends on why the gen_server is terminating. If it is because another callback function has returned a stop tuple {stop,..}, Reason will have the value specified in that tuple. If it is due to a failure, Reason is the error reason.

If the gen_server is part of a supervision tree and is ordered by its supervisor to terminate, this function will be called with Reason=shutdown if the following conditions apply:

  • the gen_server has been set to trap exit signals, and
  • the shutdown strategy as defined in the supervisor's child specification is an integer timeout value, not brutal_kill.

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

Otherwise, the gen_server will be immediately terminated.

Note that for any other reason than normal, shutdown, or {shutdown,Term} the gen_server is assumed to terminate due to an error and an error report is issued using error_logger:format/2.

What conditions cause this callback to get executed?

Starting

Assuming you have a recent Erlang available on your system (I used 18.3.3 on OS X for these tests) start it and then compile and load the code.

1> c(t).
{ok, t}
2> {ok, Pid} = t:start_link().
{ok,<0.39.0>}
3> Pid ! foo.
Got foo
foo
4> Pid ! die.
Got die
started terminate because die; sleeping for 1 second
die
finished sleeping

=ERROR REPORT==== 22-Jun-2016::13:59:23 ===
** Generic server <0.112.0> terminating 
** Last message in was die
** When Server state == {state}
** Reason for termination == 
** die

Internal to the gen_server

Crashes

These could be caused by syntax errors or other unexpected behaviors. Provoke this case by sending the crash message to the gen_server Pid.

Using the {stop, Reason, State} return value

If you're using the {stop, Reason, State} return value for a handle_info, handle_cast, handle_call implementation, terminate/2 will be called. Provoke this in your test gen_server using die message.

External to the gen_server

{'EXIT', Pid, Reason}

If the parent of the gen_server sends an 'EXIT' message, terminate will be called. Test this by sending Pid ! {'EXIT', self(), test} to a the test gen_server. This is the same message that's sent when a process is told to trap exits sent by its parent.

Signals

Using the process exit signaling mechanism - will this cause a gen_server to execute terminate/2? No it will not. You can test this by using the exit/2 command to send exit(Pid, kill) to brutally kill the test gen_server. In this case, terminate/2 will not be executed.

You can also send other exit reasons like qux or hoge. If you do this and the process is set to trap exits (see line 26) above, then terminate/2 will be called.

Will terminate/2 run to completion?

That depends on if the gen_server is part of a supervision tree and if the gen_server that's being shut down is configured to trap exit signals. Remember that the special kill signal cannot be trapped and is immediate in all cases.

If it is part of a supervision tree and the supervisor's shutdown specification is brutal_kill, the process will be killed immediately using exit(Pid, kill) which is untrappable and immediate as noted above.

If the shutdown specification is an integer, the supervisor will wait for that number of milliseconds before sending a kill signal to its children. If the terminate block runs longer than the shutdown timer in the supervisor's child spec, then it will not complete.

If the terminate block is called for some other reason it should run to completion.

1> c(t_sup).
{ok,t_sup}
2> c(t).
{ok,t}
3> {ok, SupPid} = t_sup:start_link().
{ok,<0.44.0>}
4> [{_,TestPid,_,_}] = supervisor:which_children(SupPid).
[{t,<0.45.0>,worker,[t]}]
5> TestPid ! bar.
Got bar
bar
6> supervisor:terminate_child(SupPid, t).
started terminate because shutdown; sleeping for 10 seconds
ok

Notice there is no finished sleeping output as you would expect from line 52 because the supervisor sent the kill signal after 5000 milliseconds, right in the middle of our sleep operation.

What happens if the terminate function itself crashes?

You must be one of those turtles all the down types. If terminate crashes, then it is logged to the error_logger and the process dies, as usual.

You can test this by sending the exit(Pid, daleks) message to the test gen_server.

%% Run some tests like:
%% c(t).
%% {ok, Pid} = t:start_link().
%% Pid ! foo.
%% Pid ! bar.
%% exit(Pid, normal).
%% Pid ! crash.
-module(t).
-behaviour(gen_server).
%% Required callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
%% API
-export([start_link/0]).
-record(state, {}).
%% API
start_link() ->
{ok, Pid} = gen_server:start_link(?MODULE, [], []),
% unlink ourselves so we don't crash when this Pid dies
% unlink(Pid), % uncomment this if you're testing directly from the shell and not a supervisor.
{ok, Pid}.
%% gen_server
init([]) ->
process_flag(trap_exit, true), % comment this line to stop trapping exits
{ok, #state{}}.
handle_call(_Msg, _From, S) -> {noreply, S}.
handle_cast(_Msg, S) -> {noreply, S}.
handle_info(crash, S) ->
io:format(user, "Got crash~n", []),
error(crash),
{noreply, S};
handle_info(die, S) ->
io:format(user, "Got die~n", []),
{stop, die, S};
handle_info(Msg, S) ->
io:format(user, "Got ~p~n", [Msg]),
{noreply, S}.
terminate(daleks, _S) ->
io:format(user, "started terminate because daleks; but I won't finish.~n", []),
error(exterminate), % Daleks ftw
ok;
terminate(Reason, _S) ->
io:format(user, "started terminate because ~p; sleeping for 10 seconds~n", [Reason]),
timer:sleep(10000),
io:format(user, "finished sleeping~n", []),
ok.
code_change(_Old, S, _Extra) -> {ok, S}.
-module(t_sup).
-behaviour(supervisor).
%-define(SHUTDOWN, brutal_kill). % Uncomment this line to test brutal_kill
-define(SHUTDOWN, 5000). % Wait 5 seconds before killing
%% supervisor callback
-export([init/1]).
%% API
-export([start_link/0]).
start_link() -> supervisor:start_link(?MODULE, []).
init([]) ->
ChildSpec = {t, {t, start_link, []}, temporary, ?SHUTDOWN, worker, [t]},
{ok, {{one_for_one, 10, 10}, [ChildSpec]}}.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment