Remove explicit flushes from HTTP2 encoders, decoders & flow-controllers
Motivation: Allow users of HTTP2 to control when flushes occur so they can optimize network writes. Modifications: Removed explicit calls to flush in encoder, decoder & flow-controller Connection handler now calls flush on read-complete to enable batching writes in response to reads Result: Much less flushing occurs for normal HTTP2 request and response patterns.
This commit is contained in:
parent
ce8c916f1a
commit
8271c8afcc
@ -443,7 +443,6 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
|
|||||||
// Send an ack back to the remote client.
|
// Send an ack back to the remote client.
|
||||||
// Need to retain the buffer here since it will be released after the write completes.
|
// Need to retain the buffer here since it will be released after the write completes.
|
||||||
encoder.writePing(ctx, true, data.retain(), ctx.newPromise());
|
encoder.writePing(ctx, true, data.retain(), ctx.newPromise());
|
||||||
ctx.flush();
|
|
||||||
|
|
||||||
listener.onPingRead(ctx, data);
|
listener.onPingRead(ctx, data);
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,6 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture future = frameWriter.writePriority(ctx, streamId, streamDependency, weight, exclusive, promise);
|
ChannelFuture future = frameWriter.writePriority(ctx, streamId, streamDependency, weight, exclusive, promise);
|
||||||
ctx.flush();
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,21 +223,18 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture future = frameWriter.writeSettings(ctx, settings, promise);
|
ChannelFuture future = frameWriter.writeSettings(ctx, settings, promise);
|
||||||
ctx.flush();
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
|
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
|
||||||
ChannelFuture future = frameWriter.writeSettingsAck(ctx, promise);
|
ChannelFuture future = frameWriter.writeSettingsAck(ctx, promise);
|
||||||
ctx.flush();
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, ByteBuf data, ChannelPromise promise) {
|
public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, ByteBuf data, ChannelPromise promise) {
|
||||||
ChannelFuture future = frameWriter.writePing(ctx, ack, data, promise);
|
ChannelFuture future = frameWriter.writePing(ctx, ack, data, promise);
|
||||||
ctx.flush();
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +254,6 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture future = frameWriter.writePushPromise(ctx, streamId, promisedStreamId, headers, padding, promise);
|
ChannelFuture future = frameWriter.writePushPromise(ctx, streamId, promisedStreamId, headers, padding, promise);
|
||||||
ctx.flush();
|
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,16 +340,13 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean write(int allowedBytes) {
|
public void write(int allowedBytes) {
|
||||||
int bytesWritten = 0;
|
int bytesWritten = 0;
|
||||||
|
if (data == null || (allowedBytes == 0 && size != 0)) {
|
||||||
|
// No point writing an empty DATA frame, wait for a bigger allowance.
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (data == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (allowedBytes == 0 && size != 0) {
|
|
||||||
// No point writing an empty DATA frame, wait for a bigger allowance.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int maxFrameSize = frameWriter().configuration().frameSizePolicy().maxFrameSize();
|
int maxFrameSize = frameWriter().configuration().frameSizePolicy().maxFrameSize();
|
||||||
do {
|
do {
|
||||||
int allowedFrameSize = Math.min(maxFrameSize, allowedBytes - bytesWritten);
|
int allowedFrameSize = Math.min(maxFrameSize, allowedBytes - bytesWritten);
|
||||||
@ -386,7 +378,6 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
frameWriter().writeData(ctx, stream.id(), toWrite, writeablePadding,
|
frameWriter().writeData(ctx, stream.id(), toWrite, writeablePadding,
|
||||||
size == bytesWritten && endOfStream, writePromise);
|
size == bytesWritten && endOfStream, writePromise);
|
||||||
} while (size != bytesWritten && allowedBytes > bytesWritten);
|
} while (size != bytesWritten && allowedBytes > bytesWritten);
|
||||||
return true;
|
|
||||||
} finally {
|
} finally {
|
||||||
size -= bytesWritten;
|
size -= bytesWritten;
|
||||||
}
|
}
|
||||||
@ -427,10 +418,9 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean write(int allowedBytes) {
|
public void write(int allowedBytes) {
|
||||||
frameWriter().writeHeaders(ctx, stream.id(), headers, streamDependency, weight, exclusive,
|
frameWriter().writeHeaders(ctx, stream.id(), headers, streamDependency, weight, exclusive,
|
||||||
padding, endOfStream, promise);
|
padding, endOfStream, promise);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +413,6 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
|
|||||||
|
|
||||||
// Send a window update for the stream/connection.
|
// Send a window update for the stream/connection.
|
||||||
frameWriter.writeWindowUpdate(ctx, stream.id(), deltaWindowSize, ctx.newPromise());
|
frameWriter.writeWindowUpdate(ctx, stream.id(), deltaWindowSize, ctx.newPromise());
|
||||||
ctx.flush();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
private final Http2Connection.PropertyKey stateKey;
|
private final Http2Connection.PropertyKey stateKey;
|
||||||
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
||||||
private ChannelHandlerContext ctx;
|
private ChannelHandlerContext ctx;
|
||||||
private boolean needFlush;
|
|
||||||
|
|
||||||
public DefaultHttp2RemoteFlowController(Http2Connection connection) {
|
public DefaultHttp2RemoteFlowController(Http2Connection connection) {
|
||||||
this.connection = checkNotNull(connection, "connection");
|
this.connection = checkNotNull(connection, "connection");
|
||||||
@ -185,7 +184,6 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
AbstractState state = state(stream);
|
AbstractState state = state(stream);
|
||||||
state.incrementStreamWindow(delta);
|
state.incrementStreamWindow(delta);
|
||||||
state.writeBytes(state.writableWindow());
|
state.writeBytes(state.writableWindow());
|
||||||
flush();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,11 +205,6 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.writeBytes(state.writableWindow());
|
state.writeBytes(state.writableWindow());
|
||||||
try {
|
|
||||||
flush();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
frame.error(t);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -237,16 +230,6 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
return connectionState().windowSize();
|
return connectionState().windowSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Flushes the {@link ChannelHandlerContext} if we've received any data frames.
|
|
||||||
*/
|
|
||||||
private void flush() {
|
|
||||||
if (needFlush) {
|
|
||||||
ctx.flush();
|
|
||||||
needFlush = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes as many pending bytes as possible, according to stream priority.
|
* Writes as many pending bytes as possible, according to stream priority.
|
||||||
*/
|
*/
|
||||||
@ -260,7 +243,6 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
|
|
||||||
// Now write all of the allocated bytes.
|
// Now write all of the allocated bytes.
|
||||||
connection.forEachActiveStream(WRITE_ALLOCATED_BYTES);
|
connection.forEachActiveStream(WRITE_ALLOCATED_BYTES);
|
||||||
flush();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -604,13 +586,10 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
/**
|
/**
|
||||||
* Writes the frame and decrements the stream and connection window sizes. If the frame is in the pending
|
* Writes the frame and decrements the stream and connection window sizes. If the frame is in the pending
|
||||||
* queue, the written bytes are removed from this branch of the priority tree.
|
* queue, the written bytes are removed from this branch of the priority tree.
|
||||||
* <p>
|
|
||||||
* Note: this does not flush the {@link ChannelHandlerContext}.
|
|
||||||
* </p>
|
|
||||||
*/
|
*/
|
||||||
private int write(FlowControlled frame, int allowedBytes) {
|
private int write(FlowControlled frame, int allowedBytes) {
|
||||||
int before = frame.size();
|
int before = frame.size();
|
||||||
int writtenBytes = 0;
|
int writtenBytes;
|
||||||
// In case an exception is thrown we want to remember it and pass it to cancel(Throwable).
|
// In case an exception is thrown we want to remember it and pass it to cancel(Throwable).
|
||||||
Throwable cause = null;
|
Throwable cause = null;
|
||||||
try {
|
try {
|
||||||
@ -618,7 +597,7 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
|
|||||||
|
|
||||||
// Write the portion of the frame.
|
// Write the portion of the frame.
|
||||||
writing = true;
|
writing = true;
|
||||||
needFlush |= frame.write(max(0, allowedBytes));
|
frame.write(max(0, allowedBytes));
|
||||||
if (!cancelled && frame.size() == 0) {
|
if (!cancelled && frame.size() == 0) {
|
||||||
// This frame has been fully written, remove this frame and notify it. Since we remove this frame
|
// This frame has been fully written, remove this frame and notify it. Since we remove this frame
|
||||||
// first, we're guaranteed that its error method will not be called when we call cancel.
|
// first, we're guaranteed that its error method will not be called when we call cancel.
|
||||||
|
@ -359,6 +359,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture future = goAway(ctx, null);
|
ChannelFuture future = goAway(ctx, null);
|
||||||
|
ctx.flush();
|
||||||
|
|
||||||
// If there are no active streams, close immediately after the send is complete.
|
// If there are no active streams, close immediately after the send is complete.
|
||||||
// Otherwise wait until all streams are inactive.
|
// Otherwise wait until all streams are inactive.
|
||||||
@ -389,6 +390,13 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
ctx.flush();
|
ctx.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
// Trigger flush after read on the assumption that flush is cheap if there is nothing to write and that
|
||||||
|
// for flow-control the read may release window that causes data to be written that can now be flushed.
|
||||||
|
ctx.flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles {@link Http2Exception} objects that were thrown from other handlers. Ignores all other exceptions.
|
* Handles {@link Http2Exception} objects that were thrown from other handlers. Ignores all other exceptions.
|
||||||
*/
|
*/
|
||||||
@ -478,6 +486,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
} else {
|
} else {
|
||||||
onConnectionError(ctx, cause, embedded);
|
onConnectionError(ctx, cause, embedded);
|
||||||
}
|
}
|
||||||
|
ctx.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -522,7 +531,6 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture future = frameWriter().writeRstStream(ctx, streamId, errorCode, promise);
|
ChannelFuture future = frameWriter().writeRstStream(ctx, streamId, errorCode, promise);
|
||||||
ctx.flush();
|
|
||||||
|
|
||||||
// Synchronously set the resetSent flag to prevent any subsequent calls
|
// Synchronously set the resetSent flag to prevent any subsequent calls
|
||||||
// from resulting in multiple reset frames being sent.
|
// from resulting in multiple reset frames being sent.
|
||||||
@ -557,7 +565,6 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
connection.goAwaySent(lastStreamId, errorCode, debugData);
|
connection.goAwaySent(lastStreamId, errorCode, debugData);
|
||||||
|
|
||||||
ChannelFuture future = frameWriter().writeGoAway(ctx, lastStreamId, errorCode, debugData, promise);
|
ChannelFuture future = frameWriter().writeGoAway(ctx, lastStreamId, errorCode, debugData, promise);
|
||||||
ctx.flush();
|
|
||||||
|
|
||||||
future.addListener(new GenericFutureListener<ChannelFuture>() {
|
future.addListener(new GenericFutureListener<ChannelFuture>() {
|
||||||
@Override
|
@Override
|
||||||
@ -585,7 +592,8 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the remote endpoint with with a {@code GO_AWAY} frame.
|
* Close the remote endpoint with with a {@code GO_AWAY} frame. Does <strong>not</strong> flush
|
||||||
|
* immediately, this is the responsibility of the caller.
|
||||||
*/
|
*/
|
||||||
private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause) {
|
private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause) {
|
||||||
long errorCode = cause != null ? cause.error().code() : NO_ERROR.code();
|
long errorCode = cause != null ? cause.error().code() : NO_ERROR.code();
|
||||||
|
@ -27,8 +27,8 @@ public interface Http2RemoteFlowController extends Http2FlowController {
|
|||||||
* guarantee when the data will be written or whether it will be split into multiple frames
|
* guarantee when the data will be written or whether it will be split into multiple frames
|
||||||
* before sending.
|
* before sending.
|
||||||
* <p>
|
* <p>
|
||||||
* Manually flushing the {@link ChannelHandlerContext} is not required, since the flow
|
* Manually flushing the {@link ChannelHandlerContext} is required for writes as the flow controller will
|
||||||
* controller will flush as appropriate.
|
* <strong>not</strong> flush by itself.
|
||||||
*
|
*
|
||||||
* @param ctx the context from the handler.
|
* @param ctx the context from the handler.
|
||||||
* @param stream the subject stream. Must not be the connection stream object.
|
* @param stream the subject stream. Must not be the connection stream object.
|
||||||
@ -75,15 +75,14 @@ public interface Http2RemoteFlowController extends Http2FlowController {
|
|||||||
* Writes up to {@code allowedBytes} of the encapsulated payload to the stream. Note that
|
* Writes up to {@code allowedBytes} of the encapsulated payload to the stream. Note that
|
||||||
* a value of 0 may be passed which will allow payloads with flow-control size == 0 to be
|
* a value of 0 may be passed which will allow payloads with flow-control size == 0 to be
|
||||||
* written. The flow-controller may call this method multiple times with different values until
|
* written. The flow-controller may call this method multiple times with different values until
|
||||||
* the payload is fully written.
|
* the payload is fully written, i.e it's size after the write is 0.
|
||||||
* <p>
|
* <p>
|
||||||
* When an exception is thrown the {@link Http2RemoteFlowController} will make a call to
|
* When an exception is thrown the {@link Http2RemoteFlowController} will make a call to
|
||||||
* {@link #error(Throwable)}.
|
* {@link #error(Throwable)}.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param allowedBytes an upper bound on the number of bytes the payload can write at this time.
|
* @param allowedBytes an upper bound on the number of bytes the payload can write at this time.
|
||||||
* @return {@code true} if a flush is required, {@code false} otherwise.
|
|
||||||
*/
|
*/
|
||||||
boolean write(int allowedBytes);
|
void write(int allowedBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import static org.mockito.Matchers.eq;
|
|||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.reset;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
@ -48,6 +49,7 @@ import io.netty.channel.ChannelHandlerContext;
|
|||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.DefaultChannelPromise;
|
import io.netty.channel.DefaultChannelPromise;
|
||||||
import io.netty.handler.codec.http2.Http2Exception.ClosedStreamCreationException;
|
import io.netty.handler.codec.http2.Http2Exception.ClosedStreamCreationException;
|
||||||
|
import junit.framework.AssertionFailedError;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
@ -161,6 +163,9 @@ public class DefaultHttp2ConnectionDecoderTest {
|
|||||||
|
|
||||||
// Simulate receiving the SETTINGS ACK for the initial settings.
|
// Simulate receiving the SETTINGS ACK for the initial settings.
|
||||||
decode().onSettingsAckRead(ctx);
|
decode().onSettingsAckRead(ctx);
|
||||||
|
|
||||||
|
// Disallow any further flushes now that settings ACK has been sent
|
||||||
|
when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -605,7 +610,6 @@ public class DefaultHttp2ConnectionDecoderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void settingsReadShouldSetValues() throws Exception {
|
public void settingsReadShouldSetValues() throws Exception {
|
||||||
when(connection.isServer()).thenReturn(true);
|
|
||||||
Http2Settings settings = new Http2Settings();
|
Http2Settings settings = new Http2Settings();
|
||||||
settings.pushEnabled(true);
|
settings.pushEnabled(true);
|
||||||
settings.initialWindowSize(123);
|
settings.initialWindowSize(123);
|
||||||
|
@ -60,6 +60,7 @@ import io.netty.util.concurrent.ImmediateEventExecutor;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import junit.framework.AssertionFailedError;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
@ -207,6 +208,7 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
when(ctx.newSucceededFuture()).thenReturn(future);
|
when(ctx.newSucceededFuture()).thenReturn(future);
|
||||||
when(ctx.newPromise()).thenReturn(promise);
|
when(ctx.newPromise()).thenReturn(promise);
|
||||||
when(ctx.write(any())).thenReturn(future);
|
when(ctx.write(any())).thenReturn(future);
|
||||||
|
when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
|
||||||
|
|
||||||
encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
|
encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
|
||||||
encoder.lifecycleManager(lifecycleManager);
|
encoder.lifecycleManager(lifecycleManager);
|
||||||
@ -217,7 +219,7 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
final ByteBuf data = dummyData();
|
final ByteBuf data = dummyData();
|
||||||
encoder.writeData(ctx, STREAM_ID, data, 0, true, promise);
|
encoder.writeData(ctx, STREAM_ID, data, 0, true, promise);
|
||||||
assertEquals(payloadCaptor.getValue().size(), 8);
|
assertEquals(payloadCaptor.getValue().size(), 8);
|
||||||
assertTrue(payloadCaptor.getValue().write(8));
|
payloadCaptor.getValue().write(8);
|
||||||
assertEquals(0, payloadCaptor.getValue().size());
|
assertEquals(0, payloadCaptor.getValue().size());
|
||||||
assertEquals("abcdefgh", writtenData.get(0));
|
assertEquals("abcdefgh", writtenData.get(0));
|
||||||
assertEquals(0, data.refCnt());
|
assertEquals(0, data.refCnt());
|
||||||
@ -229,7 +231,7 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
final ByteBuf data = dummyData();
|
final ByteBuf data = dummyData();
|
||||||
encoder.writeData(ctx, STREAM_ID, data, 0, true, promise);
|
encoder.writeData(ctx, STREAM_ID, data, 0, true, promise);
|
||||||
assertEquals(payloadCaptor.getValue().size(), 8);
|
assertEquals(payloadCaptor.getValue().size(), 8);
|
||||||
assertTrue(payloadCaptor.getValue().write(8));
|
payloadCaptor.getValue().write(8);
|
||||||
// writer was called 3 times
|
// writer was called 3 times
|
||||||
assertEquals(3, writtenData.size());
|
assertEquals(3, writtenData.size());
|
||||||
assertEquals("abc", writtenData.get(0));
|
assertEquals("abc", writtenData.get(0));
|
||||||
@ -244,7 +246,7 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
final ByteBuf data = dummyData();
|
final ByteBuf data = dummyData();
|
||||||
encoder.writeData(ctx, STREAM_ID, data, 5, true, promise);
|
encoder.writeData(ctx, STREAM_ID, data, 5, true, promise);
|
||||||
assertEquals(payloadCaptor.getValue().size(), 13);
|
assertEquals(payloadCaptor.getValue().size(), 13);
|
||||||
assertTrue(payloadCaptor.getValue().write(13));
|
payloadCaptor.getValue().write(13);
|
||||||
// writer was called 3 times
|
// writer was called 3 times
|
||||||
assertEquals(3, writtenData.size());
|
assertEquals(3, writtenData.size());
|
||||||
assertEquals("abcde", writtenData.get(0));
|
assertEquals("abcde", writtenData.get(0));
|
||||||
@ -262,7 +264,7 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
ByteBuf data = dummyData();
|
ByteBuf data = dummyData();
|
||||||
encoder.writeData(ctx, STREAM_ID, data, 10, true, promise);
|
encoder.writeData(ctx, STREAM_ID, data, 10, true, promise);
|
||||||
assertEquals(payloadCaptor.getValue().size(), 18);
|
assertEquals(payloadCaptor.getValue().size(), 18);
|
||||||
assertTrue(payloadCaptor.getValue().write(18));
|
payloadCaptor.getValue().write(18);
|
||||||
// writer was called 4 times
|
// writer was called 4 times
|
||||||
assertEquals(4, writtenData.size());
|
assertEquals(4, writtenData.size());
|
||||||
assertEquals("abcde", writtenData.get(0));
|
assertEquals("abcde", writtenData.get(0));
|
||||||
@ -292,7 +294,7 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
when(frameSizePolicy.maxFrameSize()).thenReturn(5);
|
when(frameSizePolicy.maxFrameSize()).thenReturn(5);
|
||||||
encoder.writeData(ctx, STREAM_ID, data, 10, true, promise);
|
encoder.writeData(ctx, STREAM_ID, data, 10, true, promise);
|
||||||
assertEquals(payloadCaptor.getValue().size(), 10);
|
assertEquals(payloadCaptor.getValue().size(), 10);
|
||||||
assertTrue(payloadCaptor.getValue().write(10));
|
payloadCaptor.getValue().write(10);
|
||||||
// writer was called 2 times
|
// writer was called 2 times
|
||||||
assertEquals(2, writtenData.size());
|
assertEquals(2, writtenData.size());
|
||||||
assertEquals("", writtenData.get(0));
|
assertEquals("", writtenData.get(0));
|
||||||
|
@ -30,6 +30,7 @@ import io.netty.buffer.Unpooled;
|
|||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
|
|
||||||
|
import junit.framework.AssertionFailedError;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
@ -64,6 +65,7 @@ public class DefaultHttp2LocalFlowControllerTest {
|
|||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
|
|
||||||
when(ctx.newPromise()).thenReturn(promise);
|
when(ctx.newPromise()).thenReturn(promise);
|
||||||
|
when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
|
||||||
|
|
||||||
connection = new DefaultHttp2Connection(false);
|
connection = new DefaultHttp2Connection(false);
|
||||||
controller = new DefaultHttp2LocalFlowController(connection, frameWriter, updateRatio);
|
controller = new DefaultHttp2LocalFlowController(connection, frameWriter, updateRatio);
|
||||||
|
@ -26,6 +26,7 @@ import static org.junit.Assert.fail;
|
|||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.anyInt;
|
import static org.mockito.Matchers.anyInt;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.reset;
|
import static org.mockito.Mockito.reset;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@ -40,7 +41,9 @@ import io.netty.util.collection.IntObjectMap;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import junit.framework.AssertionFailedError;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
@ -83,6 +86,7 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
|
|
||||||
when(ctx.newPromise()).thenReturn(promise);
|
when(ctx.newPromise()).thenReturn(promise);
|
||||||
|
when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
|
||||||
|
|
||||||
connection = new DefaultHttp2Connection(false);
|
connection = new DefaultHttp2Connection(false);
|
||||||
controller = new DefaultHttp2RemoteFlowController(connection);
|
controller = new DefaultHttp2RemoteFlowController(connection);
|
||||||
@ -127,19 +131,17 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void payloadSmallerThanWindowShouldBeSentImmediately() throws Http2Exception {
|
public void payloadSmallerThanWindowShouldBeWrittenImmediately() throws Http2Exception {
|
||||||
FakeFlowControlled data = new FakeFlowControlled(5);
|
FakeFlowControlled data = new FakeFlowControlled(5);
|
||||||
sendData(STREAM_A, data);
|
sendData(STREAM_A, data);
|
||||||
data.assertFullyWritten();
|
data.assertFullyWritten();
|
||||||
verify(ctx, times(1)).flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void emptyPayloadShouldBeSentImmediately() throws Http2Exception {
|
public void emptyPayloadShouldBeWrittenImmediately() throws Http2Exception {
|
||||||
FakeFlowControlled data = new FakeFlowControlled(0);
|
FakeFlowControlled data = new FakeFlowControlled(0);
|
||||||
sendData(STREAM_A, data);
|
sendData(STREAM_A, data);
|
||||||
data.assertFullyWritten();
|
data.assertFullyWritten();
|
||||||
verify(ctx, times(1)).flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -152,7 +154,6 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
data.assertNotWritten();
|
data.assertNotWritten();
|
||||||
sendData(STREAM_A, moreData);
|
sendData(STREAM_A, moreData);
|
||||||
moreData.assertNotWritten();
|
moreData.assertNotWritten();
|
||||||
verify(ctx, never()).flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -165,7 +166,6 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
data.assertNotWritten();
|
data.assertNotWritten();
|
||||||
sendData(STREAM_A, moreData);
|
sendData(STREAM_A, moreData);
|
||||||
moreData.assertNotWritten();
|
moreData.assertNotWritten();
|
||||||
verify(ctx, never()).flush();
|
|
||||||
|
|
||||||
connection.stream(STREAM_A).close();
|
connection.stream(STREAM_A).close();
|
||||||
data.assertError();
|
data.assertError();
|
||||||
@ -180,7 +180,6 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
sendData(STREAM_A, data);
|
sendData(STREAM_A, data);
|
||||||
// Verify that a partial frame of 5 remains to be sent
|
// Verify that a partial frame of 5 remains to be sent
|
||||||
data.assertPartiallyWritten(5);
|
data.assertPartiallyWritten(5);
|
||||||
verify(ctx, times(1)).flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -193,14 +192,12 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
sendData(STREAM_A, moreData);
|
sendData(STREAM_A, moreData);
|
||||||
data.assertPartiallyWritten(10);
|
data.assertPartiallyWritten(10);
|
||||||
moreData.assertNotWritten();
|
moreData.assertNotWritten();
|
||||||
verify(ctx, times(1)).flush();
|
|
||||||
reset(ctx);
|
reset(ctx);
|
||||||
|
|
||||||
// Update the window and verify that the rest of data and some of moreData are written
|
// Update the window and verify that the rest of data and some of moreData are written
|
||||||
incrementWindowSize(STREAM_A, 15);
|
incrementWindowSize(STREAM_A, 15);
|
||||||
data.assertFullyWritten();
|
data.assertFullyWritten();
|
||||||
moreData.assertPartiallyWritten(5);
|
moreData.assertPartiallyWritten(5);
|
||||||
verify(ctx, times(1)).flush();
|
|
||||||
|
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 25, window(CONNECTION_STREAM_ID));
|
assertEquals(DEFAULT_WINDOW_SIZE - 25, window(CONNECTION_STREAM_ID));
|
||||||
assertEquals(0, window(STREAM_A));
|
assertEquals(0, window(STREAM_A));
|
||||||
@ -1109,26 +1106,21 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
public void flowControlledWriteCompleteThrowsAnException() throws Exception {
|
public void flowControlledWriteCompleteThrowsAnException() throws Exception {
|
||||||
final Http2RemoteFlowController.FlowControlled flowControlled =
|
final Http2RemoteFlowController.FlowControlled flowControlled =
|
||||||
Mockito.mock(Http2RemoteFlowController.FlowControlled.class);
|
Mockito.mock(Http2RemoteFlowController.FlowControlled.class);
|
||||||
when(flowControlled.size()).thenReturn(100);
|
final AtomicInteger size = new AtomicInteger(150);
|
||||||
when(flowControlled.write(anyInt())).thenAnswer(new Answer<Boolean>() {
|
doAnswer(new Answer<Integer>() {
|
||||||
private int invocationCount;
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
|
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||||
switch(invocationCount) {
|
return size.get();
|
||||||
case 0:
|
|
||||||
when(flowControlled.size()).thenReturn(50);
|
|
||||||
invocationCount = 1;
|
|
||||||
return true;
|
|
||||||
case 1:
|
|
||||||
when(flowControlled.size()).thenReturn(20);
|
|
||||||
invocationCount = 2;
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
when(flowControlled.size()).thenReturn(0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}).when(flowControlled).size();
|
||||||
|
doAnswer(new Answer<Void>() {
|
||||||
|
@Override
|
||||||
|
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||||
|
size.addAndGet(-50);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).when(flowControlled).write(anyInt());
|
||||||
|
|
||||||
final Http2Stream stream = stream(STREAM_A);
|
final Http2Stream stream = stream(STREAM_A);
|
||||||
doAnswer(new Answer<Void>() {
|
doAnswer(new Answer<Void>() {
|
||||||
public Void answer(InvocationOnMock invocationOnMock) {
|
public Void answer(InvocationOnMock invocationOnMock) {
|
||||||
@ -1148,7 +1140,7 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
verify(flowControlled, never()).error(any(Throwable.class));
|
verify(flowControlled, never()).error(any(Throwable.class));
|
||||||
verify(flowControlled).writeComplete();
|
verify(flowControlled).writeComplete();
|
||||||
|
|
||||||
assertEquals(100, windowBefore - window(STREAM_A));
|
assertEquals(150, windowBefore - window(STREAM_A));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -1157,7 +1149,7 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
Mockito.mock(Http2RemoteFlowController.FlowControlled.class);
|
Mockito.mock(Http2RemoteFlowController.FlowControlled.class);
|
||||||
final Http2Stream stream = stream(STREAM_A);
|
final Http2Stream stream = stream(STREAM_A);
|
||||||
when(flowControlled.size()).thenReturn(100);
|
when(flowControlled.size()).thenReturn(100);
|
||||||
when(flowControlled.write(anyInt())).thenThrow(new RuntimeException("write failed"));
|
doThrow(new RuntimeException("write failed")).when(flowControlled).write(anyInt());
|
||||||
doAnswer(new Answer<Void>() {
|
doAnswer(new Answer<Void>() {
|
||||||
public Void answer(InvocationOnMock invocationOnMock) {
|
public Void answer(InvocationOnMock invocationOnMock) {
|
||||||
stream.close();
|
stream.close();
|
||||||
@ -1176,25 +1168,25 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
final Http2RemoteFlowController.FlowControlled flowControlled =
|
final Http2RemoteFlowController.FlowControlled flowControlled =
|
||||||
Mockito.mock(Http2RemoteFlowController.FlowControlled.class);
|
Mockito.mock(Http2RemoteFlowController.FlowControlled.class);
|
||||||
when(flowControlled.size()).thenReturn(100);
|
when(flowControlled.size()).thenReturn(100);
|
||||||
when(flowControlled.write(anyInt())).thenAnswer(new Answer<Boolean>() {
|
doAnswer(new Answer<Void>() {
|
||||||
private int invocationCount;
|
private int invocationCount;
|
||||||
@Override
|
@Override
|
||||||
public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
|
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||||
switch(invocationCount) {
|
switch(invocationCount) {
|
||||||
case 0:
|
case 0:
|
||||||
when(flowControlled.size()).thenReturn(50);
|
when(flowControlled.size()).thenReturn(50);
|
||||||
invocationCount = 1;
|
invocationCount = 1;
|
||||||
return true;
|
return null;
|
||||||
case 1:
|
case 1:
|
||||||
when(flowControlled.size()).thenReturn(20);
|
when(flowControlled.size()).thenReturn(20);
|
||||||
invocationCount = 2;
|
invocationCount = 2;
|
||||||
return true;
|
return null;
|
||||||
default:
|
default:
|
||||||
when(flowControlled.size()).thenReturn(10);
|
when(flowControlled.size()).thenReturn(10);
|
||||||
throw new RuntimeException("Write failed");
|
throw new RuntimeException("Write failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}).when(flowControlled).write(anyInt());
|
||||||
return flowControlled;
|
return flowControlled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1265,15 +1257,14 @@ public class DefaultHttp2RemoteFlowControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean write(int allowedBytes) {
|
public void write(int allowedBytes) {
|
||||||
if (allowedBytes <= 0 && currentSize != 0) {
|
if (allowedBytes <= 0 && currentSize != 0) {
|
||||||
// Write has been called but no data can be written
|
// Write has been called but no data can be written
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
writeCalled = true;
|
writeCalled = true;
|
||||||
int written = Math.min(currentSize, allowedBytes);
|
int written = Math.min(currentSize, allowedBytes);
|
||||||
currentSize -= written;
|
currentSize -= written;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int written() {
|
public int written() {
|
||||||
|
@ -358,4 +358,11 @@ public class Http2ConnectionHandlerTest {
|
|||||||
verify(data).release();
|
verify(data).release();
|
||||||
verifyNoMoreInteractions(frameWriter);
|
verifyNoMoreInteractions(frameWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void channelReadCompleteTriggersFlush() throws Exception {
|
||||||
|
handler = newHandler();
|
||||||
|
handler.channelReadComplete(ctx);
|
||||||
|
verify(ctx, times(1)).flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
public void run() {
|
public void run() {
|
||||||
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
|
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
|
||||||
newPromise());
|
newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,6 +175,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
public void run() {
|
public void run() {
|
||||||
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
|
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
|
||||||
newPromise());
|
newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -206,6 +208,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
public void run() {
|
public void run() {
|
||||||
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
|
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
|
||||||
newPromise());
|
newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,6 +240,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
public void run() {
|
public void run() {
|
||||||
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
|
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
|
||||||
true, newPromise());
|
true, newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -247,6 +251,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
public void run() {
|
public void run() {
|
||||||
http2Client.encoder().writeHeaders(ctx(), Integer.MAX_VALUE + 1, headers, 0, (short) 16, false, 0,
|
http2Client.encoder().writeHeaders(ctx(), Integer.MAX_VALUE + 1, headers, 0, (short) 16, false, 0,
|
||||||
true, newPromise());
|
true, newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -292,6 +297,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
// Write trailers.
|
// Write trailers.
|
||||||
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
|
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
|
||||||
true, newPromise());
|
true, newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -376,6 +382,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
// Write trailers.
|
// Write trailers.
|
||||||
http2Client.encoder().writeHeaders(ctx(), streamId, headers, 0, (short) 16,
|
http2Client.encoder().writeHeaders(ctx(), streamId, headers, 0, (short) 16,
|
||||||
false, 0, true, newPromise());
|
false, 0, true, newPromise());
|
||||||
|
ctx().flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user