-
Notifications
You must be signed in to change notification settings - Fork 204
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
Unidirectional streams #175
Comments
Discussed in Tokyo; people need to see a more concrete proposal. @martinthomson to noodle on it. |
Adding unidirectional streams to the protocol seems like something we could consider, but removing bidirectional streams seems a bit rash. Bidirectional streams are a protocol mechanism provided by any number of different transports (TCP, SCTP, SST, etc) and have a long history of deployment success. Bidirectional streams provide an automatic mechanism for marrying a request and response together. Without this, applications (or the transport) will need some additional mechanism to associate requests with response. Since we have no experience with QUIC or HTTP/2 over QUIC using only unidirectional streams, I fear that there are unexpolored dark corners which we will only discover once we try to start using them. Open stream limits seem like one such corner. Currently with QUIC, each endpoint can specify the total number of open streams that the peer is allowed to create. If both sides set a limit of, say, 10 streams then this allows the client to issue 10 requests and for the server to provide 10 response while at the same time, it allows the server to push 10 additional responses. If all streams were unidirectional, you might imagine and a client issuing a single request and the server responding with the response + 9 pushes, hence exhausting the open stream limit. If the client opened another request, the server would not be able to open a stream to respond. Now, you could imagine that we could change the way the stream limits apply so that "response streams" don't count in some way, but that sounds treacherous. |
That's a problem that servers already have. They can promise far in excess of the number of streams the client permits. Thus, it is safe (though perhaps inadvisable) for the client to advertise fewer streams than it uses to initiate requests. |
I don't think servers have this problem already. Today, a server can always send a response to a request from the client, regardless of how many push streams they have opened. This is because the response streams are counted against the client's limit and the push streams are counted against the server's limit. (Or vice versa I guess, depending on how you think about it.)That would not be the case if the server's push and response streams were counted agains the same limit. |
A server that supports push does have the same problem. |
Hm. Can you provide an example? |
I think what Martin is saying is that the server is able, via PUSH_PROMISE, to queue up requests whose responses can't yet be sent. That's not quite the same as saying that the client's requests also can't be sent, but from the server's perspective it's very similar -- a request is a request. Having a reasonable initial priority on pushed streams, making them less important than client requests by default, helps to mitigate this. |
Ah. I see what you're getting at. I agree that there are some similarities here. Thanks! On the other hand, I think there are some significant differences. While it's clear that in the promise case, the server can promise streams that it is not ready to send, the server is in a position to know the relative importance of the various resources it wishes to push and can chose to send them in the order that is most useful to the client. However, the server is not in a position to know the relative importance of the pushed stream to the not-yet-received request(s) from the client. This puts the server in the position of being unable to service a high-priority request when it arrives. In the push case, the server could chose to pause sending on the open push streams to reply to the request. It could also chose not to promise a push of an important sub-resounce of that request because it knows that it is at the push stream limit. This would allow the client to request that subresource and reply via a non-push stream. |
So it seems like there are a couple stages here, and it depends how far we're willing to go.
Option 1 is the status quo. I strongly support moving at least to Option 2 for the simpler state management. Note that Option 2 still requires that we retain the concept of client-initiated and server-initiated streams. Option 3 is a possibility, but requires much more change to the application layer from what we currently deal with. It has some interesting possibilities, though. |
I think that I agree, I'm fairly confident that we could move to option 2 (coupled pairs of streams) without a lot of pain. The only binding we would need is that opening a stream in one direction opens a stream in the other direction. We'd want REQUEST_RST, or DISINTEREST or something to allow an endpoint to request that an inbound channel be cancelled. I think that we want that functionality anyway. Option 3 would require some extra signaling in the application protocol to properly realize, but it has some properties that I think are fascinating. The ramifications for push and stream limits and asymmetry remain interesting despite the apparent risks and uncertainty. The worst so far doesn't relate to asymmetry as much as it does to having an unbounded number of server pushes arriving at a client with only the connection flow control window to limit them. In some ways, that's an exciting prospect and I think that I like it, but I can also appreciate how some people might be scared by the notion. |
It'd be nice to have an implementation of option 3 before heading that direction, given it's a large departure from the status quo. But I won't disagree with it's appeal for cases like server push. |
@MikeBishop, your state machines in those only seem to deal with the sender state. The receiver state is more complicated because receipt of data on a higher-numbered stream also opens a stream. The coupled state machine (option 2) also requires that a stream be opened if its pair is opened. |
This is #872 with some additional exposition and one significant change. Client-initiated streams are now EVEN. Server initiated streams are now ODD. This is a pretty noticeable change, because it inverts the current sign, and might cause some disruption in implementations. However, it makes stream 0 work within the framework better. It wasn't clear what Ryan's intent was with respect to numbering, but I wouldn't have noticed this until I noticed a conflict between prose and the table. Closes #643, #656, #720, #872, #175. This does not close the other issues that were addressed by #643, because this doesn't include any changes in the HTTP draft. I'll create a follow-up PR for that.
QUIC borrows the HTTP/2 state machine, or tries to replicate it as much as it can. Part of that replication is the idea that a stream is bidirectional. The state related to outgoing data and the state related to inbound data are tied.
#174 suggests a simplification that could open up a more dramatic simplification: independent state for each direction. #171 moves in that direction also.
The state machine that results from that is dramatically simpler: idle -> open -> closed. That is, before packets, packets and after packets.
The consequence for the HTTP mapping is largely that it needs a different method for correlating request and response.
Correlating request and response
Currently, request and response are correlated by matching the stream identifier. However, unidirectional streams would allow for cases where streams are not created symmetrically. For instance, server push would only require streams from the server. Similarly, the server would not be obligated to respond to requests in the order in which they were received (if at all, since it could reset request streams).
That implies that this would add a requirement for the server to identify which request a given stream is answering. This is an extension of the requirement in #174, which only required that the server identify a push promise (actually, it's less onerous, because you only have one request per stream).
I can imagine an arrangement where a stream containing headers identifies the stream containing the request headers from the client. Then the stream containing the body identifies the stream that contained the headers.
Saving stream identifiers
This is a minor point, though it could be relevant for very long-lived connections.
QUIC currently insists on a strictly monotonically increasing stream identifier: stream N is followed by stream N+2 always, no skipping of streams is permitted. Aside from wasting every other stream identifier, other streams are wasted by the mapping.
GET requests don't include a body, but nonetheless require that the client send a HEADERS on stream N, then a FIN on stream N+2. Including a reference to the initiating stream at the start of the body of a message would use some octets, but potentially fewer than would be used otherwise (particularly if the reference was a varlen encoding of the difference in stream numbers, which would rarely be anything other than 1).
Obviously, this is an optimization for the case where there are no bodies, but that is a case where the optimization is particularly valuable. Making more requests (which more frequently have no bodies) during the early stages of a connection is critical to performance.
The text was updated successfully, but these errors were encountered: