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() or c: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/2 is called if a callback (except init/1 ) does one of the following:

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 GenServer terminate/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
3 Likes

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:

2 Likes

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 .

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.

1 Like

Thanks for your answers!

If I understand correctly, the fact that Process.exit(pid, :shutdown) was called from IEx and not the GenServer parent lead to the observed behaviour.

@tty: you are right, I should have written “one can use …”.

2 Likes