swift-nio icon indicating copy to clipboard operation
swift-nio copied to clipboard

Add NIOAsyncWebSocketServerUpgrader

Open 0xTim opened this issue 4 years ago • 20 comments

One for discussions, adds a new async version of NIOWebSocketServerUpgrader. Thoughts? This allows the upgrader to be async/await all the way through, without needing to jump between event loops inside the class, or having optional async upgraders etc and push the boundary down to the channel handlers

0xTim avatar Oct 07 '21 11:10 0xTim

At some point, somewhere, something has to thunk between the async model and the event loop model. I think it’s fine to propose completely reimplementing upgraders in an async pattern, such that we’d end up with this, but we’ll need to add a bridge protocol. The patch as written won’t compile, because it conforms to the Future-based version of the protocol.

So I think this approach is generally fine, but we should first make the HTTP protocol upgrader API async-capable, and then we can deal with reimplementing the websocket upgrader.

Lukasa avatar Oct 07 '21 11:10 Lukasa

@Lukasa so we've hit this issue in Vapor in that we can't make protocols async compliant without breaking changes. The way we've done it is to add async versions of the protocol that conform to the non-async version to work around it, that make sense? I've done that here

0xTim avatar Oct 07 '21 11:10 0xTim

I don't think the CI failure is me now

0xTim avatar Oct 07 '21 13:10 0xTim

Yup, that was the exact design direction I wanted us to push.

Lukasa avatar Oct 07 '21 14:10 Lukasa

I don't think the CI failure is me now

The nightly build is likely broken because of the compiler crasher noted in SR-15040 (can't say for sure since the error log here doesn't show any compiler output), which will probably be fixed by the next nightly snapshot, that will include the fix from apple/swift#39582.

finagolfin avatar Oct 09 '21 09:10 finagolfin

The nightly build is broken because of Sendable conformance, mostly. 😉

Lukasa avatar Oct 09 '21 15:10 Lukasa

Happy with the style and general shape.

Lukasa avatar Oct 12 '21 14:10 Lukasa

@Lukasa cool, last thing I'll do is duplicate the tests and use the async upgrader then we should be good to go

0xTim avatar Oct 12 '21 14:10 0xTim

@Lukasa I'm struggling to get the tests to pass. Even a simple testRequiresVersion13 is triggering an error on server shutdown instead of when writing the buffer to the server. I'm guessing my upgrade handler is handler an error wrong but that's pushing my NIO knowledge

0xTim avatar Oct 12 '21 15:10 0xTim

@Lukasa any idea what's causing the run loop to not fire?

0xTim avatar Nov 17 '21 17:11 0xTim

Honestly, I don't think attempting to jerry-rig EmbeddedChannel to be suitable for testing async functions is going to be likely to work. I think we'll need an alternative to EmbeddedChannel that is an actor, and can safely execute async functions and wait for their completion.

Lukasa avatar Nov 22 '21 13:11 Lukasa

@Lukasa is this something you're planning on adding?

0xTim avatar Jan 18 '22 11:01 0xTim

I don't have any current roadmap to do it, no. But we're going to have to.

Lukasa avatar Jan 18 '22 12:01 Lukasa

This sounds like a fun project. Mind if I take a crack at it? Have you had any thoughts about the implementation that you'd be able to share?

AlanQuatermain avatar Jan 18 '22 20:01 AlanQuatermain

Sure thing, go for it.

My current thought right now is that the closes analog to an EmbeddedChannel is a channel that owns a private DispatchQueue, and that dispatches all operations onto that queue. This allows it to have the appropriate scheduling and isolation logic. Otherwise it would be the same as EmbeddedChannel (e.g. doesn't use a real clock, doesn't do things unless told).

In an ideal world it would be an actor but if it's an actor it's not possible to answer the question "am I on the event loop" in a coherent way. That's pretty unfortunate, and ends up totally disqualifying this from being a proper actor.

Lukasa avatar Jan 19 '22 09:01 Lukasa

Is there any utility in looking at an actor-based event loop implementation? It seems like that's the sticking point at the moment.

Aside from that, I feel like it ought to be possible to use unsafe continuations to dispatch work onto an event loop instance in an async-friendly fashion. It's been a while though, and maybe there are some special cases I don't recall where the channel code requires synchronicity from the event loop in respect to other channels on the same loop?

AlanQuatermain avatar Jan 19 '22 20:01 AlanQuatermain

Is there any utility in looking at an actor-based event loop implementation? It seems like that's the sticking point at the moment.

We're unlikely to be really willing to do that until we can use custom executors. The principal issue is that we want to be able to block threads on selectors, and actors cannot do that (they can't block in the cooperative thread pool). A complete actor based event loop that didn't do this would need a complete parallel I/O system that involved a separate I/O thread, and the performance implications of that are fairly alarming.

Aside from that, I feel like it ought to be possible to use unsafe continuations to dispatch work onto an event loop instance in an async-friendly fashion.

Yup, you can do that, and NIO already provides a bunch of helpers. The thing we need here is a replacement for EmbeddedEventLoop and EmbeddedChannel. The issue with these types is that they are embedded directly in the creating thread: they do not have a parallel thread context of their own. As a result, we can't use continuations to re-establish a thread context there.

This specific use-case would be a replacement for EmbeddedEventLoop that still allows control over when and how I/O happens, but that is tied to a specific actor. This is not a fully-fledged actor event loop, but it would be pretty close.

Lukasa avatar Jan 20 '22 10:01 Lukasa

@Lukasa is the NIOAsyncTestingChannel supposed to be a drop-in replacement for EmbeddedChannel?

In the new tests I'm running

try client.writeString(upgradeRequest).wait()

and it's never returning

0xTim avatar Aug 10 '22 13:08 0xTim

@0xTim Not a perfect one, because it's impossible to write a perfect one, so it's a next-best one. What does writeString do in that code?

Lukasa avatar Aug 10 '22 14:08 Lukasa

@Lukasa nothing to see here - that pointed me in the right direction!

0xTim avatar Aug 10 '22 14:08 0xTim