smedegaard
Graceful shutdown of supervised GenServer
I’ve got an app that connects to a number of TCP sockets using Ranch.
The supervision tree looks like this:
|-------|
| APP |
|-------|
|
▼
|---------------------|
| Manager(Supervisor) |
|---------------------|
| | |
▼ ▼ ▼
|-------||-------||----------------|
| Sup 1 || Sup 2 || Sup N |
|-------||-------||----------------|
| |
▼ ▼
|------------| |-------------|
| TCP Reader | | Other Child |
|------------| |-------------|
For each connection I start a GenServer in a supervisor… The GenServer init\1 calls connect\1.
def connect(config) do
case :ranch_tcp.connect(config.ip, config.port, []) do
{:ok, socket} ->
{:ok, socket}
error = {:error, :econnrefused} ->
Logger.error("Connection refused to #{inspect(config)}. Shutting down Reader")
error
error = {:error, _error} ->
Logger.error(
"CTC Socket failed to connect to #{inspect(config)}. Shutting down Reader"
)
error
end
end
What I want to do is let the Supervisor and its children die gracefully if :ranch_tcp.connect\2 returns {:error, :econnrefused}
The GenServer init looks like this
@impl true
def init(config) do
case connect(config) do
{:ok, socket} ->
#nice, start reading
{:ok, %{:config => config, socket: socket}}
{:error, :econnrefused} ->
#too bad. Close self and Supervisor gracefully
exit(:normal)
{:error, error} ->
#What the what! Try again X times
{:stop, error}
end
end
Observed behaviour
If the Genserver init\1 does not return {:ok, state} the error is propagated all the way to the Application, and it quits.
Starting ctc socket app
12:25:25.694 [error] Connection refused to %Server{ctc_name: :down_tcp_server, ip: {127, 0, 0, 1}, port: 5555, recv_buffer: 0}. Shutting down CtcReader
12:25:25.697 [info] Application ctc_socket exited: CtcSocket.Application.start(:normal, []) returned an error: shutdown: failed to start child: Supervision.Manager
** (EXIT) shutdown: failed to start child: :down_tcp_server_supervisor
** (EXIT) shutdown: failed to start child: :down_tcp_server_reader
** (EXIT) normal
** (Mix) Could not start application ctc_socket: CtcSocket.Application.start(:normal, []) returned an error: shutdown: failed to start child: Supervision.Manager
** (EXIT) shutdown: failed to start child: :down_tcp_server_supervisor
** (EXIT) shutdown: failed to start child: :down_tcp_server_reader
** (EXIT) normal
Marked As Solved
smedegaard
I read that having a GenServer monitor the shutdown of other GenServers is a common pattern.
But it seemed strange to me to have a GenServer to help my Supervisor supervising…
I ended up passing the Supervisor’s pid as a init arg to the Genservers and if I got {:error, :econnrefused} when trying to connect to the TCP socket, I close the Supervisor with Supervisor.stop\1
@impl true
def handle_info(:connect, state = %{:config => config, :supervisor => supervisor}) do
IO.puts("connecting")
case connect(config) do
{:ok, socket} ->
Process.send_after(self(), :read_data, 1)
new_state = Map.put(state, :socket, socket)
{:noreply, new_state}
{:error, :econnrefused} ->
:ok = Supervisor.stop(supervisor)
{:noreply, state}
end
end
On all other errors I let it crash 
Feel free to suggest better patterns to this 
Also Liked
amnu3387
You’ll want to return either {:stop, :normal} or :ignore from the init so that the Supervisor ignores the child otherwise it considers it to be crashing. I think in this case :ignore describes better its functionality. I think if you do {:stop, :normal} you’ll also have to change the GenServer restart strategy to be :transient (meaning it will only be restarted by the Supervisor in case it exits with something else than :normal or through :shutdown, or shutdown tuple, by using use GenServer, restart: :transient).







