-
Notifications
You must be signed in to change notification settings - Fork 205
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
Define initial PRIORITY frame and remove exclusive dependencies #2075
Conversation
(I considered making this a stream header on request streams, but since it's optional, decided a frame type was probably more appropriate. I'm open to being convinced otherwise.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if the exclusive thing need to be bundled in here.
There needs to be more text in this about managing races with PRIORITY frames. You have resolved the question of which one takes precedence, but you should mention that the identified dependency might have default priority when this is received, resulting in potentially inconsistent prioritization. Not a big problem, but worth noting I think.
@ddragana, do you know if we use the exclusive thing in our priority setup at all?
@@ -496,13 +496,9 @@ The PRIORITY frame payload has the following fields: | |||
: A two-bit field indicating the type of element being depended on. | |||
|
|||
Empty: | |||
: A three-bit field which MUST be zero when sent and MUST be ignored | |||
: A four-bit field which MUST be zero when sent and MUST be ignored | |||
on receipt. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really for this, but do we have to mandate ignoring reserved fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't, you risk an overzealous implementation enforcing the "MUST be zero" and we can't use it later without negotiation. "MUST ignore" seems cleaner than "MUST not die in flames if it's not zero."
draft-ietf-quic-http.md
Outdated
The INITIAL_PRIORITY frame payload has the following fields: | ||
|
||
Dependency Type: | ||
: A two-bit field indicating the type of element being depended on. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The alignment of the dependency type in comparison to PRIORITY is unfortunate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True -- I considered having a separate two-bit empty field, but that also felt unfortunate. If you think that's the better of two evils, I'm fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering the fact that we are changing the definition of the PRIORITY frame, it might make sense to change the order of the flags and fields as well.
Specifically, we can move the PT to the end of the flags and PEID to the end of the fields. That would help us having a unified decoder for the two frames. Additionally, we might call PEID an optional field that only exists when the frame is sent over a control stream, thereby unifiying the frame definition to one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One alternative would to have a static two-bit field that is always set to the value of 11
. There's no extra overhead since there's unused reserved bits.
However, I like @kazuho's proposal and think there is room to extend it further.
The PRIORITY frame definition says that
Element Dependency ID: A variable-length integer that identifies the element on which a dependency is being expressed. Depending on the value of Dependency Type, this contains the Stream ID of a request stream, the Push ID of a promised resource, the Placeholder ID of a placeholder, or is ignored.
IIUC the ignored case properly, wouldn't it be more efficient to make the EDID (chuckle, not this one) optional too, based on the prioritisation type. I.e. if the dependency is root of the tree, don't send an ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That actually could combine nicely -- rather than 11
meaning "the root of the tree," it means "the current stream," and we keep the restriction that reprioritizing the root of the tree is illegal. So 11
means this stream when it's send on a request, but means the root when it's sent on the control stream.
I was trying not to have conditionally-present fields, but if folks are okay with that, I could try that variant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using 11
to identify the current stream (and therefore the omission of the field) makes sense.
I was trying not to have conditionally-present fields, but if folks are okay with that, I could try that variant.
FWIW, the reason I am happy with having conditionally-present fields is that it saves bytes: omitting PEID in the INITIAL_PRIORITY frame saves one byte per request in the common, omitting EDID in the PRIORITY frame saves at least one byte in the first flight when placeholders are used.
I think omitting having bytes with no meaning is more consistent to the efforts we have spent in other parts of the transport and the h3 drafts to minimize the overhead.
The necessity of the Exclusive change might be debatable, but I think it is needed. Without Exclusive, there's an obvious race -- INITIAL_PRIORITY is racing with an Exclusive dependency on the parent, and the outcome on the server is indeterminate. With the exclusive change, it's possible to make the outcome of this addition deterministic. |
With HTTP/2, the initial HEADERS frame carries the priority information for the request. I wonder if instead of introducing an INITIAL_PRIORITY frame it would be simpler to bundle this initial priority information into the HEADERS frame instead. That would have the advantage of matching HTTP/2. |
I am afraid that creates a race condition. If a PRIORITY frame of stream X says that it depends on stream Y, and if PRIORITY frame of stream Y says it depends on X, the server cannot tell which to apply first. IIUC, the observation behind this PR is that the ordering between INITIAL_PRIORITY and PRIORITY frames can be recovered; i.e., any reference to a stream from a PRIORITY frame is guaranteed to occur after the INITIAL_PRIORITY frame of that frame is issued. |
draft-ietf-quic-http.md
Outdated
@@ -825,6 +821,70 @@ MUST be treated as a connection error of type HTTP_MALFORMED_FRAME. | |||
A server MUST treat a MAX_PUSH_ID frame payload that does not contain a single | |||
variable-length integer as a connection error of type HTTP_MALFORMED_FRAME. | |||
|
|||
### INITIAL_PRIORITY {#frame-initial-priority} | |||
|
|||
The INITIAL_PRIORITY (type=0x0F) frame specifies the client-advised priority of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To maintain consistency with PRIORITY frame, you could use "sender-advised priority". Language elsewhere qualifies that the only sender that is allowed is a client.
or,
change it to client-advised in PRIORITY
draft-ietf-quic-http.md
Outdated
INITIAL_PRIORITY frame sent on a non-request stream MUST be treated as a | ||
connection error of type HTTP_WRONG_STREAM. An INITIAL_PRIORITY frame received | ||
after other frames or received by a client MUST be treated as a stream error of | ||
type HTTP_UNEXPECTED_FRAME. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion to break this out
INITIAL_PRIORITY frames MAY be sent as the first frame of a
request stream and MUST NOT be sent thereafter. An INITIAL_PRIORITY
frame received after other frames MUST be treated as a stream error of
type HTTP_UNEXPECTED_FRAME.
INITIAL_PRIORITY frames sent on a non-request stream MUST be treated as a
connection error of type HTTP_WRONG_STREAM.
INITIAL_PRIORITY frames received by a client MUST be treated as a stream error of
type HTTP_UNEXPECTED_FRAME.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now wondering if it would it be clearer to say:
INITIAL_PRIORITY frames are OPTIONAL. When used, the frame MUST be sent as the first frame of a request stream. An INITIAL_PRIORITY frame received after other frames MUST be treated as a stream error of type HTTP_UNEXPECTED_FRAME.
draft-ietf-quic-http.md
Outdated
is being expressed. Depending on the value of Dependency Type, this contains | ||
the Stream ID of a request stream, the Push ID of a promised resource, the | ||
Placeholder ID of a placeholder, or is ignored. For details of | ||
dependencies, see {{priority}} and {{!RFC7540}}, Section 5.3. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore me...
draft-ietf-quic-http.md
Outdated
|
||
When an INITIAL_PRIORITY frame claims to reference a request, the associated ID | ||
MUST identify a client-initiated bidirectional stream. A server MUST treat | ||
receipt of PRIORITY frame with a Stream ID of any other type as a connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/PRIORITY frame/INITIAL_PRIORITY frame
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the general theme of the PR is good. I've made some minor suggestions.
The big question is: new frame or rework existing PRIORITY frame. I'll see how that conversation develops.
I considered making it part of either HEADERS or simply a stream header like the Push ID, since it MUST always come first. My reasoning was that, since it's not always going to be used, a stream header would be overkill. The conditional presence in HEADERS in H2 was based on flags, which we don't have any more 😳, so that seemed to present its own problems, plus the restriction that it only appear on the first HEADERS of a stream. It seemed simpler to restrict those semantics to their own frame type. |
+1 to having PRIORITY as a separate frame on the request stream. IMO, we should refrain from wasting bytes of a HEADERS frame, because it is used for carrying the responses as well. Bandwidth from the server to the client is the most precious resource in HTTP. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a nice improvement to me.
In addition to guaranteeing that the server would have the prioritization information, the proposed change saves multiple bytes (in addition to the points I clarified in #2075 (comment), we also save at least 3 bytes per packet by removing the need to emit a STREAM frame of a control stream when sending requests).
b220a6c
to
1920b13
Compare
Lgtm |
It's worth noting one of @kazuho's earlier comments raises a race condition that still appears possible here: Request A or Control: PRIORITY( A -> B ) These two initial frames race on parallel streams. Which is the child of the other? When A becomes a child of C, does B come with it? |
What if we duplicate the priority information on the control stream? If the priority of the new stream has already been set by the control stream, ignore the priority information on the request stream. |
Yes, I believe that ensures there's a complete ordering and eventual consistency. Even if an initial PRIORITY frame on a different stream gets things off-kilter briefly, the control stream will ensure the correct order wins in the end. With a quick exploration of the sequence above, I can't find one in which the end state is incorrect. Should that be "MAY/SHOULD repeat the desired prioritization on the control stream if dependencies are modified in close succession," or do you think repeating it should be a hard requirement? |
I think a hard requirement is too expensive. The only reason to duplicate the priority information from the request stream is if the priority changes, otherwise it's a waste. Case 1: superfluous duplication Request A: PRIORITY( A -> B ) Case 2: race possible Request A: PRIORITY( A -> B ) Case 3: ensure eventual consistency Request A: PRIORITY( A -> B ) My guess is that most clients will fall under Case 1. If request priority changes, the client should use Case 3. |
FWIW, I do not think we have a race here. For the case of:
I am not sure if that would ever happen. Having request A's initial PRIORITY specifying B as it parent and request B's initial PRIORITY specifying A means that there was a loop when the client created the two streams. That's a peculiar behavior of a client and I do not think that we need to address that. For the case of:
Under the premise that the client adds requests to the priority tree one by one and at the same time emits a INITIAL_PRIORITY frame, and that further modifications are signaled using the control streams, a server can determine the order of this pattern; PRIORITY(B -> A) predated PRIORITY(A -> B). |
For your second, there's no loop. B depends on A, regardless of A's ancestry. B might attach to A before or after it gets reparented, but that's fine. For the first, though, there's defined behavior (in 7540) of creating a dependency on a descendant -- that descendant implicitly moves up to become a peer, then the dependency stated in the priority frame is created. A client trying to leverage this behavior could produce that sequence. Another option would be to instead say that declaring a dependency on a descendant is always an error, and you need to explicitly perform the elevation-to-peer step (on the control stream). That would be consistent with removing exclusive dependencies, because it's eliminating another implicit mutation of the priority tree. |
I think I might have been unclear. My point is that PRIORITY(A -> B) carried on a request stream implies that stream B existed when A was created, and that PRIORITY(B -> A) implies the reverse. Because streams are created in order, that would never happen "under the premise that the client adds requests to the priority tree one by one and at the same time emits a INITIAL_PRIORITY frame". (copied the last subsentence from my comment above that explained the premise for the different case) |
I see the argument that a well-constructed client will not emit an invalid pattern. 🙂 It's not obvious to me how to turn that into a rule the server would use for interpretation or prohibition of those patterns. Maybe a prohibition on referencing higher stream numbers in the initial PRIORITY frame? |
👍 I like the idea of banning referring to higher stream numbers in the initial PRIORITY frame. My argument was simply that we can ignore the case (because nobody would do that), but I agree that it would be even better if we can detect and prohibit such misuse. |
Updated -- PTAL. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MikeBishop Thank you for the changes. The PR looks fine modulo the concerns stated below.
draft-ietf-quic-http.md
Outdated
HTTP_MALFORMED_FRAME. Likewise, a PRIORITY frame sent on a control stream that | ||
prioritizes the current stream MUST be treated as a connection error of type | ||
HTTP_MALFORMED_FRAME. | ||
prioritizes any other stream or expresses a dependency on a request with a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if we need "prioritizes any other stream or".
Now that exclusive flag is going away, is there any case that could hit this condition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a perhaps-unclear reference to a PRIORITY frame where Prioritized Element Type
is not 11
(i.e. the thing being prioritized is anything other than "this stream").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. Thank you for the clarification. I missed that.
My preference goes to stating that Prioritized Element Type
MUST be 11
when the frame is sent on a request stream and that it MUST NOT be 11
when the frame is sent on a control stream.
This is because I prefer to avoid having multiple representation that means the same; under the current provision, it seems that PEID could be encoded in two ways for a PRIORITY frame sent on a request stream; either set Prioritized Element Type to 11
or set it to 00
and set Prioritized Element ID to the ID of the stream. I fear that we might see interoperability issues in the wild if most of the clients use the 11
form but some used the 00
form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. The requirement for the values was already there (in the definition of Prioritized Element Type), but I'll scope the required-error language to the specific values as well.
draft-ietf-quic-http.md
Outdated
greater Stream ID than the current stream MUST be treated as a stream error of | ||
type HTTP_MALFORMED_FRAME. Likewise, a PRIORITY frame sent on a control stream | ||
that prioritizes the current stream MUST be treated as a connection error of | ||
type HTTP_MALFORMED_FRAME. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what the intent of the last sentence is.
My understanding is that a PRIORITY frame can only refer to a "Stream ID of a request stream" (assuming that the type bits are 00
(i.e. Request stream). Therefore, I think that we do not need this sentence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the inverse of the above -- sending 11
on a control stream, indicating that the thing being prioritized is the control stream itself.
Fixes #1865. The observations from the issue discussion are:
In the absence of exclusive dependencies, it's possible for a request stream to carry its own initial priority, as @dtikhonov proposed. This is guaranteed to be the first time that a priority is assigned to this request, and subsequent PRIORITY frames always override it. (If a PRIORITY frame gets reordered ahead, this initial PRIORITY is ignored.)
This is a further departure from RFC 7540's priority scheme, but as it appears exclusive priorities and reordering are somewhat incompatible, I believe is still within our design space.