/* * 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; import static org.mockito.Mockito.doAnswer; public class WeightedFairQueueByteDistributorDependencyTreeTest extends AbstractWeightedFairQueueByteDistributorDependencyTest { 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; @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); connection.forEachActiveStream(new Http2StreamVisitor() { @Override public boolean visit(Http2Stream stream) throws Http2Exception { streamA.close(); setPriority(streamB.id(), streamC.id(), DEFAULT_PRIORITY_WEIGHT, false); 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)); } @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 }; 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())); } // Unknown parent streams can come about in two ways: // 1. Because the stream is old and its state was purged // 2. This is the first reference to the stream, as implied at least by RFC7540§5.3.1: // > A dependency on a stream that is not currently in the tree — such as a stream in the // > "idle" state — results in that stream being given a default priority @Test public void unknownParentShouldBeCreatedUnderConnection() throws Exception { setup(5); // Purposefully avoid creating streamA's Http2Stream so that is it completely unknown. // It shouldn't matter whether the ID is before or after streamB.id() int streamAId = 1; Http2Stream streamB = connection.local().createStream(3, false); assertEquals(1, distributor.numChildren(connection.connectionStream().id())); assertEquals(0, distributor.numChildren(streamB.id())); // Build the tree setPriority(streamB.id(), streamAId, DEFAULT_PRIORITY_WEIGHT, false); assertEquals(1, connection.numActiveStreams()); // Level 0 assertEquals(1, distributor.numChildren(connection.connectionStream().id())); // Level 1 assertTrue(distributor.isChild(streamAId, connection.connectionStream().id(), DEFAULT_PRIORITY_WEIGHT)); assertEquals(1, distributor.numChildren(streamAId)); // Level 2 assertTrue(distributor.isChild(streamB.id(), streamAId, DEFAULT_PRIORITY_WEIGHT)); assertEquals(0, distributor.numChildren(streamB.id())); } }