Phoenix.Presence run some code when user leaves the channel

Hi. I am making a small 2 players game with Phoenix channels and I am using Presence to track connected users. My use case requires to terminate the game GenServer when any player leaves the channel, and notify another user about it.

So far I’ve achieved this by intercepting the “presence_diff” event in handle_out:

intercept ["presence_diff"]

def handle_out("presence_diff", %{leaves: leaves} = msg, socket) do
  # check if someone left
  case Enum.count(leaves) > 0 do
    true ->
      # kill gen server
      Engine.kill_game(socket.assigns.game_id)
      # notify players via terminate event
      push(socket, "terminate", %{})
      {:noreply, socket}

    _ ->
      push(socket, "presence_diff", msg)
      {:noreply, socket}
  end
end

And then on the client side I would do:

channel.on("terminate", resp => {
  channel.leave()
});

So I am wondering if it’s a good way to intercept the “presence_diff” event, do some job there and send an absolutely different event? Or is there any different approach to that?

You can always just register a channel PID to a process that monitors it, then it can do whatever it wants when the monitor tells it the channel PID dies. :slight_smile:

2 Likes

I do have a game channel for each individual game. Then I use join and terminate to signal join/leave of the game channel. I also store players in the game struct to check if You You are allowed to join/leave the channel.

This is an example of my terminate fuction, in the game channel…

  def terminate(reason, socket) do
    log("#{@name} > leave #{inspect(reason)}")

    "game:" <> id = socket.topic
    user = socket_assigned_user(socket.assigns)

    with pid when is_pid(pid) <- Games.get_game(id),
      game_state when not is_nil(game_state) <- Games.get_worker_state(pid)
    do
      if is_player(user.id, game_state), do: Games.leave_game(pid, user)
      
      if Process.alive?(pid) do
        notify_game_info(socket, pid)
      else
        broadcast!(socket, "game_removed", %{uuid: id})
      end
    end

    :ok
  end
2 Likes

:open_mouth: That helped me a lot. I was unaware of terminate callback. I needed to handle when someone leaves the channel and didn’t know I could catch that in terminate. Thank you.

Under the hood, a channel is a GenServer :slight_smile:

Be careful, terminate is a notification callback, you cannot rely on it ‘always’ being called:

3 Likes

In addition to channel GenServer callbacks, it’s possible to use a technique like Phoenix.Presence uses to know when a channel dies. I extracted a gist (haven’t run it as is, but should work) from a project I’m working on recently.

Wouldn’t just using a Monitor be so much safer in a supervision tree though?

True, it depends on what you’re wanting to optimize. Presence (and my code example) prefer consistency in the case of a failure. If you lose your monitors because you died, you would have to have a way to re-fetch that information in order to become consistent again. If you link, you will kill the other processes and they will re-connect (in the case of sockets).

Ultimately, it’s a tradeoff of if you believe your process will die and what you want the consequence of that death to be.

Yep, if you are just wanting a registry of connected processes then a monitor, but if you want to propogate death then a link, however trapping exits almost certainly means you want a monitor instead, or a supervisor.

1 Like

Indeed, as detailed in this question https://stackoverflow.com/questions/33934029/how-to-detect-if-a-user-left-a-phoenix-channel-due-to-a-network-disconnect, relying on terminate/2 could be problematic when the connection doesn’t close properly, e.g. times out. Guess people should be aware of it when they consider using terminate/2.