How to list all the connected sockets, or to track them

I’m developing an app, and I think my client is creating too many socket/channel connection, so I decided to setup some watchers. I followed Chris McCord’s code in this SO post: https://stackoverflow.com/questions/33934029/how-to-detect-if-a-user-left-a-phoenix-channel-due-to-a-network-disconnect

I created both a ChannelWatcher, and a SocketWatcher. The ChannelWatcher is working as I would expect, however the SocketWatcher is a bit weird, the pid that connects to the socket seems to die shortly after connecting (even though from the client side, the socket is still connected).

  @impl true
  def connect(%{"token" => token}, socket, _connect_info) do
    case Authentication.resource_from_access_token(token) do
      {:ok, user} ->
        SocketWatcher.monitor(self(), {__MODULE__, :on_disconnect, [user.id]})
        {:ok, Phoenix.Socket.assign(socket, current_user: user)}

      _ ->
        :error
    end
  end

  ...

  def on_disconnect(pid) do
    IO.puts("#{pid}'s socket disconnected")
  end

However the PID seems to die immediately

SocketWatcher monitoring #PID<0.3054.0>  <- Logged from the SocketWatcher.monitor function
:ok
[debug] CONNECTED TO MyAppWeb.UserSocket in 3ms
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"token" => "ACCESS_TOKEN", "vsn" => "2.0.0"}
#PID<0.3054.0>'s socket disconnected  <- Logged from UserSocket's on_disconnect function

Perhaps I misunderstand how Sockets work, but I thought it was a long-lasting PID that holds the connection. How do I find where the Sockets are listed? Are they stored in an ETS table? Can I watch them like I would a channel’s PID?

Have you looked at phoenix presence?

I have, but I don’t see how it would be any different. Whether I track the PID in Presence, or my own GenServer, the fast still remains that the PID dies shortly after it connects, and therefore sends an :EXIT/:DOWN msg to Presence. My issue is not with the channels, I’m tracking those successfully, my problem is with the sockets. I’m finding it difficult to track how many sockets are open. I was hoping there’s a ETS table, or genserver somewhere internally that tracks the Sockets.

AFAIK the sockets goes over into a channel process after successful connect (after handshake) call. I’m currently using a Presence.track call (like benwilson replied) in the channel join function with some unique/identity token, so if users join via multiple channels you can also track those (rate limiting), but you would still be able to count the number of ‘unique’ users. The presence_diff can be sent to another non-existing channel or (what i currently do) you can intercept them so they won’t go out to your users (as you don’t want them to see them).

The advantage of presence is that they automagically work in a distributed/clustered setting.

If you still want to go ahead with your own solution, I would move the functions into the channel (join) functions and try there. But you would need some deduplication inside the watchers in the same sense what you need in Presence.track.

Btw this is a nice article (pretty old but mostly still accurate AFAIK) about the inner flow of phoenix channels: https://zorbash.com/post/phoenix-websockets-under-a-microscope/

It’s the other way around, channel connections happen over a single socket. This is why my issue is not with the channels, but with the socket.

For example, if I did this on the client:

let socket1 = ...
socket1.connect();
let socket2 = ...
socket2.connect();
let socket3 = ...
socket3.connect();
let channel = socket3.channel("room:lobby", {})
channel.join();

If I only track Presence in the Channel.join, then the above code would appear to have 1 socket, with 1 channel, but it actually has 1 socket with 1 channel, and 2 sockets with 0 channels.

I’ve found a solution though,

defmodule MyAppWeb.UserSocket do
  @impl true
  def init({_, socket} = state) do
    SocketWatcher.monitor(
      self(),
      {__MODULE__, :on_disconnect, [socket.assigns.current_user.id]}
    )

    Phoenix.Socket.__init__(state)
  end

  use Phoenix.Socket

  ...
end
1 Like

connect happens within the request process as we don’t upgrade to websocket unless you allow it to happen, so that is why you see the immediate DOWN from the http request process. Note that your workaround relies on private APIs so it should not be relied on.

@chrismccord Is there a better way to track the socket’s PID than my work around? If I access the transport_pid from the socket in the channel, I’ll miss sockets that don’t connect to a channel (my whole reason for going down this rabbit hole was to find leaks in my client where it created multiple sockets).

Hey @pejrich did you ever find a way to do this without using the private API method?