-
Notifications
You must be signed in to change notification settings - Fork 243
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
Thoughts on making a sans-I/O implementation of QUIC #4
Comments
Hi Seth! At the moment the coupling to I/O is fairly weak: if you're willing to deal with the nitty-gritty details you can instantiate I do however want to make sure the API for regular users is as simple as the current What are your specific requirements? |
My requirements are that of a library author. :) Basically want to be able to use synchronous or any flavor of asynchronous (asyncio, trio, curio) datagram transport by abstracting away the serializing and de-serializing logic. I'll take a close look at what can already be accomplished with the |
I'd definitely like the Python ecosystem to be able to leverage a common QUIC base, the protocol is frighteningly complex and I don't think we'd benefit from fragmentation. Feel free to poke around and let me know if you see API decisions which are likely to be problematic. At the moment I am trying to ensure maximum interoperability with other implementations and locking this down with unit tests. I'll try and keep the public API surface to a minimum, and we can revisit the internal APIs based on your feedback. By the way if you want to help out: one blocker currently for interop tests is the lack of HTTP/3 support in my client / server examples. One stumbling block here is supporting the QPACK header compression, which is not identical to HPACK. We can however leverage the massive huffmann table from: https://github.com/python-hyper/hpack |
Here's some issues that I've identified:
|
I'd be super-invested in this too. I think the Sans-I/O pattern isn't just valuable in that it allows alternate concurrency models to build with it, but that it's also just plain more resilient, understandable, and testable. (Especially when approached as "feed events in, get bytes out" style, rather than through callbacks) Worth having a bit of look at some of the projects currently under the https://github.com/python-hyper/ umbrella. Particularly the My motivation here is looking towards HTTP/3 - since I can see how that'd fit in really nicely with https://github.com/encode/httpcore |
Regarding your comments:
The rest is a bit abstract, maybe some WIP PRs would steer me in the right direction? |
I'm catching you at a bad time for myself to provide a PR (going to be without internet connection for the next 3 days) but I will return to this with more direction. Thank you for your openness to this idea. :) |
I have read the h2 documentation and indeed like the sans-io pattern, so I will rework aioquic to provide the asyncio-specific wrapper in an aioquic.asyncio module and keep the core asyncio-free. One thing which differs from h2 is that QUIC does not rest on a reliable transport. As such, timers are needed for retransmissions. What would be the right API to inform the user that they need to arm a timer? Should I use an event for this? How should I express "when" the timer needs to fire, since I don't know the time reference of the event loop? Similarly, @tomchristie any thoughts? |
What I've got in mind so far:
|
If you take a look at h11 it's implemented with And if you already have a method that is called before each send what are your thoughts on having the |
I'll read the h11 and get back to you, many thanks for the insights. One thing which irks me is that even it the simple form which returns a list of datagrams, the API consumer code is going to be a lot less pretty than the h2 equivalent: socket.sendall(connection.data_to_send()) At the very least it's going to be: for data, addr in connection.datagrams_to_send():
sock.sendto(data, addr) With a timer this would be: datagrams, timeout = connection.datagrams_to_send()
for data, addr in datagrams:
sock.sendto(data, addr)
update_or_stop_timer(timeout) .. and this would need calling after datagram reception, or calling any mutators. |
I think regardless of what we do we aren't going to have the same niceness of other sans-I/O libraries. I think the single source for events and single source of "stuff you the implementer must do" is nice though. |
Another approach to consider would be: class Todo:
datagrams_to_send: List[Tuple[bytes, NetworkAddress]]
events_received: List[Event]
max_update_time: float
class QUICConnection:
def update(
*,
events_to_send: List[Event] = (),
datagrams_received: List[Tuple[bytes, NetworkAddress]] = (),
current_time: float,
) -> Todo:
... So the idea is that from time to time the user gives the sans-io library an update on what's going on from its side. This includes zero-or-more new high-level Events that it wants to happen, and zero-or-more new datagrams that it has received. It always includes the current time, on whatever clock it's using. The sans-io library responds by giving the user some homework: here are zero-or-more high-level events that happened for you to deal with, here are zero-or-more datagrams that need to be sent, and in any case you better call I can think of one major reason why this might not be a good idea. If you want to expose ancillary state variables on the sans-io connection object, and you think that users might want to refer to those state variables while they're looping through the new events that arrived, then you really want an h11-style These issues are super subtle, and we're still collectively figuring out the best way to cope with them as a design community. But to catch up with previous discussions, I highly recommend reading both of these discussions: CC: @Lukasa |
@njsmith thanks for the input! I've started ripping out the asyncio-specifics in the sans-io branch, for now there is a |
OK I've just landed a patch on master which moves anything asyncio-related into the aioquic/aioquic/asyncio/protocol.py Line 115 in 6b20cde
The general idea is that after calling a mutator (data received, data to send, close connection, send a ping..) you need to:
@sethmlarson is this going in the direction you want? |
Given that the |
Regarding the |
Since the interface is symmetrical once you're done with setting up the connection (correct me if I'm wrong here) do we need a distinction at the event class level? |
Well, having a uniform set of Part of why it works for those protocols is that when you switch from client to server, most of the input events turn into output events and vice-versa. |
I think I need to read some more code, I'm confused as to what these "input events" are. Would this include "commands" such as "send this data on a stream for me" or "tear down the connection"? Or is it only "a timer fired" or "a datagram was received"? |
Ah right sorry. "Event" is a term of art here. In h11, event objects are specifically a representation of things that can happen at the level of the abstract protocol semantics. Stuff like "a request with these headers" or "a part of a message body". You can think of h11 as a big transducer: you give it bytes and it tells you what events just happened, or you tell it what events you want to make happen and it tells you what bytes to send. If you read through the h11 tutorial then it emphasizes this way of thinking: https://h11.readthedocs.io/en/latest/basic-usage.html Or here's the reference docs on events: https://h11.readthedocs.io/en/latest/api.html#events |
Just happened to notice that Cloudflare's QUIC library is sans-io, so their API might be useful to compare to:
– https://blog.cloudflare.com/enjoy-a-slice-of-quic-and-rust/ |
Thanks for the link @njsmith ! A couple of points I noted:
|
Hmm, yeah, that seems bad. IMO you definitely need to let the user control the clock. People care about this. That sounds really difficult if you never pass in the time from outside. I guess in theory you could do it by having the API report back "in X seconds please give me this token", and then the user has to keep track of a bunch of tokens and give them back at the right moments, but... that sounds way more awkward and inefficient than just letting the user tell you what time it is on their favorite clock.
Interesting! The way h2 handles this is that it gives back events saying "here's some data", but the transport layer doesn't actually move the receive window until the application says "okay I processed that data", in a second step. |
Contributor to the quiche project here. Thanks for sharing your notes @jlaine. @njsmith wrote:
It's worth highlighting that quiche's HTTP/3 API works a little differently to the transport API. An application that uses quiche will generally read by calling in order: |
Both the QUIC and HTTP APIs are now sans-io so unless anyone objects I'll close this issue tomorrow. Feel free to open additional issues for changes to the API. |
Woo! 🎉 Thanks @jlaine! :) This will make this library easy to use for HTTP clients. |
Should advertise this and link to the sans-I/O "manifesto" page https://sans-io.readthedocs.io/ |
@salotz you mean like this? https://aioquic.readthedocs.io/en/latest/design.html#sans-io-apis |
🤕 yep! I only looked at the README :P |
Would you be interested in implementing QUIC without I/O in order to support both synchronous and multiple async implementations?
The text was updated successfully, but these errors were encountered: