zakora

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() 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

Marked As Solved

michalmuskala

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.

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

zakora

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 …”.

tty

tty

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.

Last Post!

zakora

zakora

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 …”.

Where Next?

Popular in Questions Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement