How to detect if a user left a Phoenix channel due to a network disconnect?

I have an Elixir/Phoenix server app and the clients connect through the build in channels system via websockets. Now I want to detect when an user leaves a channel.

Sidenote: I'm using the javascript client library inside a Google Chrome Extension. For this I extracted the ES6 code from Phoenix, transpiled it to javascript, and tweaked it a little so it runs standalone.

Now when I just close the popup, the server immediately triggers the terminate/2 function with reason = {:shutdown, :closed}. There is no kind of close-callback involved on the extension side, so this is great!

But when the client simply looses network connection (I connected a second computer and just pulled out the network plug) then terminate/2 will not trigger.

Why and how do I fix this?

I played around with the timeoutoption of transport :websocket, Phoenix.Transports.WebSocket but this did not work out.

Update: With the new awesome Phoenix 1.2 Presence stuff, this should not be needed anymore.

Answers


The proper way to do this is to not trap exits in your channel, and instead have another process monitor you. When you go down, it can invoke a callback. Below is a snippet to get you started:

# lib/my_app.ex

children = [
  ...
  worker(ChannelWatcher, [:rooms])
]

# web/channels/room_channel.ex

def join("rooms:", <> id, params, socket) do
  uid = socket.assigns.user_id]
  :ok = ChannelWatcher.monitor(:rooms, self(), {__MODULE__, :leave, [id, uid]})

  {:ok, socket}
end

def leave(room_id, user_id) do
  # handle user leaving
end

# lib/my_app/channel_watcher.ex

defmodule ChannelWatcher do
  use GenServer

  ## Client API

  def monitor(server_name, pid, mfa) do
    GenServer.call(server_name, {:monitor, pid, mfa})
  end

  def demonitor(server_name, pid) do
    GenServer.call(server_name, {:demonitor, pid})
  end

  ## Server API

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

  def init(_) do
    Process.flag(:trap_exit, true)
    {:ok, %{channels: HashDict.new()}}
  end

  def handle_call({:monitor, pid, mfa}, _from, state) do
    Process.link(pid)
    {:reply, :ok, put_channel(state, pid, mfa)}
  end

  def handle_call({:demonitor, pid}, _from, state) do
    case HashDict.fetch(state.channels, pid) do
      :error       -> {:reply, :ok, state}
      {:ok,  _mfa} ->
        Process.unlink(pid)
        {:reply, :ok, drop_channel(state, pid)}
    end
  end

  def handle_info({:EXIT, pid, _reason}, state) do
    case HashDict.fetch(state.channels, pid) do
      :error -> {:noreply, state}
      {:ok, {mod, func, args}} ->
        Task.start_link(fn -> apply(mod, func, args) end)
        {:noreply, drop_channel(state, pid)}
    end
  end

  defp drop_channel(state, pid) do
    %{state | channels: HashDict.delete(state.channels, pid)}
  end

  defp put_channel(state, pid, mfa) do
    %{state | channels: HashDict.put(state.channels, pid, mfa)}
  end
end

Need Your Help

@Documented annotation in java

java eclipse annotations

What's the purpose of @Documented annotation in java?

Working with SAML 2.0 in C# .NET 4.5

c# .net saml saml-2.0

I am trying to use pure .NET (no external classes, controls, helpers) to create a SAML message. I found some code on the interwebs; this is what I have: