How do you limit the number of clients connected to a channel?

How do you set a hard limit to how many clients can be joined to a given topic at any one time?

Ex: I want to set a limit of 5 so that if there are 5 users joined to a topic and another one tires to join they are denied entrance.

:wave:

I’d probably have an ets table for counting how many users there are in a particular chat room, and consult it in every channel join. Note that this approach wouldn’t work (or rather would require some modifications) for multi-node setup.

I’m actually curious if this can be accomplished using the presence module, since that keeps track of the number of users in a given topic.

Since presence is built on CRDTs as far as I remember, it’s eventually consistent, so that allows for more users (> limit) to join the channel at certain points. But yeah, if Presence has an API suitable for it, you can do it.

2 Likes

Thanks, I’ll try to make it happen with presence. If it works well enough I’ll post the code in this thread.

You could have a look on list/2 and perhaps do:

defmodule Example.Presence do
  use Phoenix.Presence,
    otp_app: :example,
    pubsub_server: Example.PubSub

  alias Example.Socket

  def track(socket) do
    track(socket, Socket.id(socket), %{})
  end

  def count(topic) do
    topic
    |> list
    |> map_size
  end
end

Which uses the list/1 callback injected and defined when using Phoenix.Presence. I’ve also included a track/1 function that could come in handy - that uses Phoenix’ track/3 callback.

I think I’m doing something similar.

This is what my code looks like currently.

   def join("webrtc:" <> corner_id, %{"user_id" => user_id}, socket) do
      if(Kernel.map_size(Presence.list(socket)) < 5) do
       socket = assign(socket, :current_user_id, user_id)
       send(self(), :after_join) # this is where the pressence is kept
       {:ok, %{channel: "webrtc:#{user_id}"}, assign(socket, :user_id, user_id)}
      else
       {:error, %{reason: "room is full"}}
      end
    end

Update 1:
And it doesn’t work :stuck_out_tongue:

Update 2:
This works actually but for some reason elixir interprets < as <= . I’m not sure why, there must be something I’m missing.

I’m curious, are you sure Presence.list(socket) works as intended? I’d expect you to be doing Presence.list(topic) instead. :thinking:

1 Like

What’s the difference?

Nevermind, the source code shows there’s no difference.

1 Like

Why do we have two different functions that do the same thing?

Watcha mean? Presence.list(socket) grabs the topic from the socket struct (you can see this from the source I linked above) so it’s basically the same as if you did Presence.list(topic). The docs don’t seem to mention this though, that’s why I was confused.

Aside from this, I’m not sure why you’re having that < as <= issue :thinking:.
^ How are you testing this?
^ Could there be a race condition such that a second client is joining the channel before the first one is tracked?

  1. First I reduced the hardcoded limit to 2(as in < 3) to make testing easier. Then I opened three different windows with three different users.
  2. I tested for this by having two users enter a room and then (after a few second) manually enter the room with another user. The user was allowed in but when attempting to add another user it was rejected.

So a total of 3 users entered the room?

Could it be that those users share the same socket id? What’s your call to Phoenix.track/3 like?

I’m not sure why but calling Presence.track(socket, Socket.id(socket), %{}) give’s me this error:

[error] an exception was raised:
** (UndefinedFunctionError) function ChatWeb.Socket.id/1 is undefined (module ChatWeb.Socket is not available)
    ChatWeb.Socket.id(%Phoenix.Socket{assigns: %{}, channel: ChatWeb.WebrtcChannel, channel_pid: #PID<0.457.0>, endpoint: ChatWeb.Endpoint, handler: ChatWeb.UserSocket, id: nil, join_ref: "1", joined: false, private: %{log_handle_in: :debug, log_join: :info}, pubsub_server: Chat.PubSub, ref: nil, serializer: Phoenix.Transports.V2.WebSocketSerializer, topic: "webrtc:", transport: Phoenix.Transports.WebSocket, transport_name: :websocket, transport_pid: #PID<0.455.0>, vsn: "2.0.0"})
    (chat) lib/chat_web/channels/webrtc_channel.ex:15: ChatWeb.WebrtcChannel.join/3
    (phoenix) lib/phoenix/channel/server.ex:188: Phoenix.Channel.Server.init/1
    (stdlib) gen_server.erl:374: :gen_server.init_it/2
    (stdlib) gen_server.erl:342: :gen_server.init_it/6
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

If you are doing Presence.track(socket, id, meta) with a same id for two different socket connections, since Presence.list/1 returns a map whose keys are those IDs then I presume what’s happening is that one connection is overwriting the other in this map. Thus when you grab the size of the map, it doesn’t match the number of connected users because two share a same key. :thinking:

You don’t need to call Socket.id/1 for this - although it indeed is a lot handier to call instead of having to match against the assigns on every function definition in order to grab the user id. Also, the error you have there shows that most likely that’s not what the socket module is named.

Anyhow, what’s the Presence.track/3 call you had prior?

I forgot to mention this prior but each of the connected users have a different user id. Since I’m using the user id of the individual users as the socket id.

socket = assign(socket, :current_user_id, user_id)

And each of the different users I’m using to test this have a different user id.

I see :thinking:. Can you possibly debug/log both user_id and the map returned from Presence.list/1 for each of the joining clients and show us?

Here you go:

[info] JOIN "webrtc:" to ChatWeb.WebrtcChannel
  Transport:  Phoenix.Transports.WebSocket (2.0.0)
  Serializer:  Phoenix.Transports.V2.WebSocketSerializer
  Parameters: %{"user_id" => "2"}
[info] CHECK
[info] 2
%{
  "user:1" => %{
metas: [
  %{
    initiator: false,
    phx_ref: "iBPiBd8fTm4=",
    user_id: 1,
    username: "admin"
  }
]
  },
  "user:3" => %{
metas: [
  %{initiator: false, phx_ref: "3zIVfMK1ZVs=", user_id: 3, username: "bob"}
]
  }
}
[info] Replied webrtc: :ok