/*
* Copyright 2014 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 static io.netty.handler.codec.http2.DefaultHttp2LocalFlowController.DEFAULT_WINDOW_UPDATE_RATIO;
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_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.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.concurrent.EventExecutor;
import junit.framework.AssertionFailedError;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link DefaultHttp2LocalFlowController}.
*/
public class DefaultHttp2LocalFlowControllerTest {
private static final int STREAM_ID = 1;
private DefaultHttp2LocalFlowController controller;
@Mock
private Http2FrameWriter frameWriter;
@Mock
private ChannelHandlerContext ctx;
@Mock
private EventExecutor executor;
@Mock
private ChannelPromise promise;
private DefaultHttp2Connection connection;
@Before
public void setup() throws Http2Exception {
MockitoAnnotations.initMocks(this);
when(ctx.newPromise()).thenReturn(promise);
when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
when(ctx.executor()).thenReturn(executor);
when(executor.inEventLoop()).thenReturn(true);
initController(false);
}
@Test
public void dataFrameShouldBeAccepted() throws Http2Exception {
receiveFlowControlledFrame(STREAM_ID, 10, 0, false);
verifyWindowUpdateNotSent();
}
@Test
public void windowUpdateShouldSendOnceBytesReturned() throws Http2Exception {
int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
// Return only a few bytes and verify that the WINDOW_UPDATE hasn't been sent.
assertFalse(consumeBytes(STREAM_ID, 10));
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
// Return the rest and verify the WINDOW_UPDATE is sent.
assertTrue(consumeBytes(STREAM_ID, dataSize - 10));
verifyWindowUpdateSent(STREAM_ID, dataSize);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
verifyNoMoreInteractions(frameWriter);
}
@Test
public void connectionWindowShouldAutoRefillWhenDataReceived() throws Http2Exception {
// Reconfigure controller to auto-refill the connection window.
initController(true);
int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
// Verify that we immediately refill the connection window.
verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
// Return only a few bytes and verify that the WINDOW_UPDATE hasn't been sent for the stream.
assertFalse(consumeBytes(STREAM_ID, 10));
verifyWindowUpdateNotSent(STREAM_ID);
// Return the rest and verify the WINDOW_UPDATE is sent for the stream.
assertTrue(consumeBytes(STREAM_ID, dataSize - 10));
verifyWindowUpdateSent(STREAM_ID, dataSize);
verifyNoMoreInteractions(frameWriter);
}
@Test(expected = Http2Exception.class)
public void connectionFlowControlExceededShouldThrow() throws Http2Exception {
// Window exceeded because of the padding.
receiveFlowControlledFrame(STREAM_ID, DEFAULT_WINDOW_SIZE, 1, true);
}
@Test
public void windowUpdateShouldNotBeSentAfterEndOfStream() throws Http2Exception {
int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
// Set end-of-stream on the frame, so no window update will be sent for the stream.
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, true);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
verifyWindowUpdateNotSent(STREAM_ID);
assertTrue(consumeBytes(STREAM_ID, dataSize));
verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
verifyWindowUpdateNotSent(STREAM_ID);
}
@Test
public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception {
int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
int initialWindowSize = DEFAULT_WINDOW_SIZE;
int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
// Don't set end-of-stream so we'll get a window update for the stream as well.
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
assertTrue(consumeBytes(STREAM_ID, dataSize));
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
verifyWindowUpdateSent(STREAM_ID, windowDelta);
}
@Test
public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception {
// Send a frame that takes up the entire window.
int initialWindowSize = DEFAULT_WINDOW_SIZE;
receiveFlowControlledFrame(STREAM_ID, initialWindowSize, 0, false);
assertEquals(0, window(STREAM_ID));
assertEquals(0, window(CONNECTION_STREAM_ID));
consumeBytes(STREAM_ID, initialWindowSize);
assertEquals(initialWindowSize, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
// Update the initial window size to allow another frame.
int newInitialWindowSize = 2 * initialWindowSize;
controller.initialWindowSize(newInitialWindowSize);
assertEquals(newInitialWindowSize, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
// Clear any previous calls to the writer.
reset(frameWriter);
// Send the next frame and verify that the expected window updates were sent.
receiveFlowControlledFrame(STREAM_ID, initialWindowSize, 0, false);
assertTrue(consumeBytes(STREAM_ID, initialWindowSize));
int delta = newInitialWindowSize - initialWindowSize;
verifyWindowUpdateSent(STREAM_ID, delta);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, delta);
}
@Test
public void connectionWindowShouldAdjustWithMultipleStreams() throws Http2Exception {
int newStreamId = 3;
connection.local().createStream(newStreamId, false);
try {
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
// Test that both stream and connection window are updated (or not updated) together
int data1 = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
receiveFlowControlledFrame(STREAM_ID, data1, 0, false);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(CONNECTION_STREAM_ID));
assertTrue(consumeBytes(STREAM_ID, data1));
verifyWindowUpdateSent(STREAM_ID, data1);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, data1);
reset(frameWriter);
// Create a scenario where data is depleted from multiple streams, but not enough data
// to generate a window update on those streams. The amount will be enough to generate
// a window update for the connection stream.
--data1;
int data2 = data1 >> 1;
receiveFlowControlledFrame(STREAM_ID, data1, 0, false);
receiveFlowControlledFrame(newStreamId, data1, 0, false);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(newStreamId);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(newStreamId));
assertEquals(DEFAULT_WINDOW_SIZE - (data1 << 1), window(CONNECTION_STREAM_ID));
assertFalse(consumeBytes(STREAM_ID, data1));
assertTrue(consumeBytes(newStreamId, data2));
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(newStreamId);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, data1 + data2);
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(newStreamId));
assertEquals(DEFAULT_WINDOW_SIZE - (data1 - data2), window(CONNECTION_STREAM_ID));
} finally {
connection.stream(newStreamId).close();
}
}
@Test
public void closeShouldConsumeBytes() throws Http2Exception {
receiveFlowControlledFrame(STREAM_ID, 10, 0, false);
assertEquals(10, controller.unconsumedBytes(connection.connectionStream()));
stream(STREAM_ID).close();
assertEquals(0, controller.unconsumedBytes(connection.connectionStream()));
}
@Test
public void closeShouldNotConsumeConnectionWindowWhenAutoRefilled() throws Http2Exception {
// Reconfigure controller to auto-refill the connection window.
initController(true);
receiveFlowControlledFrame(STREAM_ID, 10, 0, false);
assertEquals(0, controller.unconsumedBytes(connection.connectionStream()));
stream(STREAM_ID).close();
assertEquals(0, controller.unconsumedBytes(connection.connectionStream()));
}
@Test
public void dataReceivedForClosedStreamShouldImmediatelyConsumeBytes() throws Http2Exception {
Http2Stream stream = stream(STREAM_ID);
stream.close();
receiveFlowControlledFrame(stream, 10, 0, false);
assertEquals(0, controller.unconsumedBytes(connection.connectionStream()));
}
@Test
public void dataReceivedForNullStreamShouldImmediatelyConsumeBytes() throws Http2Exception {
receiveFlowControlledFrame(null, 10, 0, false);
assertEquals(0, controller.unconsumedBytes(connection.connectionStream()));
}
@Test
public void consumeBytesForNullStreamShouldIgnore() throws Http2Exception {
controller.consumeBytes(null, 10);
assertEquals(0, controller.unconsumedBytes(connection.connectionStream()));
}
@Test
public void globalRatioShouldImpactStreams() throws Http2Exception {
float ratio = 0.6f;
controller.windowUpdateRatio(ratio);
testRatio(ratio, DEFAULT_WINDOW_SIZE << 1, 3, false);
}
@Test
public void streamlRatioShouldImpactStreams() throws Http2Exception {
float ratio = 0.6f;
testRatio(ratio, DEFAULT_WINDOW_SIZE << 1, 3, true);
}
@Test
public void consumeBytesForZeroNumBytesShouldIgnore() throws Http2Exception {
assertFalse(controller.consumeBytes(connection.stream(STREAM_ID), 0));
}
@Test(expected = IllegalArgumentException.class)
public void consumeBytesForNegativeNumBytesShouldFail() throws Http2Exception {
assertFalse(controller.consumeBytes(connection.stream(STREAM_ID), -1));
}
private void testRatio(float ratio, int newDefaultWindowSize, int newStreamId, boolean setStreamRatio)
throws Http2Exception {
int delta = newDefaultWindowSize - DEFAULT_WINDOW_SIZE;
controller.incrementWindowSize(stream(0), delta);
Http2Stream stream = connection.local().createStream(newStreamId, false);
if (setStreamRatio) {
controller.windowUpdateRatio(stream, ratio);
}
controller.incrementWindowSize(stream, delta);
reset(frameWriter);
try {
int data1 = (int) (newDefaultWindowSize * ratio) + 1;
int data2 = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) >> 1;
receiveFlowControlledFrame(STREAM_ID, data2, 0, false);
receiveFlowControlledFrame(newStreamId, data1, 0, false);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(newStreamId);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
assertEquals(DEFAULT_WINDOW_SIZE - data2, window(STREAM_ID));
assertEquals(newDefaultWindowSize - data1, window(newStreamId));
assertEquals(newDefaultWindowSize - data2 - data1, window(CONNECTION_STREAM_ID));
assertFalse(consumeBytes(STREAM_ID, data2));
assertTrue(consumeBytes(newStreamId, data1));
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateSent(newStreamId, data1);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, data1 + data2);
assertEquals(DEFAULT_WINDOW_SIZE - data2, window(STREAM_ID));
assertEquals(newDefaultWindowSize, window(newStreamId));
assertEquals(newDefaultWindowSize, window(CONNECTION_STREAM_ID));
} finally {
connection.stream(newStreamId).close();
}
}
private static int getWindowDelta(int initialSize, int windowSize, int dataSize) {
int newWindowSize = windowSize - dataSize;
return initialSize - newWindowSize;
}
private void receiveFlowControlledFrame(int streamId, int dataSize, int padding,
boolean endOfStream) throws Http2Exception {
receiveFlowControlledFrame(stream(streamId), dataSize, padding, endOfStream);
}
private void receiveFlowControlledFrame(Http2Stream stream, int dataSize, int padding,
boolean endOfStream) throws Http2Exception {
final ByteBuf buf = dummyData(dataSize);
try {
controller.receiveFlowControlledFrame(stream, buf, padding, endOfStream);
} finally {
buf.release();
}
}
private static ByteBuf dummyData(int size) {
final ByteBuf buffer = Unpooled.buffer(size);
buffer.writerIndex(size);
return buffer;
}
private boolean consumeBytes(int streamId, int numBytes) throws Http2Exception {
return controller.consumeBytes(stream(streamId), numBytes);
}
private void verifyWindowUpdateSent(int streamId, int windowSizeIncrement) {
verify(frameWriter).writeWindowUpdate(eq(ctx), eq(streamId), eq(windowSizeIncrement), eq(promise));
}
private void verifyWindowUpdateNotSent(int streamId) {
verify(frameWriter, never()).writeWindowUpdate(eq(ctx), eq(streamId), anyInt(), eq(promise));
}
private void verifyWindowUpdateNotSent() {
verify(frameWriter, never()).writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt(),
any(ChannelPromise.class));
}
private int window(int streamId) {
return controller.windowSize(stream(streamId));
}
private Http2Stream stream(int streamId) {
return connection.stream(streamId);
}
private void initController(boolean autoRefillConnectionWindow) throws Http2Exception {
connection = new DefaultHttp2Connection(false);
controller = new DefaultHttp2LocalFlowController(connection,
DEFAULT_WINDOW_UPDATE_RATIO, autoRefillConnectionWindow).frameWriter(frameWriter);
connection.local().flowController(controller);
connection.local().createStream(STREAM_ID, false);
controller.channelHandlerContext(ctx);
}
}