Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement WebSocket negotiation support based on Plug.Conn.upgrade_adapter/3 #5030

Merged
merged 8 commits into from Nov 1, 2022

Conversation

mtrudel
Copy link
Member

@mtrudel mtrudel commented Oct 16, 2022

The work here is the first step towards implementation of the Phoenix work outlined in #5003. It implements:

  • Plug-mediated support for both WebSocket and long poll socket endpoints. WebSocket requests are upgraded via Plug.Conn.upgrade_adapter/3
  • Removal of the HTTP aspects of Phoenix.Endpoint.Cowboy2Handler as they are no longer required

Note that this PR is not entirely complete yet; it will need to have its dependencies on Plug & Plug.Cowboy updated to releases which incorporate elixir-plug/plug#1119 and elixir-plug/plug_cowboy#88 respectively. CI obviously won't pass until this is done.

lib/phoenix/endpoint.ex Outdated Show resolved Hide resolved
lib/phoenix/endpoint.ex Outdated Show resolved Hide resolved
lib/phoenix/endpoint.ex Outdated Show resolved Hide resolved
lib/phoenix/socket.ex Outdated Show resolved Hide resolved
try do
endpoint.call(conn, opts)
rescue
exception in [UndefinedFunctionError] ->
Copy link
Member Author

@mtrudel mtrudel Oct 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note we're rescuing against an UndefinedFunctionError rather than catching a throw as previously. Unsure why that implementation caught instead of rescuing, but this captures the same set of errors.

@mtrudel mtrudel marked this pull request as ready for review October 21, 2022 15:30
@mtrudel
Copy link
Member Author

mtrudel commented Oct 21, 2022

@josevalim as requested, this work is done and ready for review! See #5003 for an overview & merge proposal.

@josevalim
Copy link
Member

Thanks, I got the PRs. I am swamped with other work but I will review once I have time!

@josevalim
Copy link
Member

Thank you @mtrudel!

I think this looks great but I would break this PR in two. The first PR is to use upgrade_adapter without using Sock, simply by calling the handler directly which will be our existing Cowboy implementation - without touching the bulk of the handler code (i.e. we will remove the call to endpoint.__handler__ but leave the rest pretty much as is).

This in itself will already allow you to plug bandit in, in a transparent way. Then we can have another PR to discuss and move to Sock altogether. :)

lib/phoenix/endpoint.ex Outdated Show resolved Hide resolved
@@ -909,13 +852,38 @@ defmodule Phoenix.Endpoint do

"""
defmacro socket(path, module, opts \\ []) do
module = Macro.expand(module, %{__CALLER__ | function: {:__handler__, 2}})
module = Macro.expand(module, __CALLER__)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to keep the %{function : ...} bit to avoid compile-time dependencies. If we need to pick another name, it can be call/2, as it is now a regular Plug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is past the limit of my macro brain :) You're saying basically

Suggested change
module = Macro.expand(module, __CALLER__)
module = Macro.expand(module, %{__CALLER__ | function: {:call, 2}})

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Or :socket_dispatch instead of :call if we go down that route. The name doesn't matter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@mtrudel
Copy link
Member Author

mtrudel commented Oct 29, 2022

Thank you @mtrudel!

I think this looks great but I would break this PR in two. The first PR is to use upgrade_adapter without using Sock, simply by calling the handler directly which will be our existing Cowboy implementation - without touching the bulk of the handler code (i.e. we will remove the call to endpoint.__handler__ but leave the rest pretty much as is).

Very good!

@@ -0,0 +1,36 @@
defmodule Phoenix.Endpoint.AttemptCodeReloadPlug do
@moduledoc ~S"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this as @moduledoc false, as it should be private, but keep those notes as code comments!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you would need to use this from Bandit? Which then means it has to be public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup!

@mtrudel
Copy link
Member Author

mtrudel commented Oct 30, 2022

Branch updated with the first PR of the two-PR split @josevalim requested previously. It's fully functional and testing green.

A couple of notes:

  • Phoenix.Endpoint.Cowboy2Handler is still around, but the scope is now narrowed to just providing :cowboy_websoket conformance. This will disappear into Sock in a subsequent PR
  • The Cowboy-specific setup code in Phoenix.Transports.WebSocket makes this version still Cowboy-specific (again, this will disappear into Sock in a subsequent PR)
  • Changes to Phoenix.Endpoint are minimized

Is this what you'd envisioned, @josevalim ? If you're happy with this approach then I think this PR can stand for proper review.

@mtrudel mtrudel force-pushed the sock_support branch 3 times, most recently from aa6211a to 12f6a57 Compare October 31, 2022 01:29
@mtrudel mtrudel changed the title Implement WebSocket support based on Plug.Conn.upgrade_adapter/3 and Sock Implement WebSocket negotiation support based on Plug.Conn.upgrade_adapter/3 Oct 31, 2022
Comment on lines 83 to 84
# Cures a Cowboy race condition where it doesn't see our declared websocket_init/1
_ = Code.ensure_loaded?(Phoenix.Endpoint.Cowboy2Handler)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was reliably buggy on the server's first WebSocket upgrade without this - switching the handler within Cowboy seems to cause a race with the new handler not being available right away, and so the new handler's (optional!) websocket_init/1 was not being called.

Since this isn't a module that never gets reloaded we're OK having this just load once at startup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why this happens: we should fix it in Plug.Cowboy. We should call handler.module_info(:module) before passing it to the websocket upgrade. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have republished v2.6.0. We should be able to remove this line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that this fixes the issue. Branch updated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand for next time, does that change fix it due to the side effect of ensuring the module is in fact loaded before calling upgrade/5?

@mtrudel
Copy link
Member Author

mtrudel commented Oct 31, 2022

mix deps updated for plug 1.14 and plug_cowboy 2.6.0. Should be good to go!

@mtrudel
Copy link
Member Author

mtrudel commented Oct 31, 2022

Integration test failure looks to be unrelated

@josevalim
Copy link
Member

This PR looks great to me. I only need to figure out a way to make the websocket bits inside the WebSocket transport Pluggable. I will tidy it up and I should merge it tomorrow!

@mtrudel
Copy link
Member Author

mtrudel commented Oct 31, 2022

If you want to take the option, I've got a good deal of that stuff cleaned up in my 'part 2' PR (still pending). I'm expecting to have it out in the next day or so. It's definitely something that we should look at, but IMO it's strictly beyond the minimal scope of this PR.

@josevalim josevalim merged commit 06b314e into phoenixframework:master Nov 1, 2022
@josevalim
Copy link
Member

Thank you @mtrudel, this is huge! 💚 💙 💜 💛 ❤️

@josevalim
Copy link
Member

josevalim commented Nov 1, 2022

Hi @mtrudel! I have pushed 08c6e61, which should allow you to inject a upgrade so you can inject sock for now. I don't expect this to be available in the long term, it is temporary for experimentation, so make sure that you depend on Phoenix ~> 1.7.x.

I think we want to move the socket connections all the way down to the router. This way we get logging, session handling, and more for free. The same way we just got SSL handling for free. This also means LiveDashboard no longer has to ask users to invoke socket in the endpoint. The same for Oban and pretty much every LiveView lib out there. This will be a huge improvement thanks to your work!

Since there is some API work necessary to make this happen, that's better in the hands of @chrismccord! So I would expect this to happen only after v1.7 is out. :)

@mtrudel mtrudel deleted the sock_support branch November 1, 2022 15:42
@mtrudel
Copy link
Member Author

mtrudel commented Nov 1, 2022

AMAZING!

I see two streams of followup work coming from this:

First, I'll get to work on implementing generic upgrade / handler support via Sock as we've talked about above. I suspect that should be up for review within a week or so; I'll be dropping a few points for early discussion down below to make sure we're aligned on this. There shouldn't be any API facing work here; this is all plumbing. Do you see any reason that this couldn't make it into 1.7 assuming I get the work done soon?

Second, in terms of moving socket down into router, that's going to simplify things a ton, both within Phoenix and for users as you say. Given that this will change the user facing socket definition API, I agree that this should be tackled post-1.7.

@josevalim
Copy link
Member

Do you see any reason that this couldn't make it into 1.7 assuming I get the work done soon?

I can give feedback once the PR is up. I am worried about the second item affecting the API from the first item but maybe that won't be the case depending on how isolated it is.

@mtrudel
Copy link
Member Author

mtrudel commented Nov 1, 2022

I don't think there's going to be any real overlap between the two items, but I agree, let's wait and see how the PR shakes out.

My main uncertainty as this point is whether the Sock API should subsume the Phoenix.Socket.Transport.connect/1 function, and call it as part of the upgrade process (that is, should Sock manage the 'take a conn and some config and turn it into initial socket state' flow or not)?

The way it is now in master, the work to build initial socket state is spread between Phoenix.Socket.connect/1, Phoenix.Socket.Transport.connect_info/3, and Phoenix.Transports.WebSocket.call/2. More broadly, any prospective Sock user is going to need to manage the 'how do I get info out of my originating conn and into my socket' in an adhoc manner. It feels like this should be brought into the Sock lifecycle.

In Phoenix's case, if the responsibility for getting info out of the originating conn moved into Phoenix.Socket.Transport.connect (whose signature/arity would need to change to also take config & opts), it would allow all that disparate setup code to be consolidated. This would also drop a bunch of common code between WebSocket and LongPoll, I think.

This could always be accommodated later on by adding an optional step of this nature to the Sock lifecycle & moving some stuff in WebSocket.call/2 around, so I don't think it's a blocking concern to getting Sock support in here.

Any strong feelings one way or the other?

@mtrudel
Copy link
Member Author

mtrudel commented Nov 1, 2022

Also, thanks again for being so amazing to work with. You really do make it look easy!

@josevalim
Copy link
Member

My main uncertainty as this point is whether the Sock API should subsume the 'Phoenix.Socket.Transport.connect/1` function, and call it as part of the upgrade process (that is, should Sock manage the 'take a conn and some config and turn it into initial socket state' flow or not)?

Right now, I am thinking that the Sock contract would exactly be the phoenix_websocket_upgrade contract that we have in master right now.

I also really think we should have "Web" in the name of the project if it is going to be WebSocket specific. :) WebSocket, WebSock, WS are all fine by me. :)

@mtrudel
Copy link
Member Author

mtrudel commented Nov 1, 2022

Right now, I am thinking that the Sock contract would exactly be the phoenix_websocket_upgrade contract that we have in master right now.

Agree that that's a fine place to start. My concern is really more of 'future optimization' than anything fundamentally structural.

I also really think we should have "Web" in the name of the project if it is going to be WebSocket specific. :) WebSocket, WebSock, WS are all fine by me. :)

Instead of 'Sock', you mean? The good names all mostly already taken in Hex, sadly.

Though WebSock isn't! And it's a shoutout to a name I haven't heard for a long, long time! https://en.wikipedia.org/wiki/Trumpet_Winsock

@mtrudel
Copy link
Member Author

mtrudel commented Nov 1, 2022

Getting this working on Bandit was a breeze!

It's official: Bandit 0.5.7 (just released) will FULLY support Phoenix as of 1.7 (even if Sock support doesn't land)! Fantastic!

Thanks again for all your hard work on this @josevalim!

@mtrudel mtrudel mentioned this pull request Nov 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants