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?
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.
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
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).