Graceful shutdown of GenServer

I am writing an Elixir application with GenServer that launches an external application at startup and closes it, and performs another cleanup on exit. I added boot functionality to the init/1 callback and cleanup code in terminate/2 .

The init code works fine when starting GenServer, and the terminate method is called when the :stop signal :stop sent manually, but in cases of unexpected exits and interrupts (as in the case of pressing Ctrl + C) in IEx, the termination code is not called.


Currently, I have switched to tons of forums, blog posts, and documentation, including:

From Elixir Docs - GenServers :

If a GenServer receives an output signal (this is not :normal ) from any process when it does not catch the outputs, it will exit abruptly for the same reason and therefore do not call terminate/2 . Note that the process does NOT catch the default outputs and sends an output signal when the associated process terminates or its node is disabled.

Therefore, it is not guaranteed that terminate/2 is called when a GenServer terminates. For such reasons, we usually recommend that important cleaning rules should be followed in separate processes, either using monitoring or the links themselves.

but I have no idea how to get :init.stop , linked processes or anything else to work with this (since this is my first time with GenServers).


This is my code:

 defmodule MyAwesomeApp do use GenServer def start do GenServer.start_link(__MODULE__, nil) end def init(state) do # Do Bootup stuff IO.puts "Starting: #{inspect(state)}" {:ok, state} end def terminate(reason, state) do # Do Shutdown Stuff IO.puts "Going Down: #{inspect(state)}" :normal end end MyAwesomeApp.start 
+7
elixir gen-server
source share
3 answers

To increase the likelihood of a terminate callback, the server process must catch the outputs. However, even with this, the callback cannot be called in some situations (for example, when the process is brutally killed or when it itself crashes). See here for more details.

As already mentioned, if you want to politely shut down your system, you must call :init.stop , which will recursively terminate the control tree that calls the terminate callbacks.

As you noticed, there is no way to catch the sudden exits of the BEAM OS process from the inside. This is a self-defining property: the BEAM process ends abruptly, so it cannot run any code (since it is completed) πŸ™‚. Therefore, if BEAM is brutally terminated, the callback will not be called.

If you unconditionally want to do something when BEAM dies, you need to discover it from another OS process. I am not sure what your specific use case is, but provided that you have some strong needs for this, then running another BEAM node on the same (or different) machine may work here. Then you can have one process on one node monitoring another process on another node, so you can respond even if BEAM is brutally killed.

Nevertheless, your life will be simpler if you do not need to unconditionally run some cleaning logic, so think about whether the code in terminate mandatory, or rather pleasant.

+4
source share

I can offer you two solutions.

The first is mentioned in the documents.

Please note that the process does not block the outputs.

You have to set your traps for server exits. For this:

 Process.flag(:trap_exit, true) 

This causes the terminate/2 process to be called upon exit.

But another solution is to pass this initialization to the top supervisor. The supervisor then passes the external link to the gen server. But here you do not have a terminate callback to exit an external application, if necessary. The external application will simply be killed when the dispatcher stops.

+3
source share

If your attempt to make it work in iex and Process.flag(:trap_exit, true) does not work, make sure that you use GenServer.start instead of GenServer.start_link , otherwise the shell process will crash and capture does not matter.

Here is an example:

 defmodule Server do use GenServer require Logger def start() do GenServer.start(__MODULE__, [], []) end def init(_) do Logger.info "starting" Process.flag(:trap_exit, true) # your trap_exit call should be here {:ok, :some_state} end # handle the trapped exit call def handle_info({:EXIT, _from, reason}, state) do Logger.info "exiting" cleanup(reason, state) {:stop, reason, state} # see GenServer docs for other return types end # handle termination def terminate(reason, state) do Logger.info "terminating" cleanup(reason, state) state end defp cleanup(_reason, _state) do # Cleanup whatever you need cleaned up end end 

In iex you should now see the captured exit call

 iex> {:ok, pid} = Server.start() iex> Process.exit(pid, :something_bad) 
0
source share

All Articles