Search code examples
processelixirmonitor

handle_info :DOWN is not called after monitored process dies


I am having an issue with Process.monitor/1. My initial use case was to monitor Phoenix Channel and do some cleanup after it dies. However, I didn't manage to set it up in Phoenix and decided to test it out with pure GenServers.

So, I have a simple GenServer and I want to track when it dies:

defmodule Temp.Server do
  use GenServer

  def start_link(_), do: GenServer.start_link(__MODULE__, %{})

  def init(args) do
    Temp.Monitor.monitor(self())
    {:ok, args}
  end
end

And another GenServer that monitors:

defmodule Temp.Monitor do
  use GenServer
  require Logger

  def start_link(_) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def monitor(pid) do
    Process.monitor(pid)
  end

  def handle_info({:DOWN, ref, :process, _, _}, state) do
    Logger.info("DOWN")

    {:noreply, state}
  end
end

So, if I understand correctly, Process.monitor will start monitoring the Temp.Server process, and should call the handle_info matching :DOWN when Server process dies. If I try it in iex:

iex> {_, pid} = Temp.Server.start_link([])
{:ok, #PID<0.23068.3>}                    
iex> Process.exit(pid, :kill)             
true     

I expect handle_info being called from Monitor module and logging "DOWN", however that doesn't happen. What am I doing wrong? I assume it doesn't work because I call to monitor from Server process Temp.Monitor.monitor(self()), but I just can't figure out how else should I do it.


Solution

  • When you call the Temp.Monitor.monitor/1 method, it's still running in Temp.Server's own process, not Temp.Monitor's. That means the :DOWN message is sent to Temp.Server when Temp.Server dies, which is redundant.

    What you want to do is, pass the pid of your server process to Temp.Monitor and have it call the Process.Monitor method from it's own process so it can monitor it. That can only happen from one of the GenServer callbacks.

    You can do that by moving your implementation in to handle_call/3 or handle_cast/3:

    defmodule Temp.Monitor do
      use GenServer
      require Logger
    
      def start_link(_) do
        GenServer.start_link(__MODULE__, [], name: __MODULE__)
      end
    
      def monitor(pid) do
        GenServer.cast(__MODULE__, {:monitor, pid})
      end
    
      def handle_cast({:monitor, pid}, state) do
        Process.monitor(pid)
        {:noreply, state}
      end
    
      def handle_info({:DOWN, ref, :process, _, _}, state) do
        Logger.info("DOWN")
    
        {:noreply, state}
      end
    end