Graceful shutdown of GenServer
To increase chances of the terminate
callback being invoked, the server process should trap exits. However, even with that, the callback might not be invoked in some situations (e.g. when the process is brutally killed, or when it crashes itself). For more details see here.
As mentioned, if you want to politely shutdown your system, you should invoke :init.stop
, which will recursively shutdown the supervision tree causing terminate
callbacks to be invoked.
As you noticed, there is no way of catching abrupt BEAM OS process exits from within. It's a self-defining property: the BEAM process terminates suddenly, so it can't run any code (since it terminated) ð. Hence, if BEAM is brutally terminated, the callback will not be invoked.
If you unconditionally want to do something when BEAM dies, you need to detect this from another OS process. I'm not sure what's your exact use case, but assuming you have some strong needs for this, then running another BEAM node, on the same (or another) machine, could work here. Then you could have one process on one node monitoring another process on another node, so you can react even if BEAM is brutally killed.
However, your life will be simpler if you don't need to unconditionally run some cleanup logic, so consider whether the code in terminate
is a must, or rather a nice-to-have.
If your trying to get it to work in iex
and Process.flag(:trap_exit, true)
is not working, make sure you are using GenServer.start
instead of GenServer.start_link
otherwise the shell process will crash and the trapping won't matter.
Here's 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 a trapped exit call
iex> {:ok, pid} = Server.start()
iex> Process.exit(pid, :something_bad)
I can suggest you two solutions.
The first one is mentioned in docs.
Note that a process does NOT trap exits.
You have to make your gen server process trap exits. To do this:
Process.flag(:trap_exit, true)
This makes your process call terminate/2
upon exit.
But another solution, is to hand over this initialization to the upper supervisor. Then have supervisor pass the external application reference to gen server. But here, you don't have a terminate
-like callback to exit external application if necessary. The external application will just be killed, when supervisor stops.