zakora
Trying to understand GenServer terminate/2 behavior when trapping exit
Out of curiosity I started to learn how to gracefully shutdown an Elixir application, but I have some trouble understanding the behavior of GenServer terminate/2.
So far I have understood that:
- calling
System.stop(),:init.stop()orc:q()from IEx will properly shutdown the application and the Elixir VM. - calling
Application.stop(:myapp)from IEx will properly shutdown the application but keep the Elixir VM alive.
If one wants to do some clean-up in a GenServer during the shutdown, one must use Process.flag(:trap_exit, true) in the init/1 of said GenServer and perform the clean-up in terminate/2.
What I am not sure of:
Calling Process.exit(pid, :shutdown) (where pid is the GenServer pid) will effectively send an info message with 'EXIT' to the GenServer but terminate/2 will not be called and the GenServer is still alive.
Is this because the exit signal is not sent from the parent but from IEx?
The documentation states:
terminate/2is called if a callback (exceptinit/1) does one of the following:
- (…)
- the
GenServertraps exits (usingProcess.flag/2) and the parent process sends an exit signal
What I don’t understand:
Calling Supervisor.stop(Myapp.Supervisor) will call terminate/2 but no info message is received by the GenServer. Since Process.exit(child_pid, :shutdown) is called automatically in this case, I was expecting to also receive an info message.
The documentation states:
The termination happens by sending a shutdown exit signal, via
Process.exit(child_pid, :shutdown), to the child process and then awaiting for a time interval for the child process to terminate.
Example:
The following mix.exs shows that:
- calling
Process.exit(pid, :shutdown)from IEx will not call the GenServerterminate/2, the GenServer is still alive. - calling
Supervisor.stop(Myapp.Supervisor)from IEx will not send an info message to the GenServer.
defmodule Myapp.MixProject do
use Mix.Project
def project do
[
app: :myapp,
version: "0.1.0",
elixir: "~> 1.7",
start_permanent: Mix.env() == :prod,
]
end
def application do
[
extra_applications: [:logger],
mod: {Myapp.Application, []}
]
end
end
defmodule Myapp.Application do
use Application
def start(_type, _args) do
children = [
{Myapp.Worker, name: Myapp.Worker},
]
opts = [strategy: :one_for_one, name: Myapp.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule Myapp.Worker do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, :ok, opts)
end
def init(:ok) do
Process.flag(:trap_exit, true)
{:ok, nil}
end
def handle_call(:ping, _from, state) do
{:reply, :pong, state}
end
def handle_info(msg, state) do
IO.puts "message received by #{__MODULE__}: #{inspect msg}"
{:noreply, state}
end
def terminate(reason, _state) do
IO.puts "#{__MODULE__}.terminate/2 called wit reason: #{inspect reason}"
end
end
Marked As Solved
michalmuskala
One of the elements of the contract of the OTP special process (and GenServer is such a process) is to die when it receives an EXIT message from parent. That’s what you’re seeing - the parent EXIT message is intercepted by GenServer internals and causes a suicide of the server.
http://erlang.org/doc/design_principles/spec_proc.html
If the special process is set to trap exits and if the parent process terminates, the expected behavior is to terminate with the same reason:
Also Liked
zakora
tty
If one wants to do some clean-up in a GenServer during the shutdown, one must use
Process.flag(:trap_exit, true)in theinit/1of said GenServer and perform the clean-up interminate/2.
According to the GenServer docs the one must use Process.flag(:trap_exit, true) in the init/1 is inaccurate. It is one of the 5 possible conditions terminate/2 is called, not the only condition.








