When does terminate get called?
In Erlang, in a gen_server
, when does terminate get called? Also, some
messing around with dbg
for tracing.
tl;dr: if your process is trapping exits, terminate
is called for
everything except exit(Pid, kill)
.
Boilerplate
Let’s start with a simple gen_server
. See this
gist for the
boilerplate.
Then, in the Erlang shell:
1> c(example_server).
{ok, example_server}
2> {ok, Pid} = example_server:start_link().
{ok, <0.48.0>}
3> exit(Pid, kill).
** exception exit: killed
There’s nothing surprising there: we sent an exit signal (with kill
) to the
process. It died, which killed the shell process.
Tracing
In order to see if terminate
is called, we’re going to either need some
tracing (via dbg
) or some logging (which means writing some code).
Roger flips a coin
Tracing it is. So, starting from the beginning, we have:
1> c(example_server).
{ok, example_server}
2> dbg:start().
{ok,<0.41.0>}
3> dbg:tracer().
{ok,<0.41.0>}
4> dbg:tp(example_server, []).
{ok,[{matched,nonode@nohost,10}]}
5> dbg:p(all, c).
{ok,[{matched,nonode@nohost,26}]}
6> {ok, Pid} = example_server:start_link().
(<0.33.0>) call example_server:start_link()
(<0.47.0>) call example_server:init([])
{ok,<0.47.0>}
We can turn on a bunch more tracing for that process with the following.
7> dbg:p(Pid, [c,m,p]).
Do not specify all
for the process here…
And we can see that’s working, because:
8> Pid ! hello.
(<0.47.0>) << hello
(<0.47.0>) call example_server:handle_info(hello,undefined)
hello
And, if we send it an exit signal, specifying normal
:
9> exit(Pid, normal)
true
OK, let’s try it the hard way:
10> exit(Pid, kill).
(<0.47.0>) exit killed
(<0.47.0>) unregister example_server
process_flag
But terminate
didn’t get called, because we’re not trapping exits. See
process_flag
. To trap
exits in a gen_server
:
init([]) ->
process_flag(trap_exit, true),
State = undefined,
{ok, State}.
If we’re trapping exits, we see, for example:
9> exit(Pid, normal).
(<0.48.0>) << {'EXIT',<0.33.0>,normal}
(<0.48.0>) call example_server:terminate(normal,undefined)
(<0.48.0>) exit normal
(<0.48.0>) unregister example_server
true
Exit reasons
What about other exit reasons?
exit(Pid, normal)
callsterminate(normal, State)
; no exception.exit(Pid, shutdown)
callsterminate(shutdown, State)
; exception.exit(Pid, {shutdown, whoops})
callsterminate({shutdown, whoops}, State)
; exception.exit(Pid, computer_says_no)
callsterminate(computer_says_no, State)
; exception.exit(Pid, kill)
does not callterminate/2
; exception.
What’s going on here is that:
- The exit signals (apart from
kill
) are converted to{'EXIT', From, Reason}
messages. - The
gen_server
module handles the'EXIT'
messages and callsMod:terminate
, passing the reason. - After
Mod:terminate
has returned, the original reason is passed toexit/1
, which is propagated to linked processes (usually a supervisor of some sort).
Shutdown
OK, so what if the process itself chooses to stop? This is a gen_server
, so
it’s allowed to return {stop, NewState}
from the callback functions.
Add these clauses to the top of handle_call
:
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call({stop, Reason}, _From, State) ->
{stop, Reason, ok, State};
Add these clauses to the top of handle_info
:
handle_info(stop, State) ->
{stop, normal, State};
handle_info({stop, Reason}, State) ->
{stop, Reason, State};
And, now, we can poke it with, for example:
ok = gen_server:call(example_server, {stop, hammer_time}).
Errors
What about if the process runs into a missing function clause? Or if it calls
exit/1
itself?
Yes, terminate
is still called.
Conclusion
At this point, I ran out of things to try, and I’ve come to the conclusion
that, provided you’re trapping exits, terminate
will be called for everything
except exit(Pid, kill)
.