netty5/codec-http2/src/test/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributorDependencyTreeTest.java

903 lines
43 KiB
Java
Raw Normal View History

HTTP/2 Non Active Stream RFC Corrections Motivation: codec-http2 couples the dependency tree state with the remainder of the stream state (Http2Stream). This makes implementing constraints where stream state and dependency tree state diverge in the RFC challenging. For example the RFC recommends retaining dependency tree state after a stream transitions to closed [1]. Dependency tree state can be exchanged on streams in IDLE. In practice clients may use stream IDs for the purpose of establishing QoS classes and therefore retaining this dependency tree state can be important to client perceived performance. It is difficult to limit the total amount of state we retain when stream state and dependency tree state is combined. Modifications: - Remove dependency tree, priority, and weight related items from public facing Http2Connection and Http2Stream APIs. This information is optional to track and depends on the flow controller implementation. - Move all dependency tree, priority, and weight related code from DefaultHttp2Connection to WeightedFairQueueByteDistributor. This is currently the only place which cares about priority. We can pull out the dependency tree related code in the future if it is generally useful to expose for other implementations. - DefaultHttp2Connection should explicitly limit the number of reserved streams now that IDLE streams are no longer created. Result: More compliant with the HTTP/2 RFC. Fixes https://github.com/netty/netty/issues/6206. [1] https://tools.ietf.org/html/rfc7540#section-5.3.4
2017-01-24 21:50:39 +01:00
/*
* Copyright 2017 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockitoAnnotations;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
import static io.netty.handler.codec.http2.WeightedFairQueueByteDistributor.INITIAL_CHILDREN_MAP_SIZE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
HTTP/2 Non Active Stream RFC Corrections Motivation: codec-http2 couples the dependency tree state with the remainder of the stream state (Http2Stream). This makes implementing constraints where stream state and dependency tree state diverge in the RFC challenging. For example the RFC recommends retaining dependency tree state after a stream transitions to closed [1]. Dependency tree state can be exchanged on streams in IDLE. In practice clients may use stream IDs for the purpose of establishing QoS classes and therefore retaining this dependency tree state can be important to client perceived performance. It is difficult to limit the total amount of state we retain when stream state and dependency tree state is combined. Modifications: - Remove dependency tree, priority, and weight related items from public facing Http2Connection and Http2Stream APIs. This information is optional to track and depends on the flow controller implementation. - Move all dependency tree, priority, and weight related code from DefaultHttp2Connection to WeightedFairQueueByteDistributor. This is currently the only place which cares about priority. We can pull out the dependency tree related code in the future if it is generally useful to expose for other implementations. - DefaultHttp2Connection should explicitly limit the number of reserved streams now that IDLE streams are no longer created. Result: More compliant with the HTTP/2 RFC. Fixes https://github.com/netty/netty/issues/6206. [1] https://tools.ietf.org/html/rfc7540#section-5.3.4
2017-01-24 21:50:39 +01:00
import static org.mockito.Mockito.doAnswer;
public class WeightedFairQueueByteDistributorDependencyTreeTest extends
AbstractWeightedFairQueueByteDistributorDependencyTest {
@Before
public void setup() throws Http2Exception {
MockitoAnnotations.initMocks(this);
setup(0);
}
private void setup(int maxStateOnlySize) {
connection = new DefaultHttp2Connection(false);
distributor = new WeightedFairQueueByteDistributor(connection, maxStateOnlySize);
// Assume we always write all the allocated bytes.
doAnswer(writeAnswer(false)).when(writer).write(any(Http2Stream.class), anyInt());
}
@Test
public void closingStreamWithChildrenDoesNotCauseConcurrentModification() throws Http2Exception {
// We create enough streams to wrap around the child array. We carefully craft the stream ids so that they hash
// codes overlap with respect to the child collection. If the implementation is not careful this may lead to a
// concurrent modification exception while promoting all children to the connection stream.
final Http2Stream streamA = connection.local().createStream(1, false);
final int numStreams = INITIAL_CHILDREN_MAP_SIZE - 1;
for (int i = 0, streamId = 3; i < numStreams; ++i, streamId += INITIAL_CHILDREN_MAP_SIZE) {
final Http2Stream stream = connection.local().createStream(streamId, false);
setPriority(stream.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
}
assertEquals(INITIAL_CHILDREN_MAP_SIZE, connection.numActiveStreams());
streamA.close();
assertEquals(numStreams, connection.numActiveStreams());
}
@Test
public void closeWhileIteratingDoesNotNPE() throws Http2Exception {
final Http2Stream streamA = connection.local().createStream(3, false);
final Http2Stream streamB = connection.local().createStream(5, false);
final Http2Stream streamC = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
HTTP/2 Non Active Stream RFC Corrections Motivation: codec-http2 couples the dependency tree state with the remainder of the stream state (Http2Stream). This makes implementing constraints where stream state and dependency tree state diverge in the RFC challenging. For example the RFC recommends retaining dependency tree state after a stream transitions to closed [1]. Dependency tree state can be exchanged on streams in IDLE. In practice clients may use stream IDs for the purpose of establishing QoS classes and therefore retaining this dependency tree state can be important to client perceived performance. It is difficult to limit the total amount of state we retain when stream state and dependency tree state is combined. Modifications: - Remove dependency tree, priority, and weight related items from public facing Http2Connection and Http2Stream APIs. This information is optional to track and depends on the flow controller implementation. - Move all dependency tree, priority, and weight related code from DefaultHttp2Connection to WeightedFairQueueByteDistributor. This is currently the only place which cares about priority. We can pull out the dependency tree related code in the future if it is generally useful to expose for other implementations. - DefaultHttp2Connection should explicitly limit the number of reserved streams now that IDLE streams are no longer created. Result: More compliant with the HTTP/2 RFC. Fixes https://github.com/netty/netty/issues/6206. [1] https://tools.ietf.org/html/rfc7540#section-5.3.4
2017-01-24 21:50:39 +01:00
connection.forEachActiveStream(new Http2StreamVisitor() {
@Override
public boolean visit(Http2Stream stream) throws Http2Exception {
streamA.close();
setPriority(streamB.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
HTTP/2 Non Active Stream RFC Corrections Motivation: codec-http2 couples the dependency tree state with the remainder of the stream state (Http2Stream). This makes implementing constraints where stream state and dependency tree state diverge in the RFC challenging. For example the RFC recommends retaining dependency tree state after a stream transitions to closed [1]. Dependency tree state can be exchanged on streams in IDLE. In practice clients may use stream IDs for the purpose of establishing QoS classes and therefore retaining this dependency tree state can be important to client perceived performance. It is difficult to limit the total amount of state we retain when stream state and dependency tree state is combined. Modifications: - Remove dependency tree, priority, and weight related items from public facing Http2Connection and Http2Stream APIs. This information is optional to track and depends on the flow controller implementation. - Move all dependency tree, priority, and weight related code from DefaultHttp2Connection to WeightedFairQueueByteDistributor. This is currently the only place which cares about priority. We can pull out the dependency tree related code in the future if it is generally useful to expose for other implementations. - DefaultHttp2Connection should explicitly limit the number of reserved streams now that IDLE streams are no longer created. Result: More compliant with the HTTP/2 RFC. Fixes https://github.com/netty/netty/issues/6206. [1] https://tools.ietf.org/html/rfc7540#section-5.3.4
2017-01-24 21:50:39 +01:00
return true;
}
});
}
@Test
public void localStreamCanDependUponIdleStream() throws Http2Exception {
setup(1);
Http2Stream streamA = connection.local().createStream(1, false);
setPriority(3, streamA.id(), MIN_WEIGHT, true);
assertTrue(distributor.isChild(3, streamA.id(), MIN_WEIGHT));
}
@Test
public void remoteStreamCanDependUponIdleStream() throws Http2Exception {
setup(1);
Http2Stream streamA = connection.remote().createStream(2, false);
setPriority(4, streamA.id(), MIN_WEIGHT, true);
assertTrue(distributor.isChild(4, streamA.id(), MIN_WEIGHT));
}
@Test
public void prioritizeShouldUseDefaults() throws Exception {
Http2Stream stream = connection.local().createStream(1, false);
assertTrue(distributor.isChild(stream.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
assertEquals(0, distributor.numChildren(stream.id()));
}
@Test
public void reprioritizeWithNoChangeShouldDoNothing() throws Exception {
Http2Stream stream = connection.local().createStream(1, false);
setPriority(stream.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT, false);
assertTrue(distributor.isChild(stream.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
assertEquals(0, distributor.numChildren(stream.id()));
}
@Test
public void stateOnlyPriorityShouldBePreservedWhenStreamsAreCreatedAndClosed() throws Http2Exception {
setup(3);
short weight3 = MIN_WEIGHT + 1;
short weight5 = (short) (weight3 + 1);
short weight7 = (short) (weight5 + 1);
setPriority(3, connection.connectionStream().id(), weight3, true);
setPriority(5, connection.connectionStream().id(), weight5, true);
setPriority(7, connection.connectionStream().id(), weight7, true);
assertEquals(0, connection.numActiveStreams());
verifyStateOnlyPriorityShouldBePreservedWhenStreamsAreCreated(weight3, weight5, weight7);
// Now create stream objects and ensure the state and dependency tree is preserved.
Http2Stream streamA = connection.local().createStream(3, false);
Http2Stream streamB = connection.local().createStream(5, false);
Http2Stream streamC = connection.local().createStream(7, false);
assertEquals(3, connection.numActiveStreams());
verifyStateOnlyPriorityShouldBePreservedWhenStreamsAreCreated(weight3, weight5, weight7);
// Close all the streams and ensure the state and dependency tree is preserved.
streamA.close();
streamB.close();
streamC.close();
assertEquals(0, connection.numActiveStreams());
verifyStateOnlyPriorityShouldBePreservedWhenStreamsAreCreated(weight3, weight5, weight7);
}
private void verifyStateOnlyPriorityShouldBePreservedWhenStreamsAreCreated(short weight3, short weight5,
short weight7) {
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(7, connection.connectionStream().id(), weight7));
assertEquals(1, distributor.numChildren(7));
// Level 2
assertTrue(distributor.isChild(5, 7, weight5));
assertEquals(1, distributor.numChildren(5));
// Level 3
assertTrue(distributor.isChild(3, 5, weight3));
assertEquals(0, distributor.numChildren(3));
}
private static final int leadersId = 3; // js, css
private static final int unblockedId = 5;
private static final int backgroundId = 7;
private static final int speculativeId = 9;
private static final int followersId = 11; // images
private static final short leadersWeight = 201;
private static final short unblockedWeight = 101;
private static final short backgroundWeight = 1;
private static final short speculativeWeight = 1;
private static final short followersWeight = 1;
@Test
public void fireFoxQoSStreamsRemainAfterDataStreamsAreClosed() throws Http2Exception {
// http://bitsup.blogspot.com/2015/01/http2-dependency-priorities-in-firefox.html
setup(5);
setPriority(leadersId, connection.connectionStream().id(), leadersWeight, false);
setPriority(unblockedId, connection.connectionStream().id(), unblockedWeight, false);
setPriority(backgroundId, connection.connectionStream().id(), backgroundWeight, false);
setPriority(speculativeId, backgroundId, speculativeWeight, false);
setPriority(followersId, leadersId, followersWeight, false);
verifyFireFoxQoSStreams();
// Simulate a HTML request
short htmlGetStreamWeight = 2;
Http2Stream htmlGetStream = connection.local().createStream(13, false);
setPriority(htmlGetStream.id(), followersId, htmlGetStreamWeight, false);
Http2Stream favIconStream = connection.local().createStream(15, false);
setPriority(favIconStream.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT, false);
Http2Stream cssStream = connection.local().createStream(17, false);
setPriority(cssStream.id(), leadersId, DEFAULT_PRIORITY_WEIGHT, false);
Http2Stream jsStream = connection.local().createStream(19, false);
setPriority(jsStream.id(), leadersId, DEFAULT_PRIORITY_WEIGHT, false);
Http2Stream imageStream = connection.local().createStream(21, false);
setPriority(imageStream.id(), followersId, 1, false);
// Level 0
assertEquals(4, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(leadersId, connection.connectionStream().id(), leadersWeight));
assertEquals(3, distributor.numChildren(leadersId));
assertTrue(distributor.isChild(unblockedId, connection.connectionStream().id(), unblockedWeight));
assertEquals(0, distributor.numChildren(unblockedId));
assertTrue(distributor.isChild(backgroundId, connection.connectionStream().id(), backgroundWeight));
assertEquals(1, distributor.numChildren(backgroundId));
assertTrue(distributor.isChild(favIconStream.id(), connection.connectionStream().id(),
DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(favIconStream.id()));
// Level 2
assertTrue(distributor.isChild(followersId, leadersId, followersWeight));
assertEquals(2, distributor.numChildren(followersId));
assertTrue(distributor.isChild(speculativeId, backgroundId, speculativeWeight));
assertEquals(0, distributor.numChildren(speculativeId));
assertTrue(distributor.isChild(cssStream.id(), leadersId, DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(cssStream.id()));
assertTrue(distributor.isChild(jsStream.id(), leadersId, DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(jsStream.id()));
// Level 3
assertTrue(distributor.isChild(htmlGetStream.id(), followersId, htmlGetStreamWeight));
assertEquals(0, distributor.numChildren(htmlGetStream.id()));
assertTrue(distributor.isChild(imageStream.id(), followersId, followersWeight));
assertEquals(0, distributor.numChildren(imageStream.id()));
// Close all the data streams and ensure the "priority only streams" are retained in the dependency tree.
htmlGetStream.close();
favIconStream.close();
cssStream.close();
jsStream.close();
imageStream.close();
verifyFireFoxQoSStreams();
}
private void verifyFireFoxQoSStreams() {
// Level 0
assertEquals(3, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(leadersId, connection.connectionStream().id(), leadersWeight));
assertEquals(1, distributor.numChildren(leadersId));
assertTrue(distributor.isChild(unblockedId, connection.connectionStream().id(), unblockedWeight));
assertEquals(0, distributor.numChildren(unblockedId));
assertTrue(distributor.isChild(backgroundId, connection.connectionStream().id(), backgroundWeight));
assertEquals(1, distributor.numChildren(backgroundId));
// Level 2
assertTrue(distributor.isChild(followersId, leadersId, followersWeight));
assertEquals(0, distributor.numChildren(followersId));
assertTrue(distributor.isChild(speculativeId, backgroundId, speculativeWeight));
assertEquals(0, distributor.numChildren(speculativeId));
}
@Test
public void lowestPrecedenceStateShouldBeDropped() throws Http2Exception {
setup(3);
short weight3 = MAX_WEIGHT;
short weight5 = (short) (weight3 - 1);
short weight7 = (short) (weight5 - 1);
short weight9 = (short) (weight7 - 1);
setPriority(3, connection.connectionStream().id(), weight3, true);
setPriority(5, connection.connectionStream().id(), weight5, true);
setPriority(7, connection.connectionStream().id(), weight7, false);
assertEquals(0, connection.numActiveStreams());
verifyLowestPrecedenceStateShouldBeDropped1(weight3, weight5, weight7);
// Attempt to create a new item in the dependency tree but the maximum amount of "state only" streams is meet
// so a stream will have to be dropped. Currently the new stream is the lowest "precedence" so it is dropped.
setPriority(9, 3, weight9, false);
assertEquals(0, connection.numActiveStreams());
verifyLowestPrecedenceStateShouldBeDropped1(weight3, weight5, weight7);
// Set the priority for stream 9 such that its depth in the dependency tree is numerically lower than stream 3,
// and therefore the dependency state associated with stream 3 will be dropped.
setPriority(9, 5, weight9, true);
verifyLowestPrecedenceStateShouldBeDropped2(weight9, weight5, weight7);
// Test that stream which has been activated is lower priority than other streams that have not been activated.
Http2Stream streamA = connection.local().createStream(5, false);
streamA.close();
verifyLowestPrecedenceStateShouldBeDropped2(weight9, weight5, weight7);
// Stream 3 (hasn't been opened) should result in stream 5 being dropped.
setPriority(3, 9, weight3, false);
verifyLowestPrecedenceStateShouldBeDropped3(weight3, weight7, weight9);
// Stream 5's state has been discarded so we should be able to re-insert this state.
setPriority(5, 0, weight5, false);
verifyLowestPrecedenceStateShouldBeDropped4(weight5, weight7, weight9);
// All streams are at the same level, so stream ID should be used to drop the numeric lowest valued stream.
short weight11 = (short) (weight9 - 1);
setPriority(11, 0, weight11, false);
verifyLowestPrecedenceStateShouldBeDropped5(weight7, weight9, weight11);
}
private void verifyLowestPrecedenceStateShouldBeDropped1(short weight3, short weight5, short weight7) {
// Level 0
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(7, connection.connectionStream().id(), weight7));
assertEquals(0, distributor.numChildren(7));
assertTrue(distributor.isChild(5, connection.connectionStream().id(), weight5));
assertEquals(1, distributor.numChildren(5));
// Level 2
assertTrue(distributor.isChild(3, 5, weight3));
assertEquals(0, distributor.numChildren(3));
}
private void verifyLowestPrecedenceStateShouldBeDropped2(short weight9, short weight5, short weight7) {
// Level 0
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(7, connection.connectionStream().id(), weight7));
assertEquals(0, distributor.numChildren(7));
assertTrue(distributor.isChild(5, connection.connectionStream().id(), weight5));
assertEquals(1, distributor.numChildren(5));
// Level 2
assertTrue(distributor.isChild(9, 5, weight9));
assertEquals(0, distributor.numChildren(9));
}
private void verifyLowestPrecedenceStateShouldBeDropped3(short weight3, short weight7, short weight9) {
// Level 0
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(7, connection.connectionStream().id(), weight7));
assertEquals(0, distributor.numChildren(7));
assertTrue(distributor.isChild(9, connection.connectionStream().id(), weight9));
assertEquals(1, distributor.numChildren(9));
// Level 2
assertTrue(distributor.isChild(3, 9, weight3));
assertEquals(0, distributor.numChildren(3));
}
private void verifyLowestPrecedenceStateShouldBeDropped4(short weight5, short weight7, short weight9) {
// Level 0
assertEquals(3, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(5, connection.connectionStream().id(), weight5));
assertEquals(0, distributor.numChildren(5));
assertTrue(distributor.isChild(7, connection.connectionStream().id(), weight7));
assertEquals(0, distributor.numChildren(7));
assertTrue(distributor.isChild(9, connection.connectionStream().id(), weight9));
assertEquals(0, distributor.numChildren(9));
}
private void verifyLowestPrecedenceStateShouldBeDropped5(short weight7, short weight9, short weight11) {
// Level 0
assertEquals(3, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(11, connection.connectionStream().id(), weight11));
assertEquals(0, distributor.numChildren(11));
assertTrue(distributor.isChild(7, connection.connectionStream().id(), weight7));
assertEquals(0, distributor.numChildren(7));
assertTrue(distributor.isChild(9, connection.connectionStream().id(), weight9));
assertEquals(0, distributor.numChildren(9));
}
@Test
public void priorityOnlyStreamsArePreservedWhenReservedStreamsAreClosed() throws Http2Exception {
setup(1);
short weight3 = MIN_WEIGHT;
setPriority(3, connection.connectionStream().id(), weight3, true);
Http2Stream streamA = connection.local().createStream(5, false);
Http2Stream streamB = connection.remote().reservePushStream(4, streamA);
// Level 0
assertEquals(3, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(3, connection.connectionStream().id(), weight3));
assertEquals(0, distributor.numChildren(3));
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamA.id()));
assertTrue(distributor.isChild(streamB.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
// Close both streams.
streamB.close();
streamA.close();
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(3, connection.connectionStream().id(), weight3));
assertEquals(0, distributor.numChildren(3));
}
@Test
public void insertExclusiveShouldAddNewLevel() throws Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(4, connection.numActiveStreams());
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamA.id()));
// Level 2
assertTrue(distributor.isChild(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamD.id()));
// Level 3
assertTrue(distributor.isChild(streamB.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamC.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamC.id()));
}
@Test
public void existingChildMadeExclusiveShouldNotCreateTreeCycle() throws Http2Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Stream C is already dependent on Stream A, but now make that an exclusive dependency
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(4, connection.numActiveStreams());
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamA.id()));
// Level 2
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamC.id()));
// Level 3
assertTrue(distributor.isChild(streamB.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamD.id()));
}
@Test
public void newExclusiveChildShouldUpdateOldParentCorrectly() throws Http2Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
Http2Stream streamE = connection.local().createStream(9, false);
Http2Stream streamF = connection.local().createStream(11, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamF.id(), streamE.id(), DEFAULT_PRIORITY_WEIGHT, false);
// F is now going to be exclusively dependent on A, after this we should check that stream E
// prioritizableForTree is not over decremented.
setPriority(streamF.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(6, connection.numActiveStreams());
// Level 0
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamE.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamE.id()));
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamA.id()));
// Level 2
assertTrue(distributor.isChild(streamF.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamF.id()));
// Level 3
assertTrue(distributor.isChild(streamB.id(), streamF.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamC.id(), streamF.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamC.id()));
// Level 4
assertTrue(distributor.isChild(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamD.id()));
}
@Test
public void weightChangeWithNoTreeChangeShouldBeRespected() throws Http2Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(4, connection.numActiveStreams());
short newWeight = (short) (DEFAULT_PRIORITY_WEIGHT + 1);
setPriority(streamD.id(), streamA.id(), newWeight, false);
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamA.id()));
// Level 2
assertTrue(distributor.isChild(streamD.id(), streamA.id(), newWeight));
assertEquals(2, distributor.numChildren(streamD.id()));
// Level 3
assertTrue(distributor.isChild(streamB.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamC.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamC.id()));
}
@Test
public void sameNodeDependentShouldNotStackOverflowNorChangePrioritizableForTree() throws Http2Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
boolean[] exclusives = { true, false };
short[] weights = { DEFAULT_PRIORITY_WEIGHT, 100, 200, DEFAULT_PRIORITY_WEIGHT };
HTTP/2 Non Active Stream RFC Corrections Motivation: codec-http2 couples the dependency tree state with the remainder of the stream state (Http2Stream). This makes implementing constraints where stream state and dependency tree state diverge in the RFC challenging. For example the RFC recommends retaining dependency tree state after a stream transitions to closed [1]. Dependency tree state can be exchanged on streams in IDLE. In practice clients may use stream IDs for the purpose of establishing QoS classes and therefore retaining this dependency tree state can be important to client perceived performance. It is difficult to limit the total amount of state we retain when stream state and dependency tree state is combined. Modifications: - Remove dependency tree, priority, and weight related items from public facing Http2Connection and Http2Stream APIs. This information is optional to track and depends on the flow controller implementation. - Move all dependency tree, priority, and weight related code from DefaultHttp2Connection to WeightedFairQueueByteDistributor. This is currently the only place which cares about priority. We can pull out the dependency tree related code in the future if it is generally useful to expose for other implementations. - DefaultHttp2Connection should explicitly limit the number of reserved streams now that IDLE streams are no longer created. Result: More compliant with the HTTP/2 RFC. Fixes https://github.com/netty/netty/issues/6206. [1] https://tools.ietf.org/html/rfc7540#section-5.3.4
2017-01-24 21:50:39 +01:00
assertEquals(4, connection.numActiveStreams());
// The goal is to call setPriority with the same parent and vary the parameters
// we were at one point adding a circular depends to the tree and then throwing
// a StackOverflow due to infinite recursive operation.
for (short weight : weights) {
for (boolean exclusive : exclusives) {
setPriority(streamD.id(), streamA.id(), weight, exclusive);
assertEquals(0, distributor.numChildren(streamB.id()));
assertEquals(0, distributor.numChildren(streamC.id()));
assertEquals(1, distributor.numChildren(streamA.id()));
assertEquals(2, distributor.numChildren(streamD.id()));
assertFalse(distributor.isChild(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertFalse(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertTrue(distributor.isChild(streamB.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertTrue(distributor.isChild(streamC.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertTrue(distributor.isChild(streamD.id(), streamA.id(), weight));
}
}
}
@Test
public void multipleCircularDependencyShouldUpdatePrioritizable() throws Http2Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(4, connection.numActiveStreams());
// Bring B to the root
setPriority(streamA.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, true);
// Move all streams to be children of B
setPriority(streamC.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Move A back to the root
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
// Move all streams to be children of A
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(3, distributor.numChildren(streamA.id()));
// Level 2
assertTrue(distributor.isChild(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamC.id()));
assertTrue(distributor.isChild(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamD.id()));
}
@Test
public void removeWithPrioritizableDependentsShouldNotRestructureTree() throws Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Default removal policy will cause it to be removed immediately.
streamB.close();
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamA.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamA.id()));
// Level 2
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamC.id()));
assertTrue(distributor.isChild(streamD.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamD.id()));
}
@Test
public void closeWithNoPrioritizableDependentsShouldRestructureTree() throws Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
Http2Stream streamE = connection.local().createStream(9, false);
Http2Stream streamF = connection.local().createStream(11, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Close internal nodes, leave 1 leaf node open, the only remaining stream is the one that is not closed (E).
streamA.close();
streamB.close();
streamC.close();
streamD.close();
streamF.close();
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamE.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamE.id()));
}
@Test
public void priorityChangeWithNoPrioritizableDependentsShouldRestructureTree() throws Exception {
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
Http2Stream streamE = connection.local().createStream(9, false);
Http2Stream streamF = connection.local().createStream(11, false);
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamC.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamD.id(), streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
setPriority(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Leave leaf nodes open (E & F)
streamA.close();
streamB.close();
streamC.close();
streamD.close();
// Move F to depend on C, even though C is closed.
setPriority(streamF.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Level 0
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamE.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamE.id()));
assertTrue(distributor.isChild(streamF.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamF.id()));
}
@Test
public void circularDependencyShouldRestructureTree() throws Exception {
// Using example from https://tools.ietf.org/html/rfc7540#section-5.3.3
// Initialize all the nodes
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
Http2Stream streamE = connection.local().createStream(9, false);
Http2Stream streamF = connection.local().createStream(11, false);
assertEquals(6, distributor.numChildren(connection.connectionStream().id()));
assertEquals(0, distributor.numChildren(streamA.id()));
assertEquals(0, distributor.numChildren(streamB.id()));
assertEquals(0, distributor.numChildren(streamC.id()));
assertEquals(0, distributor.numChildren(streamD.id()));
assertEquals(0, distributor.numChildren(streamE.id()));
assertEquals(0, distributor.numChildren(streamF.id()));
// Build the tree
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(5, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamA.id()));
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(4, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamA.id()));
setPriority(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(3, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamC.id()));
setPriority(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamC.id()));
setPriority(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamD.id()));
assertEquals(6, connection.numActiveStreams());
// Non-exclusive re-prioritization of a->d.
setPriority(streamA.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamD.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamD.id()));
// Level 2
assertTrue(distributor.isChild(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamF.id()));
assertTrue(distributor.isChild(streamA.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamA.id()));
// Level 3
assertTrue(distributor.isChild(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamC.id()));
// Level 4
assertTrue(distributor.isChild(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamE.id()));
}
@Test
public void circularDependencyWithExclusiveShouldRestructureTree() throws Exception {
// Using example from https://tools.ietf.org/html/rfc7540#section-5.3.3
// Initialize all the nodes
Http2Stream streamA = connection.local().createStream(1, false);
Http2Stream streamB = connection.local().createStream(3, false);
Http2Stream streamC = connection.local().createStream(5, false);
Http2Stream streamD = connection.local().createStream(7, false);
Http2Stream streamE = connection.local().createStream(9, false);
Http2Stream streamF = connection.local().createStream(11, false);
assertEquals(6, distributor.numChildren(connection.connectionStream().id()));
assertEquals(0, distributor.numChildren(streamA.id()));
assertEquals(0, distributor.numChildren(streamB.id()));
assertEquals(0, distributor.numChildren(streamC.id()));
assertEquals(0, distributor.numChildren(streamD.id()));
assertEquals(0, distributor.numChildren(streamE.id()));
assertEquals(0, distributor.numChildren(streamF.id()));
// Build the tree
setPriority(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(5, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamA.id()));
setPriority(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(4, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamA.id()));
setPriority(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(3, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamD.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamC.id()));
setPriority(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(2, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(2, distributor.numChildren(streamC.id()));
setPriority(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
assertTrue(distributor.isChild(streamF.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamD.id()));
assertEquals(6, connection.numActiveStreams());
// Exclusive re-prioritization of a->d.
setPriority(streamA.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT, true);
// Level 0
assertEquals(1, distributor.numChildren(connection.connectionStream().id()));
// Level 1
assertTrue(distributor.isChild(streamD.id(), connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamD.id()));
// Level 2
assertTrue(distributor.isChild(streamA.id(), streamD.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(3, distributor.numChildren(streamA.id()));
// Level 3
assertTrue(distributor.isChild(streamB.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamB.id()));
assertTrue(distributor.isChild(streamF.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamF.id()));
assertTrue(distributor.isChild(streamC.id(), streamA.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(1, distributor.numChildren(streamC.id()));
// Level 4;
assertTrue(distributor.isChild(streamE.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT));
assertEquals(0, distributor.numChildren(streamE.id()));
}
}