How to monitor LiveViews reliably?

So I have a LiveView that I monitor using the following code:

defmodule IntisyncWeb.LiveViewMonitor do
  @moduledoc """
    Monitors LiveView processes and calls their `unmount` functions when they die
  """
  use GenServer

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg, name: {:global, __MODULE__})
  end

  def init(_) do
    {:ok, %{views: %{}}}
  end

  def monitor(pid, view_module, meta) do
    server_pid = GenServer.whereis({:global, __MODULE__})
    GenServer.call(server_pid, {:monitor, pid, view_module, meta})
  end

  def handle_call({:monitor, pid, view_module, meta}, _, %{views: views} = state) do
    _ref = Process.monitor(pid)
    {:reply, :ok, %{state | views: Map.put(views, pid, {view_module, meta})}}
  end

  def handle_info({:DOWN, ref, :process, pid, reason}, state) do
    {{module, meta}, new_views} = Map.pop(state.views, pid)

    Process.demonitor(ref)

    Task.start(fn -> module.unmount(reason, meta) end)

    {:noreply, %{state | views: new_views}}
  end
end

Then, on the LiveView I do the following:

defmodule IntisyncWeb.HubLive do
  use IntisyncWeb, :live_view

  alias IntisyncWeb.LiveViewMonitor
  alias Intisync.SessionsSupervisor

  def mount(_params, _session, socket) do
    socket = assign(socket, :session_id, nil)

    {:ok, socket}
  end

  def handle_params(%{"id" => session_id}, _uri, socket) do
    if connected?(socket),
      do: handle_connected(session_id, socket),
      else: {:noreply, socket}
  end

  defp handle_connected(session_id, socket) do
    socket = assign(socket, :session_id, session_id)

    if exists_session?(session_id) do
      socket = socket |> put_flash(:error, "Unauthorized") |> redirect(to: "/")
      {:noreply, socket}
    else
      {:ok, pid} = SessionsSupervisor.start_session(session_id)
      LiveViewMonitor.monitor(self(), __MODULE__, {pid})
      {:noreply, assign(socket, :session_server_pid, pid)}
    end
  end

  def unmount(_reason, {session_server_pid}) do
    SessionsSupervisor.close_session(session_server_pid)
  end
end

Also, it is important to note that I do the following on my Router:

defmodule IntisyncWeb.Router do
  use IntisyncWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {IntisyncWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", IntisyncWeb do
    pipe_through :browser

    live "/", LobbyLive
    live "/sessions/:id/remote", RemoteLive

    live_session :hub, root_layout: {IntisyncWeb.Layouts, :hub_root} do
      live "/sessions/:id", HubLive
    end
  end
  end
end

For context, the SessionsSupervisor starts a gen server that contains the state of the session, and I want this session gen server to be terminated when this live view goes down. The reason I do this is because in this sessions you can invite other people that need to read this state, so thats why I have a gen server that stores this shared state :slight_smile:

The problem here is that sometimes, when I refresh the HubLive page, the unmount function doesn’t seem to be called. I expected that when I refresh a page containing a live view the following happens: LiveView terminates → New live view is mounted

So that means, in some browsers when refreshing the page, the live view doesn’t die automatically (At least that is my hypothesis, I don’t know certainly)

Another thing could be that I in the router I put the HubLive under a live_session, and it might be that when navigating from a live view to the same live view under the same live_session that the live view doesn’t die?
The only reason I do this is because HubLive uses a different root layout than the other views and this is the first way I learnt how to assign a different root to a view, so if you know a better way let me know.

And last but not least, maybe I should somehow monitor the socket pid instead of the live view pid? How do I even do that? How do I get the pid of a socket?

It’s more like:

  • user presses “Refresh”
  • browser closes the websocket connection
    • server starts tearing down the connection + cleaning up
  • browser initiates a new websocket connection
    • server starts new connection machinery

The second step is fire-and-forget from the browser’s perspective, so there’s no guarantee that the server is done cleaning up the old stuff when the new request arrives.

I’m not sure what LiveViewMonitor is adding here; a simpler alternative would be for the process started by start_session to directly monitor the caller. You could even build in some fault-tolerance by making the “session” process wait for 30s or so after getting a :DOWN, in case the user’s network connection failed briefly.

1 Like