Skip to content

Commit 3fc1fb1

Browse files
authored
allow setting max_channels_per_transport on the socket in endpoint (#6871)
1 parent 896a512 commit 3fc1fb1

5 files changed

Lines changed: 171 additions & 5 deletions

File tree

‎lib/phoenix/endpoint.ex‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ defmodule Phoenix.Endpoint do
746746

747747
paths =
748748
if websocket do
749-
websocket = put_auth_token(websocket, opts[:auth_token])
749+
websocket = put_socket_opts(websocket, opts)
750750
config = Phoenix.Socket.Transport.load_config(websocket, Phoenix.Transports.WebSocket)
751751
plug_init = {endpoint, socket, config}
752752
{conn_ast, match_path} = socket_path(path, config)
@@ -757,7 +757,7 @@ defmodule Phoenix.Endpoint do
757757

758758
paths =
759759
if longpoll do
760-
longpoll = put_auth_token(longpoll, opts[:auth_token])
760+
longpoll = put_socket_opts(longpoll, opts)
761761
config = Phoenix.Socket.Transport.load_config(longpoll, Phoenix.Transports.LongPoll)
762762
plug_init = {endpoint, socket, config}
763763
{conn_ast, match_path} = socket_path(path, config)
@@ -769,8 +769,25 @@ defmodule Phoenix.Endpoint do
769769
paths
770770
end
771771

772-
defp put_auth_token(true, enabled), do: [auth_token: enabled]
773-
defp put_auth_token(opts, enabled), do: Keyword.put(opts, :auth_token, enabled)
772+
# Copies top-level socket options into the transport config,
773+
# which is what the socket receives on connect.
774+
defp put_socket_opts(true, opts), do: put_socket_opts([], opts)
775+
776+
defp put_socket_opts(transport_opts, opts) do
777+
transport_opts = Keyword.put(transport_opts, :auth_token, opts[:auth_token])
778+
779+
case Keyword.fetch(opts, :max_channels_per_transport) do
780+
{:ok, max} when is_integer(max) and max > 0 ->
781+
Keyword.put(transport_opts, :max_channels_per_transport, max)
782+
783+
{:ok, other} ->
784+
raise ArgumentError,
785+
":max_channels_per_transport must be a positive integer, got: #{inspect(other)}"
786+
787+
:error ->
788+
transport_opts
789+
end
790+
end
774791

775792
defp socket_path(path, config) do
776793
end_path_fragment = Keyword.fetch!(config, :path)
@@ -865,6 +882,15 @@ defmodule Phoenix.Endpoint do
865882
866883
Custom transports might implement their own mechanism.
867884
885+
* `:max_channels_per_transport` - the maximum number of channels that may be
886+
joined per transport process. Overrides the value given to `use Phoenix.Socket`,
887+
which defaults to `100`. This is useful for sockets you don't define yourself,
888+
such as `Phoenix.LiveView.Socket`:
889+
890+
socket "/live", Phoenix.LiveView.Socket,
891+
websocket: [connect_info: [session: @session_options]],
892+
max_channels_per_transport: 200
893+
868894
You can also pass the options below on `use Phoenix.Socket`.
869895
The values specified here override the value in `use Phoenix.Socket`.
870896

‎lib/phoenix/socket.ex‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ defmodule Phoenix.Socket do
8080
to handle channels. Defaults to the number of cores.
8181
8282
* `:max_channels_per_transport` - the maximum number of channels that may be
83-
joined per transport process. Defaults to `100`.
83+
joined per transport process. Defaults to `100`. It can also be set when
84+
mounting the socket in your endpoint, which overrides the value given here.
85+
See `Phoenix.Endpoint.socket/3`.
8486
8587
## Garbage collection
8688

‎test/phoenix/endpoint/endpoint_test.exs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,5 +438,21 @@ defmodule Phoenix.Endpoint.EndpointTest do
438438
socket "/ws", UserSocket, longpoll: [path: "/ws", check_origin: false, drainer: []]
439439
end
440440
end
441+
442+
assert_raise ArgumentError, ~r/:max_channels_per_transport must be a positive integer/, fn ->
443+
defmodule MyInvalidSocketEndpoint3 do
444+
use Phoenix.Endpoint, otp_app: :phoenix
445+
446+
socket "/ws", UserSocket, max_channels_per_transport: nil
447+
end
448+
end
449+
450+
assert_raise ArgumentError, ~r/:max_channels_per_transport must be a positive integer/, fn ->
451+
defmodule MyInvalidSocketEndpoint4 do
452+
use Phoenix.Endpoint, otp_app: :phoenix
453+
454+
socket "/ws", UserSocket, longpoll: true, max_channels_per_transport: 0
455+
end
456+
end
441457
end
442458
end

‎test/phoenix/integration/long_poll_channels_test.exs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ defmodule Phoenix.Integration.LongPollChannelsTest do
148148
check_origin: ["//example.com"]
149149
]
150150

151+
socket "/ws/limited", UserSocket,
152+
longpoll: [
153+
window_ms: 200,
154+
pubsub_timeout_ms: 200,
155+
check_origin: ["//example.com"]
156+
],
157+
max_channels_per_transport: 1
158+
151159
socket "/ws/connect_info", UserSocketConnectInfo,
152160
longpoll: [
153161
window_ms: 200,
@@ -456,6 +464,44 @@ defmodule Phoenix.Integration.LongPollChannelsTest do
456464
] = resp.body["messages"]
457465
end
458466

467+
test "#{@mode}: honors max_channels_per_transport given to socket/3" do
468+
prefix = "room:limited-#{System.unique_integer([:positive])}"
469+
first_topic = "#{prefix}-1"
470+
session = join("/ws/limited", first_topic, @vsn, "1", @mode)
471+
472+
resp = poll(:get, "/ws/limited", @vsn, session)
473+
assert resp.body["status"] == 200
474+
475+
assert [
476+
%Message{
477+
event: "phx_reply",
478+
payload: %{"response" => %{}, "status" => "ok"},
479+
topic: ^first_topic
480+
},
481+
%Message{event: "user_entered", topic: ^first_topic},
482+
%Message{event: "joined", topic: ^first_topic}
483+
] = resp.body["messages"]
484+
485+
overflow_topic = "#{prefix}-2"
486+
resp = join_existing_session("/ws/limited", session, overflow_topic, @vsn, "2", "2")
487+
assert resp.body["status"] == 200
488+
489+
resp = poll(:get, "/ws/limited", @vsn, session)
490+
assert resp.body["status"] == 200
491+
492+
assert [
493+
%Message{
494+
event: "phx_reply",
495+
payload: %{
496+
"response" => %{"reason" => "too many channels joined"},
497+
"status" => "error"
498+
},
499+
ref: "2",
500+
topic: ^overflow_topic
501+
}
502+
] = resp.body["messages"]
503+
end
504+
459505
test "#{@mode}: transport x_headers are extracted to the socket connect_info" do
460506
session =
461507
join("/ws/connect_info", "room:lobby", @vsn, "1", @mode, %{}, %{}, %{

‎test/phoenix/integration/websocket_channels_test.exs‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,20 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
197197
timeout: 200
198198
]
199199

200+
socket "/ws/endpoint_limited", UserSocket,
201+
websocket: [
202+
check_origin: ["//example.com"],
203+
timeout: 200
204+
],
205+
max_channels_per_transport: 1
206+
207+
socket "/ws/endpoint_override", LimitedUserSocket,
208+
websocket: [
209+
check_origin: ["//example.com"],
210+
timeout: 200
211+
],
212+
max_channels_per_transport: 2
213+
200214
socket "/ws/connect_info", UserSocketConnectInfo,
201215
websocket: [
202216
check_origin: ["//example.com"],
@@ -397,6 +411,68 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
397411
}
398412
end
399413

414+
test "honors max_channels_per_transport given to socket/3" do
415+
{:ok, sock} =
416+
WebsocketClient.connect(
417+
self(),
418+
"ws://127.0.0.1:#{@port}/ws/endpoint_limited/websocket?vsn=#{@vsn}",
419+
@serializer
420+
)
421+
422+
WebsocketClient.join(sock, "room:limited-1", %{})
423+
424+
assert_receive %Message{
425+
event: "phx_reply",
426+
payload: %{"response" => %{}, "status" => "ok"},
427+
ref: "1",
428+
topic: "room:limited-1"
429+
}
430+
431+
WebsocketClient.join(sock, "room:limited-2", %{})
432+
433+
assert_receive %Message{
434+
event: "phx_reply",
435+
payload: %{
436+
"response" => %{"reason" => "too many channels joined"},
437+
"status" => "error"
438+
},
439+
ref: "2",
440+
topic: "room:limited-2"
441+
}
442+
end
443+
444+
test "max_channels_per_transport given to socket/3 overrides the use Phoenix.Socket value" do
445+
{:ok, sock} =
446+
WebsocketClient.connect(
447+
self(),
448+
"ws://127.0.0.1:#{@port}/ws/endpoint_override/websocket?vsn=#{@vsn}",
449+
@serializer
450+
)
451+
452+
for index <- 1..2 do
453+
topic = "room:limited-#{index}"
454+
WebsocketClient.join(sock, topic, %{})
455+
456+
assert_receive %Message{
457+
event: "phx_reply",
458+
payload: %{"response" => %{}, "status" => "ok"},
459+
topic: ^topic
460+
}
461+
end
462+
463+
WebsocketClient.join(sock, "room:limited-3", %{})
464+
465+
assert_receive %Message{
466+
event: "phx_reply",
467+
payload: %{
468+
"response" => %{"reason" => "too many channels joined"},
469+
"status" => "error"
470+
},
471+
ref: "3",
472+
topic: "room:limited-3"
473+
}
474+
end
475+
400476
test "transport x_headers are extracted to the socket connect_info" do
401477
extra_headers = [{"x-application", "Phoenix"}]
402478

0 commit comments

Comments
 (0)