How to monitor parent process and handle its death (with having child state in handler)?

Hi all,
Assume I have two processes: Parent and Child, Child is a GenServer. Parent launches Child, and Child must react when Parent dies.
Normally, I would start Child with start_link, set the Process.flag(:trap_exit, true) and just handle the message in terminate. But I need the Child state on the exit handler function, which is not provided with terminate.

I managed to do it, but I am not sure my approach is the best available - I am sure there is a better way to do it :slight_smile: below are essential parts of my code.

Parent runs the child with the simple start function, passing own PID:

{:ok, pid} = Child.start({%{}, self()})

Child starts monitoring the parent on initialization. I added check if the parent is still alive in case it dies in the meantime:

def init({state, parent_pid}) do
  if Process.alive?(parent_pid) do
     Process.monitor(parent_pid)
  else
     Process.exit(self(), :normal)
  end
  {:ok, state}
end

And now I can handle the :DOWN message from the monitor:

def handle_info({:DOWN, _ref, :process, _pid, {_reason, _state}}, state) do
  # ... all the stuff needed to run before commit suicide
  parent_died(state)
  Process.exit(self(), :normal)
end

It works, and I have an access to the Childs state in parent_died/1 function. But is it a good approach to archive it? I especially don’t like using Process.exit/2 here. Normally OTP is more elegant than that…

2 Likes

You should be using supervisors here, they exist to handle process life cycles. I recommend going through the Mix getting started guides as they cover a basic example. From there the Elixir in Action book has great coverage of this stuff.

1 Like

Normally I would use the supervisor, but I could not find out how to handle parent death from the child, with having an access to the child state. Is it possible with the supervisor tree and how?

1 Like

There’s a bit of an X Y problem going on. You’re trying to “make the child handle the death of the parent”. This however is an implementation detail of a bigger problem. What is the problem you’re trying to solve? Then we can talk about process architectures that make that happen.

1 Like

The big picture:

I am creating Drab, the library to allow access to browser resources directly from Phoenix (github). It provides the API to launch JS, manipulate DOM, etc - not important in this case. What is important, Drab handles (on Phoenix side) callbacks like page_loaded or connected. You may write your own function to handle this.

What I want to archive is to provide the disconnected callback, which is launched every time browser disconnects (network, navigate away, closing browser). In Phoenix, when you disconnect, Phoenix.Channel exits, and this is my opportunity to catch it.

Architecture:

It is almost exactly the same like in the example which I gave in the first post.
Drab is a GenServer started from Phoenix.Channel (so Channel is a Parent for Drab) after join (1 Drab for 1 Channel):

defmodule Drab.Channel do
  use Phoenix.Channel

  def join("drab:" <> url_path, _, socket) do
    # socket already contains controller and action
    socket_with_path = socket |> assign(:url_path, url_path)

    {:ok, pid} = Drab.start({%{}, self(), Drab.get_commander(socket)})
    socket_with_pid = assign(socket_with_path, :drab_pid, pid)

    {:ok, socket_with_pid}
  end

Child:

defmodule Drab do
  use GenServer

  def init({store, channel_pid, commander}) do
    if Process.alive?(channel_pid) do
      Process.monitor(channel_pid)
    else
      Logger.error("Socket died before starting Drab process.")
      Process.exit(self(), :normal)
    end
    {:ok, {store, commander}}
  end

and then handle the Parent (Channel) exits, and run the callback handler (if it is defined):

  def handle_info({:DOWN, _ref, :process, _pid, {_reason, _state}}, {store, commander}) do
    if commander.__drab__().ondisconnect do
      :ok = apply(commander, 
            drab_config(commander).ondisconnect, 
            [store])
    end
    Process.exit(self(), :normal)
    {:noreply, {store, commander}}
  end

As you can see, I pass the GenServer state (store) to the disconnect handler. This is a clue of the issue - without this, I could simply run Drab with start_link, set the Process.flag(:trap_exit, true) and just handle the message in terminate. Or run it supervised by Channel. Unfortunately, you don’t have the process state on :EXIT handlers.

1 Like

I’m a bit confused here. If you start a GenServer with start_link, and trap exits in the child process, then child’s terminate will be invoked if the parent terminates, and you’ll have the access to the child state:

iex> defmodule Child do
  use GenServer

  def start_link(), do: GenServer.start_link(__MODULE__, nil)

  def init(_) do
    Process.flag(:trap_exit, true)
    {:ok, :some_state}
  end

  def terminate(_reason, state) do
    IO.puts "Child terminating with the state #{inspect state}"
  end
end

iex> spawn(fn -> Child.start_link() end)
Child terminating with the state :some_state

Another option to consider is to trap exits in the channel process and handle disconnection in the terminate callback, which could allow you to completely remove the child process.

3 Likes

Hi Saša,
You are absolutely right! I must be blind or just stupid, I have not seen the state in terminate before. There is no excuse for me.

Thanks a million! Case closed.

1 Like