Fix bug in HTTP/2 outbound flow control
Motivation: The outbound flow controller logic does not properly reset the allocated bytes between successive invocations of the priority algorithm. Modifications: Updated the priority algorithm to reset the allocated bytes for each stream. Result: Each call to the priority algorithm now starts with zero allocated bytes for each stream.
This commit is contained in:
parent
d220afa885
commit
f23f3b9617
@ -35,7 +35,6 @@ import io.netty.util.collection.IntObjectHashMap;
|
|||||||
import io.netty.util.collection.IntObjectMap;
|
import io.netty.util.collection.IntObjectMap;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -388,8 +387,7 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final Collection<? extends Http2Stream> children() {
|
public final Collection<? extends Http2Stream> children() {
|
||||||
DefaultStream[] childrenArray = children.values(DefaultStream.class);
|
return children.values();
|
||||||
return Arrays.asList(childrenArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -421,10 +419,10 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
if (newParent != parent() || exclusive) {
|
if (newParent != parent() || exclusive) {
|
||||||
List<ParentChangedEvent> events = null;
|
List<ParentChangedEvent> events = null;
|
||||||
if (newParent.isDescendantOf(this)) {
|
if (newParent.isDescendantOf(this)) {
|
||||||
events = new ArrayList<ParentChangedEvent>(2 + (exclusive ? newParent.children().size() : 0));
|
events = new ArrayList<ParentChangedEvent>(2 + (exclusive ? newParent.numChildren(): 0));
|
||||||
parent.takeChild(newParent, false, events);
|
parent.takeChild(newParent, false, events);
|
||||||
} else {
|
} else {
|
||||||
events = new ArrayList<ParentChangedEvent>(1 + (exclusive ? newParent.children().size() : 0));
|
events = new ArrayList<ParentChangedEvent>(1 + (exclusive ? newParent.numChildren() : 0));
|
||||||
}
|
}
|
||||||
newParent.takeChild(this, exclusive, events);
|
newParent.takeChild(this, exclusive, events);
|
||||||
notifyParentChanged(events);
|
notifyParentChanged(events);
|
||||||
@ -563,7 +561,7 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
// move any previous children to the child node, becoming grand children
|
// move any previous children to the child node, becoming grand children
|
||||||
// of this node.
|
// of this node.
|
||||||
if (!children.isEmpty()) {
|
if (!children.isEmpty()) {
|
||||||
for (DefaultStream grandchild : removeAllChildren().values(DefaultStream.class)) {
|
for (DefaultStream grandchild : removeAllChildren().values()) {
|
||||||
child.takeChild(grandchild, false, events);
|
child.takeChild(grandchild, false, events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -590,7 +588,7 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
totalChildWeights -= child.weight();
|
totalChildWeights -= child.weight();
|
||||||
|
|
||||||
// Move up any grand children to be directly dependent on this node.
|
// Move up any grand children to be directly dependent on this node.
|
||||||
for (DefaultStream grandchild : child.children.values(DefaultStream.class)) {
|
for (DefaultStream grandchild : child.children.values()) {
|
||||||
takeChild(grandchild, false, events);
|
takeChild(grandchild, false, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import io.netty.channel.ChannelFuture;
|
|||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.ChannelPromiseAggregator;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
|
||||||
@ -268,12 +267,6 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
// There were previous DATA frames sent. We need to send the HEADERS only after the most
|
// There were previous DATA frames sent. We need to send the HEADERS only after the most
|
||||||
// recent DATA frame to keep them in sync...
|
// recent DATA frame to keep them in sync...
|
||||||
|
|
||||||
// Wrap the original promise in an aggregate which will complete the original promise
|
|
||||||
// once the headers are written.
|
|
||||||
final ChannelPromiseAggregator aggregatePromise = new ChannelPromiseAggregator(promise);
|
|
||||||
final ChannelPromise innerPromise = ctx.newPromise();
|
|
||||||
aggregatePromise.add(innerPromise);
|
|
||||||
|
|
||||||
// Only write the HEADERS frame after the previous DATA frame has been written.
|
// Only write the HEADERS frame after the previous DATA frame has been written.
|
||||||
final Http2Stream theStream = stream;
|
final Http2Stream theStream = stream;
|
||||||
lastDataWrite.addListener(new ChannelFutureListener() {
|
lastDataWrite.addListener(new ChannelFutureListener() {
|
||||||
@ -281,13 +274,13 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
public void operationComplete(ChannelFuture future) throws Exception {
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
if (!future.isSuccess()) {
|
if (!future.isSuccess()) {
|
||||||
// The DATA write failed, also fail this write.
|
// The DATA write failed, also fail this write.
|
||||||
innerPromise.setFailure(future.cause());
|
promise.setFailure(future.cause());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the write.
|
// Perform the write.
|
||||||
writeHeaders(ctx, theStream, headers, streamDependency, weight, exclusive, padding,
|
writeHeaders(ctx, theStream, headers, streamDependency, weight, exclusive, padding,
|
||||||
endOfStream, innerPromise);
|
endOfStream, promise);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,6 +33,12 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
*/
|
*/
|
||||||
public static final double DEFAULT_WINDOW_UPDATE_RATIO = 0.5;
|
public static final double DEFAULT_WINDOW_UPDATE_RATIO = 0.5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default maximum connection size used as a limit when the number of active streams is
|
||||||
|
* large. Set to 2 MiB.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT_MAX_CONNECTION_WINDOW_SIZE = 1048576 * 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A value for the window update ratio to be use in order to disable window updates for
|
* A value for the window update ratio to be use in order to disable window updates for
|
||||||
* a stream (i.e. {@code 0}).
|
* a stream (i.e. {@code 0}).
|
||||||
@ -41,6 +47,7 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
|
|
||||||
private final Http2Connection connection;
|
private final Http2Connection connection;
|
||||||
private final Http2FrameWriter frameWriter;
|
private final Http2FrameWriter frameWriter;
|
||||||
|
private int maxConnectionWindowSize = DEFAULT_MAX_CONNECTION_WINDOW_SIZE;
|
||||||
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
||||||
|
|
||||||
public DefaultHttp2InboundFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
|
public DefaultHttp2InboundFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
|
||||||
@ -59,6 +66,14 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DefaultHttp2InboundFlowController setMaxConnectionWindowSize(int maxConnectionWindowSize) {
|
||||||
|
if (maxConnectionWindowSize <= 0) {
|
||||||
|
throw new IllegalArgumentException("maxConnectionWindowSize must be > 0");
|
||||||
|
}
|
||||||
|
this.maxConnectionWindowSize = maxConnectionWindowSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialInboundWindowSize(int newWindowSize) throws Http2Exception {
|
public void initialInboundWindowSize(int newWindowSize) throws Http2Exception {
|
||||||
int deltaWindowSize = newWindowSize - initialWindowSize;
|
int deltaWindowSize = newWindowSize - initialWindowSize;
|
||||||
@ -114,7 +129,6 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
int dataLength = data.readableBytes() + padding;
|
int dataLength = data.readableBytes() + padding;
|
||||||
boolean windowUpdateSent = false;
|
boolean windowUpdateSent = false;
|
||||||
try {
|
try {
|
||||||
// Apply the connection-level flow control.
|
|
||||||
windowUpdateSent = applyConnectionFlowControl(ctx, dataLength);
|
windowUpdateSent = applyConnectionFlowControl(ctx, dataLength);
|
||||||
|
|
||||||
// Apply the stream-level flow control.
|
// Apply the stream-level flow control.
|
||||||
@ -214,6 +228,25 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
windowUpdateRatio = ratio;
|
windowUpdateRatio = ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the initial size of this window.
|
||||||
|
*/
|
||||||
|
int initialWindowSize() {
|
||||||
|
int maxWindowSize = initialWindowSize;
|
||||||
|
if (streamId == CONNECTION_STREAM_ID) {
|
||||||
|
// Determine the maximum number of streams that we can allow without integer overflow
|
||||||
|
// of maxWindowSize * numStreams. Also take care to avoid division by zero when
|
||||||
|
// maxWindowSize == 0.
|
||||||
|
int maxNumStreams = Integer.MAX_VALUE;
|
||||||
|
if (maxWindowSize > 0) {
|
||||||
|
maxNumStreams /= maxWindowSize;
|
||||||
|
}
|
||||||
|
int numStreams = Math.min(maxNumStreams, Math.max(1, connection.numActiveStreams()));
|
||||||
|
maxWindowSize = Math.min(maxConnectionWindowSize, maxWindowSize * numStreams);
|
||||||
|
}
|
||||||
|
return maxWindowSize;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the flow control window for this stream if it is appropriate.
|
* Updates the flow control window for this stream if it is appropriate.
|
||||||
*
|
*
|
||||||
@ -224,7 +257,7 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int threshold = (int) (initialWindowSize * windowUpdateRatio);
|
int threshold = (int) (initialWindowSize() * windowUpdateRatio);
|
||||||
if (window <= threshold) {
|
if (window <= threshold) {
|
||||||
updateWindow(ctx);
|
updateWindow(ctx);
|
||||||
return true;
|
return true;
|
||||||
@ -290,10 +323,8 @@ public class DefaultHttp2InboundFlowController implements Http2InboundFlowContro
|
|||||||
*/
|
*/
|
||||||
void updateWindow(ChannelHandlerContext ctx) throws Http2Exception {
|
void updateWindow(ChannelHandlerContext ctx) throws Http2Exception {
|
||||||
// Expand the window for this stream back to the size of the initial window.
|
// Expand the window for this stream back to the size of the initial window.
|
||||||
int deltaWindowSize = initialWindowSize - window;
|
int deltaWindowSize = initialWindowSize() - window;
|
||||||
addAndGet(deltaWindowSize);
|
addAndGet(deltaWindowSize);
|
||||||
|
|
||||||
// Send a window update for the stream/connection.
|
|
||||||
frameWriter.writeWindowUpdate(ctx, streamId, deltaWindowSize, ctx.newPromise());
|
frameWriter.writeWindowUpdate(ctx, streamId, deltaWindowSize, ctx.newPromise());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,37 +25,35 @@ import static java.lang.Math.max;
|
|||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.ChannelPromiseAggregator;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic implementation of {@link Http2OutboundFlowController}.
|
* Basic implementation of {@link Http2OutboundFlowController}.
|
||||||
*/
|
*/
|
||||||
public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowController {
|
public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A comparators that sorts priority nodes in ascending order by the amount of priority data available for its
|
* A {@link Comparator} that sorts streams in ascending order the amount of streamable data.
|
||||||
* subtree.
|
|
||||||
*/
|
*/
|
||||||
private static final Comparator<Http2Stream> DATA_WEIGHT = new Comparator<Http2Stream>() {
|
private static final Comparator<Http2Stream> DATA_ORDER = new Comparator<Http2Stream>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(Http2Stream o1, Http2Stream o2) {
|
public int compare(Http2Stream o1, Http2Stream o2) {
|
||||||
final long result = ((long) state(o1).priorityBytes()) * o1.weight() -
|
return state(o1).streamableBytesForTree() - state(o2).streamableBytesForTree();
|
||||||
((long) state(o2).priorityBytes()) * o2.weight();
|
|
||||||
return result > 0 ? 1 : (result < 0 ? -1 : 0);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final Http2Connection connection;
|
private final Http2Connection connection;
|
||||||
private final Http2FrameWriter frameWriter;
|
private final Http2FrameWriter frameWriter;
|
||||||
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
||||||
|
private boolean frameSent;
|
||||||
private ChannelHandlerContext ctx;
|
private ChannelHandlerContext ctx;
|
||||||
|
|
||||||
public DefaultHttp2OutboundFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
|
public DefaultHttp2OutboundFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
|
||||||
@ -93,7 +91,7 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
|
public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
|
||||||
Http2Stream parent = stream.parent();
|
Http2Stream parent = stream.parent();
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
state(parent).incrementPriorityBytes(state(stream).priorityBytes());
|
state(parent).incrementStreamableBytesForTree(state(stream).streamableBytesForTree());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +99,7 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
|
public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
|
||||||
Http2Stream parent = stream.parent();
|
Http2Stream parent = stream.parent();
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
state(parent).incrementPriorityBytes(-state(stream).priorityBytes());
|
state(parent).incrementStreamableBytesForTree(-state(stream).streamableBytesForTree());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -134,10 +132,6 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateOutboundWindowSize(int streamId, int delta) throws Http2Exception {
|
public void updateOutboundWindowSize(int streamId, int delta) throws Http2Exception {
|
||||||
if (delta <= 0) {
|
|
||||||
throw new IllegalArgumentException("delta must be > 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamId == CONNECTION_STREAM_ID) {
|
if (streamId == CONNECTION_STREAM_ID) {
|
||||||
// Update the connection window and write any pending frames for all streams.
|
// Update the connection window and write any pending frames for all streams.
|
||||||
connectionState().incrementStreamWindow(delta);
|
connectionState().incrementStreamWindow(delta);
|
||||||
@ -252,131 +246,108 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
* Writes as many pending bytes as possible, according to stream priority.
|
* Writes as many pending bytes as possible, according to stream priority.
|
||||||
*/
|
*/
|
||||||
private void writePendingBytes() throws Http2Exception {
|
private void writePendingBytes() throws Http2Exception {
|
||||||
|
frameSent = false;
|
||||||
// Recursively write as many of the total writable bytes as possible.
|
|
||||||
Http2Stream connectionStream = connection.connectionStream();
|
Http2Stream connectionStream = connection.connectionStream();
|
||||||
int totalAllowance = state(connectionStream).priorityBytes();
|
OutboundFlowState connectionState = state(connectionStream);
|
||||||
writeAllowedBytes(connectionStream, totalAllowance);
|
int connectionWindow = Math.max(0, connectionState.window());
|
||||||
|
|
||||||
// Optimization: only flush once for all written frames. If it's null, there are no
|
// Allocate the bytes for the entire priority tree.
|
||||||
// data frames to send anyway.
|
allocateBytesForTree(connectionStream, connectionWindow);
|
||||||
|
|
||||||
|
// Perform the write of the allocated bytes for each stream.
|
||||||
|
for (Http2Stream stream : connection.activeStreams()) {
|
||||||
|
OutboundFlowState state = state(stream);
|
||||||
|
// The allocated bytes are for the entire sub-tree but the write will be limited
|
||||||
|
// by the number of pending bytes for the stream.
|
||||||
|
state.writeBytes(state.allocatedBytesForTree());
|
||||||
|
state.resetAllocatedBytesForTree();
|
||||||
|
}
|
||||||
|
connectionState.resetAllocatedBytesForTree();
|
||||||
|
|
||||||
|
// Only flush once for all written frames.
|
||||||
|
if (frameSent) {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively traverses the priority tree rooted at the given node. Attempts to write the allowed bytes for the
|
* Allocates as many bytes as possible for the given tree within the provided connection window.
|
||||||
* streams in this sub tree based on their weighted priorities.
|
|
||||||
*
|
*
|
||||||
* @param allowance
|
* @param stream the tree for which the given bytes are to be allocated.
|
||||||
* an allowed number of bytes that may be written to the streams in this subtree
|
* @param connectionWindow the connection window that acts as an upper bound on the total number
|
||||||
|
* of bytes that can be allocated for the tree.
|
||||||
|
* @return the total number of bytes actually allocated for this subtree.
|
||||||
*/
|
*/
|
||||||
private void writeAllowedBytes(Http2Stream stream, int allowance) throws Http2Exception {
|
private int allocateBytesForTree(Http2Stream stream, int connectionWindow) {
|
||||||
// Write the allowed bytes for this node. If not all of the allowance was used,
|
|
||||||
// restore what's left so that it can be propagated to future nodes.
|
|
||||||
OutboundFlowState state = state(stream);
|
OutboundFlowState state = state(stream);
|
||||||
int bytesWritten = state.writeBytes(allowance);
|
connectionWindow = min(connectionWindow, state.unallocatedBytesForTree());
|
||||||
allowance -= bytesWritten;
|
|
||||||
|
|
||||||
if (allowance <= 0 || stream.isLeaf()) {
|
// Determine the amount of bytes to allocate for 'this' stream.
|
||||||
|
int streamable = Math.max(0, state.streamableBytes() - state.allocatedBytesForTree());
|
||||||
|
int totalAllocated = min(connectionWindow, streamable);
|
||||||
|
|
||||||
|
connectionWindow -= totalAllocated;
|
||||||
|
int remainingInTree = state.streamableBytesForTree() - totalAllocated;
|
||||||
|
if (stream.isLeaf() || remainingInTree <= 0 || connectionWindow <= 0) {
|
||||||
// Nothing left to do in this subtree.
|
// Nothing left to do in this subtree.
|
||||||
return;
|
state.allocateBytesForTree(totalAllocated);
|
||||||
|
return totalAllocated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip the remaining connection flow control window by the allowance.
|
// If the window is big enough to fit all the remaining data. Just write everything
|
||||||
int remainingWindow = min(allowance, connectionWindow());
|
|
||||||
|
|
||||||
// The total number of unallocated bytes from the children of this node.
|
|
||||||
int unallocatedBytes = state.priorityBytes() - state.streamableBytes();
|
|
||||||
|
|
||||||
// Optimization. If the window is big enough to fit all the data. Just write everything
|
|
||||||
// and skip the priority algorithm.
|
// and skip the priority algorithm.
|
||||||
if (unallocatedBytes <= remainingWindow) {
|
if (remainingInTree <= connectionWindow) {
|
||||||
for (Http2Stream child : stream.children()) {
|
for (Http2Stream child : stream.children()) {
|
||||||
writeAllowedBytes(child, state(child).unallocatedPriorityBytes());
|
int writtenToChild = allocateBytesForTree(child, connectionWindow);
|
||||||
|
totalAllocated += writtenToChild;
|
||||||
|
connectionWindow -= writtenToChild;
|
||||||
}
|
}
|
||||||
return;
|
state.allocateBytesForTree(totalAllocated);
|
||||||
|
return totalAllocated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and sort the children of this node. They are sorted in ascending order the total
|
Http2Stream[] children = stream.children().toArray(new Http2Stream[0]);
|
||||||
// priority bytes for the subtree scaled by the weight of the node. The algorithm gives
|
Arrays.sort(children, DATA_ORDER);
|
||||||
// preference to nodes that appear later in the list, since the weight of each node
|
|
||||||
// increases in value as the list is iterated. This means that with this node ordering,
|
|
||||||
// the most bytes will be written to those nodes with the largest aggregate number of
|
|
||||||
// bytes and the highest priority.
|
|
||||||
List<Http2Stream> states = new ArrayList<Http2Stream>(stream.children());
|
|
||||||
Collections.sort(states, DATA_WEIGHT);
|
|
||||||
|
|
||||||
// Iterate over the children and spread the remaining bytes across them as is appropriate
|
// Clip the total remaining bytes by the connection window.
|
||||||
// based on the weights. This algorithm loops over all of the children more than once,
|
int totalWeight = stream.totalChildWeights();
|
||||||
// although it should typically only take a few passes to complete. In each pass we
|
int tail = children.length;
|
||||||
// give a node its share of the current remaining bytes. The node's weight and bytes
|
// Outer loop: continue until we've exhausted the connection window or allocated all bytes in the tree.
|
||||||
// allocated are then decremented from the totals, so that the subsequent
|
while (tail > 0 && connectionWindow > 0) {
|
||||||
// nodes split the difference. If after being processed, a node still has writable data,
|
int tailNextPass = 0;
|
||||||
// it is added back to the queue for further processing in the next pass.
|
int totalWeightNextPass = 0;
|
||||||
int remainingWeight = stream.totalChildWeights();
|
|
||||||
int nextTail = 0;
|
|
||||||
int unallocatedBytesForNextPass = 0;
|
|
||||||
int remainingWeightForNextPass = 0;
|
|
||||||
for (int head = 0, tail = states.size();; ++head) {
|
|
||||||
if (head >= tail) {
|
|
||||||
// We've reached the end one pass of the nodes. Reset the totals based on
|
|
||||||
// the nodes that were re-added to the deque since they still have data available.
|
|
||||||
unallocatedBytes = unallocatedBytesForNextPass;
|
|
||||||
remainingWeight = remainingWeightForNextPass;
|
|
||||||
unallocatedBytesForNextPass = 0;
|
|
||||||
remainingWeightForNextPass = 0;
|
|
||||||
head = 0;
|
|
||||||
tail = nextTail;
|
|
||||||
nextTail = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the next state, or break if nothing to do.
|
// Inner loop: allocate bytes to the children based on their weight.
|
||||||
if (head >= tail) {
|
for (int index = 0; index < tail && connectionWindow > 0; ++index) {
|
||||||
break;
|
Http2Stream child = children[index];
|
||||||
}
|
OutboundFlowState childState = state(child);
|
||||||
Http2Stream next = states.get(head);
|
|
||||||
OutboundFlowState nextState = state(next);
|
|
||||||
int weight = next.weight();
|
|
||||||
|
|
||||||
// Determine the value (in bytes) of a single unit of weight.
|
// Determine the ratio of this stream to all children.
|
||||||
double dataToWeightRatio = min(unallocatedBytes, remainingWindow) / (double) remainingWeight;
|
int weight = child.weight();
|
||||||
unallocatedBytes -= nextState.unallocatedPriorityBytes();
|
double weightRatio = weight / (double) totalWeight;
|
||||||
remainingWeight -= weight;
|
|
||||||
|
|
||||||
if (dataToWeightRatio > 0.0 && nextState.unallocatedPriorityBytes() > 0) {
|
int windowSlice = Math.max(1, (int) Math.round(connectionWindow * weightRatio));
|
||||||
|
|
||||||
// Determine the portion of the current writable data that is assigned to this
|
// Allocate the bytes for this child.
|
||||||
// node.
|
int allocated = allocateBytesForTree(child, windowSlice);
|
||||||
int writableChunk = (int) (weight * dataToWeightRatio);
|
|
||||||
|
|
||||||
// Clip the chunk allocated by the total amount of unallocated data remaining in
|
totalAllocated += allocated;
|
||||||
// the node.
|
connectionWindow -= allocated;
|
||||||
int allocatedChunk = min(writableChunk, nextState.unallocatedPriorityBytes());
|
totalWeight -= weight;
|
||||||
|
|
||||||
// Update the remaining connection window size.
|
if (childState.unallocatedBytesForTree() > 0) {
|
||||||
remainingWindow -= allocatedChunk;
|
// This stream still has more data, add it to the next pass.
|
||||||
|
children[tailNextPass++] = child;
|
||||||
// Mark these bytes as allocated.
|
totalWeightNextPass += weight;
|
||||||
nextState.allocatePriorityBytes(allocatedChunk);
|
|
||||||
if (nextState.unallocatedPriorityBytes() > 0) {
|
|
||||||
// There is still data remaining for this stream. Add it back to the queue
|
|
||||||
// for the next pass.
|
|
||||||
unallocatedBytesForNextPass += nextState.unallocatedPriorityBytes();
|
|
||||||
remainingWeightForNextPass += weight;
|
|
||||||
states.set(nextTail++, next);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextState.allocatedPriorityBytes() > 0) {
|
totalWeight = totalWeightNextPass;
|
||||||
// Write the allocated data for this stream.
|
tail = tailNextPass;
|
||||||
writeAllowedBytes(next, nextState.allocatedPriorityBytes());
|
}
|
||||||
|
|
||||||
// We're done with this node. Remark all bytes as unallocated for future
|
state.allocateBytesForTree(totalAllocated);
|
||||||
// invocations.
|
return totalAllocated;
|
||||||
nextState.allocatePriorityBytes(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -387,8 +358,8 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
private final Http2Stream stream;
|
private final Http2Stream stream;
|
||||||
private int window = initialWindowSize;
|
private int window = initialWindowSize;
|
||||||
private int pendingBytes;
|
private int pendingBytes;
|
||||||
private int priorityBytes;
|
private int streamableBytesForTree;
|
||||||
private int allocatedPriorityBytes;
|
private int allocatedBytesForTree;
|
||||||
private ChannelFuture lastNewFrame;
|
private ChannelFuture lastNewFrame;
|
||||||
|
|
||||||
private OutboundFlowState(Http2Stream stream) {
|
private OutboundFlowState(Http2Stream stream) {
|
||||||
@ -401,6 +372,36 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the number of bytes allocated to this tree by the priority algorithm.
|
||||||
|
*/
|
||||||
|
private void allocateBytesForTree(int bytes) {
|
||||||
|
allocatedBytesForTree += bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of bytes that have been allocated to this tree by the priority algorithm.
|
||||||
|
*/
|
||||||
|
private int allocatedBytesForTree() {
|
||||||
|
return allocatedBytesForTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of unallocated bytes (i.e. {@link #streamableBytesForTree()} -
|
||||||
|
* {@link #allocatedBytesForTree()}).
|
||||||
|
*/
|
||||||
|
private int unallocatedBytesForTree() {
|
||||||
|
return streamableBytesForTree - allocatedBytesForTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the number of bytes allocated to this stream. This is called at the end of the priority
|
||||||
|
* algorithm for each stream to reset the count for the next invocation.
|
||||||
|
*/
|
||||||
|
private void resetAllocatedBytesForTree() {
|
||||||
|
allocatedBytesForTree = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increments the flow control window for this stream by the given delta and returns the new value.
|
* Increments the flow control window for this stream by the given delta and returns the new value.
|
||||||
*/
|
*/
|
||||||
@ -414,7 +415,8 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
|
|
||||||
// Update this branch of the priority tree if the streamable bytes have changed for this
|
// Update this branch of the priority tree if the streamable bytes have changed for this
|
||||||
// node.
|
// node.
|
||||||
incrementPriorityBytes(streamableBytes() - previouslyStreamable);
|
int streamableDelta = streamableBytes() - previouslyStreamable;
|
||||||
|
incrementStreamableBytesForTree(streamableDelta);
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,41 +444,17 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
return max(0, min(pendingBytes, window));
|
return max(0, min(pendingBytes, window));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
int streamableBytesForTree() {
|
||||||
* The aggregate total of all {@link #streamableBytes()} for subtree rooted at this node.
|
return streamableBytesForTree;
|
||||||
*/
|
|
||||||
int priorityBytes() {
|
|
||||||
return priorityBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used by the priority algorithm to allocate bytes to this stream.
|
|
||||||
*/
|
|
||||||
private void allocatePriorityBytes(int bytes) {
|
|
||||||
allocatedPriorityBytes += bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used by the priority algorithm to get the intermediate allocation of bytes to this stream.
|
|
||||||
*/
|
|
||||||
int allocatedPriorityBytes() {
|
|
||||||
return allocatedPriorityBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used by the priority algorithm to determine the number of writable bytes that have not yet been allocated.
|
|
||||||
*/
|
|
||||||
private int unallocatedPriorityBytes() {
|
|
||||||
return priorityBytes - allocatedPriorityBytes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new frame with the given values but does not add it to the pending queue.
|
* Creates a new frame with the given values but does not add it to the pending queue.
|
||||||
*/
|
*/
|
||||||
private Frame newFrame(ChannelPromise promise, ByteBuf data, int padding, boolean endStream) {
|
private Frame newFrame(final ChannelPromise promise, ByteBuf data, int padding, boolean endStream) {
|
||||||
// Store this as the future for the most recent write attempt.
|
// Store this as the future for the most recent write attempt.
|
||||||
lastNewFrame = promise;
|
lastNewFrame = promise;
|
||||||
return new Frame(new ChannelPromiseAggregator(promise), data, padding, endStream);
|
return new Frame(new SimplePromiseAggregator(promise), data, padding, endStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -489,7 +467,7 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
/**
|
/**
|
||||||
* Returns the the head of the pending queue, or {@code null} if empty.
|
* Returns the the head of the pending queue, or {@code null} if empty.
|
||||||
*/
|
*/
|
||||||
Frame peek() {
|
private Frame peek() {
|
||||||
return pendingWriteQueue.peek();
|
return pendingWriteQueue.peek();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -512,7 +490,7 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
* the number of pending writes available, or because a frame does not support splitting on arbitrary
|
* the number of pending writes available, or because a frame does not support splitting on arbitrary
|
||||||
* boundaries.
|
* boundaries.
|
||||||
*/
|
*/
|
||||||
private int writeBytes(int bytes) throws Http2Exception {
|
private int writeBytes(int bytes) {
|
||||||
int bytesWritten = 0;
|
int bytesWritten = 0;
|
||||||
if (!stream.localSideOpen()) {
|
if (!stream.localSideOpen()) {
|
||||||
return bytesWritten;
|
return bytesWritten;
|
||||||
@ -544,13 +522,14 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively increments the priority bytes for this branch in the priority tree starting at the current node.
|
* Recursively increments the streamable bytes for this branch in the priority tree starting
|
||||||
|
* at the current node.
|
||||||
*/
|
*/
|
||||||
private void incrementPriorityBytes(int numBytes) {
|
private void incrementStreamableBytesForTree(int numBytes) {
|
||||||
if (numBytes != 0) {
|
if (numBytes != 0) {
|
||||||
priorityBytes += numBytes;
|
streamableBytesForTree += numBytes;
|
||||||
if (!stream.isRoot()) {
|
if (!stream.isRoot()) {
|
||||||
state(stream.parent()).incrementPriorityBytes(numBytes);
|
state(stream.parent()).incrementStreamableBytesForTree(numBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -561,12 +540,12 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
private final class Frame {
|
private final class Frame {
|
||||||
final ByteBuf data;
|
final ByteBuf data;
|
||||||
final boolean endStream;
|
final boolean endStream;
|
||||||
final ChannelPromiseAggregator promiseAggregator;
|
final SimplePromiseAggregator promiseAggregator;
|
||||||
final ChannelPromise promise;
|
final ChannelPromise promise;
|
||||||
int padding;
|
int padding;
|
||||||
boolean enqueued;
|
boolean enqueued;
|
||||||
|
|
||||||
Frame(ChannelPromiseAggregator promiseAggregator, ByteBuf data, int padding,
|
Frame(SimplePromiseAggregator promiseAggregator, ByteBuf data, int padding,
|
||||||
boolean endStream) {
|
boolean endStream) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.padding = padding;
|
this.padding = padding;
|
||||||
@ -594,8 +573,9 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increments the number of pending bytes for this node. If there was any change to the number of bytes that
|
* Increments the number of pending bytes for this node. If there was any change to the
|
||||||
* fit into the stream window, then {@link #incrementPriorityBytes} to recursively update this branch of the
|
* number of bytes that fit into the stream window, then
|
||||||
|
* {@link #incrementStreamableBytesForTree} to recursively update this branch of the
|
||||||
* priority tree.
|
* priority tree.
|
||||||
*/
|
*/
|
||||||
private void incrementPendingBytes(int numBytes) {
|
private void incrementPendingBytes(int numBytes) {
|
||||||
@ -603,7 +583,7 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
pendingBytes += numBytes;
|
pendingBytes += numBytes;
|
||||||
|
|
||||||
int delta = streamableBytes() - previouslyStreamable;
|
int delta = streamableBytes() - previouslyStreamable;
|
||||||
incrementPriorityBytes(delta);
|
incrementStreamableBytesForTree(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -613,7 +593,7 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
* <p>
|
* <p>
|
||||||
* Note: this does not flush the {@link ChannelHandlerContext}.
|
* Note: this does not flush the {@link ChannelHandlerContext}.
|
||||||
*/
|
*/
|
||||||
void write() throws Http2Exception {
|
void write() {
|
||||||
// Using a do/while loop because if the buffer is empty we still need to call
|
// Using a do/while loop because if the buffer is empty we still need to call
|
||||||
// the writer once to send the empty frame.
|
// the writer once to send the empty frame.
|
||||||
final Http2FrameSizePolicy frameSizePolicy = frameWriter.configuration().frameSizePolicy();
|
final Http2FrameSizePolicy frameSizePolicy = frameWriter.configuration().frameSizePolicy();
|
||||||
@ -622,9 +602,15 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
int frameBytes = Math.min(bytesToWrite, frameSizePolicy.maxFrameSize());
|
int frameBytes = Math.min(bytesToWrite, frameSizePolicy.maxFrameSize());
|
||||||
if (frameBytes == bytesToWrite) {
|
if (frameBytes == bytesToWrite) {
|
||||||
// All the bytes fit into a single HTTP/2 frame, just send it all.
|
// All the bytes fit into a single HTTP/2 frame, just send it all.
|
||||||
|
try {
|
||||||
connectionState().incrementStreamWindow(-bytesToWrite);
|
connectionState().incrementStreamWindow(-bytesToWrite);
|
||||||
incrementStreamWindow(-bytesToWrite);
|
incrementStreamWindow(-bytesToWrite);
|
||||||
|
} catch (Http2Exception e) {
|
||||||
|
// Should never get here since we're decrementing.
|
||||||
|
throw new AssertionError("Invalid window state when writing frame: " + e.getMessage());
|
||||||
|
}
|
||||||
frameWriter.writeData(ctx, stream.id(), data, padding, endStream, promise);
|
frameWriter.writeData(ctx, stream.id(), data, padding, endStream, promise);
|
||||||
|
frameSent = true;
|
||||||
decrementPendingBytes(bytesToWrite);
|
decrementPendingBytes(bytesToWrite);
|
||||||
if (enqueued) {
|
if (enqueued) {
|
||||||
// It's enqueued - remove it from the head of the pending write queue.
|
// It's enqueued - remove it from the head of the pending write queue.
|
||||||
@ -659,8 +645,6 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
* @return the partial frame.
|
* @return the partial frame.
|
||||||
*/
|
*/
|
||||||
Frame split(int maxBytes) {
|
Frame split(int maxBytes) {
|
||||||
// TODO: Should padding be spread across chunks or only at the end?
|
|
||||||
|
|
||||||
// The requested maxBytes should always be less than the size of this frame.
|
// The requested maxBytes should always be less than the size of this frame.
|
||||||
assert maxBytes < size() : "Attempting to split a frame for the full size.";
|
assert maxBytes < size() : "Attempting to split a frame for the full size.";
|
||||||
|
|
||||||
@ -690,4 +674,33 @@ public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowCont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight promise aggregator.
|
||||||
|
*/
|
||||||
|
private class SimplePromiseAggregator {
|
||||||
|
final ChannelPromise promise;
|
||||||
|
final AtomicInteger awaiting = new AtomicInteger();
|
||||||
|
final ChannelFutureListener listener = new ChannelFutureListener() {
|
||||||
|
@Override
|
||||||
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
|
if (!future.isSuccess()) {
|
||||||
|
promise.tryFailure(future.cause());
|
||||||
|
} else {
|
||||||
|
if (awaiting.decrementAndGet() == 0) {
|
||||||
|
promise.trySuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SimplePromiseAggregator(ChannelPromise promise) {
|
||||||
|
this.promise = promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(ChannelPromise promise) {
|
||||||
|
awaiting.incrementAndGet();
|
||||||
|
promise.addListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package io.netty.handler.codec.http2;
|
|||||||
import static io.netty.handler.codec.http2.DefaultHttp2InboundFlowController.WINDOW_UPDATE_OFF;
|
import static io.netty.handler.codec.http2.DefaultHttp2InboundFlowController.WINDOW_UPDATE_OFF;
|
||||||
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
|
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
|
||||||
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
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.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
@ -71,14 +72,14 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void dataFrameShouldBeAccepted() throws Http2Exception {
|
public void dataFrameShouldBeAccepted() throws Http2Exception {
|
||||||
applyFlowControl(10, 0, false);
|
applyFlowControl(STREAM_ID, 10, 0, false);
|
||||||
verifyWindowUpdateNotSent();
|
verifyWindowUpdateNotSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = Http2Exception.class)
|
@Test(expected = Http2Exception.class)
|
||||||
public void connectionFlowControlExceededShouldThrow() throws Http2Exception {
|
public void connectionFlowControlExceededShouldThrow() throws Http2Exception {
|
||||||
// Window exceeded because of the padding.
|
// Window exceeded because of the padding.
|
||||||
applyFlowControl(DEFAULT_WINDOW_SIZE, 1, true);
|
applyFlowControl(STREAM_ID, DEFAULT_WINDOW_SIZE, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -88,7 +89,7 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
int windowDelta = DEFAULT_WINDOW_SIZE - newWindow;
|
int windowDelta = DEFAULT_WINDOW_SIZE - newWindow;
|
||||||
|
|
||||||
// Set end-of-stream on the frame, so no window update will be sent for the stream.
|
// Set end-of-stream on the frame, so no window update will be sent for the stream.
|
||||||
applyFlowControl(dataSize, 0, true);
|
applyFlowControl(STREAM_ID, dataSize, 0, true);
|
||||||
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
|
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
int dataSize = DEFAULT_WINDOW_SIZE / 2 + 1;
|
int dataSize = DEFAULT_WINDOW_SIZE / 2 + 1;
|
||||||
|
|
||||||
// Set end-of-stream on the frame, so no window update will be sent for the stream.
|
// Set end-of-stream on the frame, so no window update will be sent for the stream.
|
||||||
applyFlowControl(dataSize, 0, true);
|
applyFlowControl(STREAM_ID, dataSize, 0, true);
|
||||||
verifyWindowUpdateNotSent();
|
verifyWindowUpdateNotSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
|
int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
|
||||||
|
|
||||||
// Don't set end-of-stream so we'll get a window update for the stream as well.
|
// Don't set end-of-stream so we'll get a window update for the stream as well.
|
||||||
applyFlowControl(dataSize, 0, false);
|
applyFlowControl(STREAM_ID, dataSize, 0, false);
|
||||||
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
|
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
|
||||||
verifyWindowUpdateSent(STREAM_ID, windowDelta);
|
verifyWindowUpdateSent(STREAM_ID, windowDelta);
|
||||||
}
|
}
|
||||||
@ -122,7 +123,7 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
|
int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
|
||||||
|
|
||||||
// Don't set end-of-stream so we'll get a window update for the stream as well.
|
// Don't set end-of-stream so we'll get a window update for the stream as well.
|
||||||
applyFlowControl(dataSize, 0, false);
|
applyFlowControl(STREAM_ID, dataSize, 0, false);
|
||||||
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
|
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
|
||||||
verifyWindowUpdateNotSent(STREAM_ID);
|
verifyWindowUpdateNotSent(STREAM_ID);
|
||||||
}
|
}
|
||||||
@ -131,7 +132,7 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception {
|
public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception {
|
||||||
// Send a frame that takes up the entire window.
|
// Send a frame that takes up the entire window.
|
||||||
int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
int initialWindowSize = DEFAULT_WINDOW_SIZE;
|
||||||
applyFlowControl(initialWindowSize, 0, false);
|
applyFlowControl(STREAM_ID, initialWindowSize, 0, false);
|
||||||
|
|
||||||
// Update the initial window size to allow another frame.
|
// Update the initial window size to allow another frame.
|
||||||
int newInitialWindowSize = 2 * initialWindowSize;
|
int newInitialWindowSize = 2 * initialWindowSize;
|
||||||
@ -141,21 +142,66 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
reset(frameWriter);
|
reset(frameWriter);
|
||||||
|
|
||||||
// Send the next frame and verify that the expected window updates were sent.
|
// Send the next frame and verify that the expected window updates were sent.
|
||||||
applyFlowControl(initialWindowSize, 0, false);
|
applyFlowControl(STREAM_ID, initialWindowSize, 0, false);
|
||||||
int delta = newInitialWindowSize - initialWindowSize;
|
int delta = newInitialWindowSize - initialWindowSize;
|
||||||
verifyWindowUpdateSent(CONNECTION_STREAM_ID, delta);
|
verifyWindowUpdateSent(CONNECTION_STREAM_ID, delta);
|
||||||
verifyWindowUpdateSent(STREAM_ID, delta);
|
verifyWindowUpdateSent(STREAM_ID, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void connectionWindowShouldExpandWithNumberOfStreams() throws Http2Exception {
|
||||||
|
// Create another stream
|
||||||
|
int newStreamId = 3;
|
||||||
|
connection.local().createStream(newStreamId, false);
|
||||||
|
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_ID));
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
|
||||||
|
|
||||||
|
// Receive some data - this should cause the connection window to expand.
|
||||||
|
int data1 = 50;
|
||||||
|
int expectedMaxConnectionWindow = DEFAULT_WINDOW_SIZE * 2;
|
||||||
|
applyFlowControl(STREAM_ID, data1, 0, false);
|
||||||
|
verifyWindowUpdateNotSent(STREAM_ID);
|
||||||
|
verifyWindowUpdateSent(CONNECTION_STREAM_ID, DEFAULT_WINDOW_SIZE + data1);
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
|
||||||
|
assertEquals(expectedMaxConnectionWindow, window(CONNECTION_STREAM_ID));
|
||||||
|
|
||||||
|
reset(frameWriter);
|
||||||
|
|
||||||
|
// Close the new stream.
|
||||||
|
connection.stream(newStreamId).close();
|
||||||
|
|
||||||
|
// Read more data and verify that the stream window refreshes but the
|
||||||
|
// connection window continues collapsing.
|
||||||
|
int data2 = window(STREAM_ID);
|
||||||
|
applyFlowControl(STREAM_ID, data2, 0, false);
|
||||||
|
verifyWindowUpdateSent(STREAM_ID, data1 + data2);
|
||||||
|
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_ID));
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE * 2 - data2 , window(CONNECTION_STREAM_ID));
|
||||||
|
|
||||||
|
reset(frameWriter);
|
||||||
|
|
||||||
|
// Read enough data to cause a WINDOW_UPDATE for both the stream and connection and
|
||||||
|
// verify the new maximum of the connection window.
|
||||||
|
int data3 = window(STREAM_ID);
|
||||||
|
applyFlowControl(STREAM_ID, data3, 0, false);
|
||||||
|
verifyWindowUpdateSent(STREAM_ID, DEFAULT_WINDOW_SIZE);
|
||||||
|
verifyWindowUpdateSent(CONNECTION_STREAM_ID, DEFAULT_WINDOW_SIZE
|
||||||
|
- (DEFAULT_WINDOW_SIZE * 2 - (data2 + data3)));
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_ID));
|
||||||
|
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
|
||||||
|
}
|
||||||
|
|
||||||
private static int getWindowDelta(int initialSize, int windowSize, int dataSize) {
|
private static int getWindowDelta(int initialSize, int windowSize, int dataSize) {
|
||||||
int newWindowSize = windowSize - dataSize;
|
int newWindowSize = windowSize - dataSize;
|
||||||
return initialSize - newWindowSize;
|
return initialSize - newWindowSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFlowControl(int dataSize, int padding, boolean endOfStream) throws Http2Exception {
|
private void applyFlowControl(int streamId, int dataSize, int padding, boolean endOfStream) throws Http2Exception {
|
||||||
final ByteBuf buf = dummyData(dataSize);
|
final ByteBuf buf = dummyData(dataSize);
|
||||||
try {
|
try {
|
||||||
controller.onDataRead(ctx, STREAM_ID, buf, padding, endOfStream);
|
controller.onDataRead(ctx, streamId, buf, padding, endOfStream);
|
||||||
} finally {
|
} finally {
|
||||||
buf.release();
|
buf.release();
|
||||||
}
|
}
|
||||||
@ -179,4 +225,8 @@ public class DefaultHttp2InboundFlowControllerTest {
|
|||||||
verify(frameWriter, never()).writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt(),
|
verify(frameWriter, never()).writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt(),
|
||||||
any(ChannelPromise.class));
|
any(ChannelPromise.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int window(int streamId) {
|
||||||
|
return connection.stream(streamId).inboundFlow().window();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -317,6 +317,52 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successiveSendsShouldNotInteract() throws Http2Exception {
|
||||||
|
// Collapse the connection window to force queueing.
|
||||||
|
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -window(CONNECTION_STREAM_ID));
|
||||||
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
|
|
||||||
|
ByteBuf data = dummyData(5, 5);
|
||||||
|
ByteBuf dataOnly = data.slice(0, 5);
|
||||||
|
try {
|
||||||
|
// Queue data for stream A and allow most of it to be written.
|
||||||
|
send(STREAM_A, dataOnly.slice(), 5);
|
||||||
|
verifyNoWrite(STREAM_A);
|
||||||
|
verifyNoWrite(STREAM_B);
|
||||||
|
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 8);
|
||||||
|
ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
|
||||||
|
captureWrite(STREAM_A, argument, 3, false);
|
||||||
|
ByteBuf writtenBuf = argument.getValue();
|
||||||
|
assertEquals(dataOnly, writtenBuf);
|
||||||
|
assertEquals(65527, window(STREAM_A));
|
||||||
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
|
|
||||||
|
resetFrameWriter();
|
||||||
|
|
||||||
|
// Queue data for stream B and allow the rest of A and all of B to be written.
|
||||||
|
send(STREAM_B, dataOnly.slice(), 5);
|
||||||
|
verifyNoWrite(STREAM_A);
|
||||||
|
verifyNoWrite(STREAM_B);
|
||||||
|
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 12);
|
||||||
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
|
|
||||||
|
// Verify the rest of A is written.
|
||||||
|
captureWrite(STREAM_A, argument, 2, false);
|
||||||
|
writtenBuf = argument.getValue();
|
||||||
|
assertEquals(Unpooled.EMPTY_BUFFER, writtenBuf);
|
||||||
|
assertEquals(65525, window(STREAM_A));
|
||||||
|
|
||||||
|
argument = ArgumentCaptor.forClass(ByteBuf.class);
|
||||||
|
captureWrite(STREAM_B, argument, 5, false);
|
||||||
|
writtenBuf = argument.getValue();
|
||||||
|
assertEquals(dataOnly, writtenBuf);
|
||||||
|
assertEquals(65525, window(STREAM_B));
|
||||||
|
} finally {
|
||||||
|
manualSafeRelease(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void negativeWindowShouldNotThrowException() throws Http2Exception {
|
public void negativeWindowShouldNotThrowException() throws Http2Exception {
|
||||||
final int initWindow = 20;
|
final int initWindow = 20;
|
||||||
@ -609,8 +655,8 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10);
|
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10);
|
||||||
assertEquals(0, window(CONNECTION_STREAM_ID));
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
assertEquals(0, window(STREAM_A));
|
assertEquals(0, window(STREAM_A));
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_B));
|
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_B), 2);
|
||||||
assertEquals((2 * DEFAULT_WINDOW_SIZE) - 5, window(STREAM_C) + window(STREAM_D));
|
assertEquals((2 * DEFAULT_WINDOW_SIZE) - 5, window(STREAM_C) + window(STREAM_D), 5);
|
||||||
|
|
||||||
final ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
|
final ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
|
||||||
|
|
||||||
@ -618,7 +664,7 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
verifyNoWrite(STREAM_A);
|
verifyNoWrite(STREAM_A);
|
||||||
|
|
||||||
captureWrite(STREAM_B, captor, 0, false);
|
captureWrite(STREAM_B, captor, 0, false);
|
||||||
assertEquals(5, captor.getValue().readableBytes());
|
assertEquals(5, captor.getValue().readableBytes(), 2);
|
||||||
|
|
||||||
// Verify that C and D each shared half of A's allowance. Since A's allowance (5) cannot
|
// Verify that C and D each shared half of A's allowance. Since A's allowance (5) cannot
|
||||||
// be split evenly, one will get 3 and one will get 2.
|
// be split evenly, one will get 3 and one will get 2.
|
||||||
@ -626,7 +672,7 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
int c = captor.getValue().readableBytes();
|
int c = captor.getValue().readableBytes();
|
||||||
captureWrite(STREAM_D, captor, 0, false);
|
captureWrite(STREAM_D, captor, 0, false);
|
||||||
int d = captor.getValue().readableBytes();
|
int d = captor.getValue().readableBytes();
|
||||||
assertEquals(5, c + d);
|
assertEquals(5, c + d, 4);
|
||||||
assertEquals(1, Math.abs(c - d));
|
assertEquals(1, Math.abs(c - d));
|
||||||
} finally {
|
} finally {
|
||||||
manualSafeRelease(bufs);
|
manualSafeRelease(bufs);
|
||||||
@ -795,18 +841,18 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
// Verify that the entire frame was sent.
|
// Verify that the entire frame was sent.
|
||||||
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10);
|
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10);
|
||||||
assertEquals(0, window(CONNECTION_STREAM_ID));
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_A));
|
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_A), 2);
|
||||||
assertEquals(0, window(STREAM_B));
|
assertEquals(0, window(STREAM_B));
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_D));
|
assertEquals(DEFAULT_WINDOW_SIZE - 5, window(STREAM_D), 2);
|
||||||
|
|
||||||
final ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
|
final ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
|
||||||
|
|
||||||
// Verify that A received all the bytes.
|
// Verify that A received all the bytes.
|
||||||
captureWrite(STREAM_A, captor, 0, false);
|
captureWrite(STREAM_A, captor, 0, false);
|
||||||
assertEquals(5, captor.getValue().readableBytes());
|
assertEquals(5, captor.getValue().readableBytes(), 2);
|
||||||
captureWrite(STREAM_D, captor, 0, false);
|
captureWrite(STREAM_D, captor, 0, false);
|
||||||
assertEquals(5, captor.getValue().readableBytes());
|
assertEquals(5, captor.getValue().readableBytes(), 2);
|
||||||
verifyNoWrite(STREAM_B);
|
verifyNoWrite(STREAM_B);
|
||||||
verifyNoWrite(STREAM_C);
|
verifyNoWrite(STREAM_C);
|
||||||
} finally {
|
} finally {
|
||||||
@ -875,7 +921,7 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
assertEquals(aWritten, min);
|
assertEquals(aWritten, min);
|
||||||
assertEquals(bWritten, max);
|
assertEquals(bWritten, max);
|
||||||
assertTrue(aWritten < cWritten);
|
assertTrue(aWritten < cWritten);
|
||||||
assertEquals(cWritten, dWritten);
|
assertEquals(cWritten, dWritten, 1);
|
||||||
assertTrue(cWritten < bWritten);
|
assertTrue(cWritten < bWritten);
|
||||||
|
|
||||||
assertEquals(0, window(CONNECTION_STREAM_ID));
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
@ -899,7 +945,7 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void samePriorityShouldWriteEqualData() throws Http2Exception {
|
public void samePriorityShouldDistributeBasedOnData() throws Http2Exception {
|
||||||
// Block the connection
|
// Block the connection
|
||||||
exhaustStreamWindow(CONNECTION_STREAM_ID);
|
exhaustStreamWindow(CONNECTION_STREAM_ID);
|
||||||
|
|
||||||
@ -928,10 +974,10 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
// Allow 1000 bytes to be sent.
|
// Allow 1000 bytes to be sent.
|
||||||
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 999);
|
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 999);
|
||||||
assertEquals(0, window(CONNECTION_STREAM_ID));
|
assertEquals(0, window(CONNECTION_STREAM_ID));
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 333, window(STREAM_A));
|
assertEquals(DEFAULT_WINDOW_SIZE - 333, window(STREAM_A), 50);
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 333, window(STREAM_B));
|
assertEquals(DEFAULT_WINDOW_SIZE - 333, window(STREAM_B), 50);
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_C));
|
||||||
assertEquals(DEFAULT_WINDOW_SIZE - 333, window(STREAM_D));
|
assertEquals(DEFAULT_WINDOW_SIZE - 333, window(STREAM_D), 50);
|
||||||
|
|
||||||
captureWrite(STREAM_A, captor, 0, false);
|
captureWrite(STREAM_A, captor, 0, false);
|
||||||
int aWritten = captor.getValue().readableBytes();
|
int aWritten = captor.getValue().readableBytes();
|
||||||
@ -943,9 +989,9 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
int dWritten = captor.getValue().readableBytes();
|
int dWritten = captor.getValue().readableBytes();
|
||||||
|
|
||||||
assertEquals(999, aWritten + bWritten + dWritten);
|
assertEquals(999, aWritten + bWritten + dWritten);
|
||||||
assertEquals(333, aWritten);
|
assertEquals(333, aWritten, 50);
|
||||||
assertEquals(333, bWritten);
|
assertEquals(333, bWritten, 50);
|
||||||
assertEquals(333, dWritten);
|
assertEquals(333, dWritten, 50);
|
||||||
} finally {
|
} finally {
|
||||||
manualSafeRelease(bufs);
|
manualSafeRelease(bufs);
|
||||||
}
|
}
|
||||||
@ -993,16 +1039,16 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
|
|
||||||
OutboundFlowState state = state(stream0);
|
OutboundFlowState state = state(stream0);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamA);
|
state = state(streamA);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamB);
|
state = state(streamB);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B)), state.streamableBytesForTree());
|
||||||
state = state(streamC);
|
state = state(streamC);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.streamableBytesForTree());
|
||||||
state = state(streamD);
|
state = state(streamD);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.streamableBytesForTree());
|
||||||
} finally {
|
} finally {
|
||||||
manualSafeRelease(bufs);
|
manualSafeRelease(bufs);
|
||||||
}
|
}
|
||||||
@ -1063,17 +1109,17 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
streamB.setPriority(STREAM_A, DEFAULT_PRIORITY_WEIGHT, true);
|
streamB.setPriority(STREAM_A, DEFAULT_PRIORITY_WEIGHT, true);
|
||||||
OutboundFlowState state = state(stream0);
|
OutboundFlowState state = state(stream0);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamA);
|
state = state(streamA);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamB);
|
state = state(streamB);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamC);
|
state = state(streamC);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.streamableBytesForTree());
|
||||||
state = state(streamD);
|
state = state(streamD);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.streamableBytesForTree());
|
||||||
} finally {
|
} finally {
|
||||||
manualSafeRelease(bufs);
|
manualSafeRelease(bufs);
|
||||||
}
|
}
|
||||||
@ -1142,19 +1188,19 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
assertEquals(
|
assertEquals(
|
||||||
calculateStreamSizeSum(streamSizes,
|
calculateStreamSizeSum(streamSizes,
|
||||||
Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D, STREAM_E)),
|
Arrays.asList(STREAM_A, STREAM_B, STREAM_C, STREAM_D, STREAM_E)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamA);
|
state = state(streamA);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_E, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_A, STREAM_E, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamB);
|
state = state(streamB);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B)), state.streamableBytesForTree());
|
||||||
state = state(streamC);
|
state = state(streamC);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.streamableBytesForTree());
|
||||||
state = state(streamD);
|
state = state(streamD);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.streamableBytesForTree());
|
||||||
state = state(streamE);
|
state = state(streamE);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_E, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_E, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
} finally {
|
} finally {
|
||||||
manualSafeRelease(bufs);
|
manualSafeRelease(bufs);
|
||||||
}
|
}
|
||||||
@ -1212,15 +1258,15 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
|
|
||||||
OutboundFlowState state = state(stream0);
|
OutboundFlowState state = state(stream0);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B, STREAM_C, STREAM_D)),
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B, STREAM_C, STREAM_D)),
|
||||||
state.priorityBytes());
|
state.streamableBytesForTree());
|
||||||
state = state(streamA);
|
state = state(streamA);
|
||||||
assertEquals(0, state.priorityBytes());
|
assertEquals(0, state.streamableBytesForTree());
|
||||||
state = state(streamB);
|
state = state(streamB);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_B)), state.streamableBytesForTree());
|
||||||
state = state(streamC);
|
state = state(streamC);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_C)), state.streamableBytesForTree());
|
||||||
state = state(streamD);
|
state = state(streamD);
|
||||||
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.priorityBytes());
|
assertEquals(calculateStreamSizeSum(streamSizes, Arrays.asList(STREAM_D)), state.streamableBytesForTree());
|
||||||
} finally {
|
} finally {
|
||||||
manualSafeRelease(bufs);
|
manualSafeRelease(bufs);
|
||||||
}
|
}
|
||||||
@ -1264,37 +1310,7 @@ public class DefaultHttp2OutboundFlowControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void exhaustStreamWindow(int streamId) throws Http2Exception {
|
private void exhaustStreamWindow(int streamId) throws Http2Exception {
|
||||||
final int dataLength = window(streamId);
|
controller.updateOutboundWindowSize(streamId, -window(streamId));
|
||||||
final ByteBuf data = dummyData(dataLength, 0);
|
|
||||||
try {
|
|
||||||
if (streamId == CONNECTION_STREAM_ID) {
|
|
||||||
// Find a stream that we can use to shrink the connection window.
|
|
||||||
int streamToWrite = 0;
|
|
||||||
for (Http2Stream stream : connection.activeStreams()) {
|
|
||||||
if (stream.outboundFlow().window() >= dataLength) {
|
|
||||||
streamToWrite = stream.id();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to STREAM_A to decrease the connection window and then restore STREAM_A's window.
|
|
||||||
int prevWindow = window(streamToWrite);
|
|
||||||
send(streamToWrite, data, 0);
|
|
||||||
int delta = prevWindow - window(streamToWrite);
|
|
||||||
controller.updateOutboundWindowSize(streamToWrite, delta);
|
|
||||||
} else {
|
|
||||||
// Write to the stream and then restore the connection window.
|
|
||||||
int prevWindow = window(CONNECTION_STREAM_ID);
|
|
||||||
send(streamId, data, 0);
|
|
||||||
int delta = prevWindow - window(CONNECTION_STREAM_ID);
|
|
||||||
controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the frameWriter so that this write doesn't interfere with other tests.
|
|
||||||
resetFrameWriter();
|
|
||||||
} finally {
|
|
||||||
manualSafeRelease(data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetFrameWriter() {
|
private void resetFrameWriter() {
|
||||||
|
@ -25,15 +25,18 @@ import static org.junit.Assert.assertEquals;
|
|||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Matchers.anyBoolean;
|
||||||
import static org.mockito.Matchers.anyInt;
|
import static org.mockito.Matchers.anyInt;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
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 io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
@ -52,9 +55,6 @@ import io.netty.util.concurrent.Future;
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -63,7 +63,6 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
@ -92,6 +91,8 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
@Before
|
@Before
|
||||||
public void setup() throws Exception {
|
public void setup() throws Exception {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
|
mockFlowControl(clientListener);
|
||||||
|
mockFlowControl(serverListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -248,19 +249,20 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
final int length = 10485760; // 10MB
|
final int length = 10485760; // 10MB
|
||||||
|
|
||||||
// Create a buffer filled with random bytes.
|
// Create a buffer filled with random bytes.
|
||||||
final byte[] bytes = new byte[length];
|
final ByteBuf data = randomBytes(length);
|
||||||
new Random().nextBytes(bytes);
|
|
||||||
final ByteBuf data = Unpooled.wrappedBuffer(bytes);
|
|
||||||
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
|
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
|
||||||
doAnswer(new Answer<Void>() {
|
doAnswer(new Answer<Integer>() {
|
||||||
@Override
|
@Override
|
||||||
public Void answer(InvocationOnMock in) throws Throwable {
|
public Integer answer(InvocationOnMock in) throws Throwable {
|
||||||
ByteBuf buf = (ByteBuf) in.getArguments()[2];
|
ByteBuf buf = (ByteBuf) in.getArguments()[2];
|
||||||
|
int padding = (Integer) in.getArguments()[3];
|
||||||
|
int processedBytes = buf.readableBytes() + padding;
|
||||||
|
|
||||||
buf.readBytes(out, buf.readableBytes());
|
buf.readBytes(out, buf.readableBytes());
|
||||||
return null;
|
return processedBytes;
|
||||||
}
|
}
|
||||||
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
|
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
|
||||||
any(ByteBuf.class), eq(0), Mockito.anyBoolean());
|
any(ByteBuf.class), eq(0), anyBoolean());
|
||||||
try {
|
try {
|
||||||
// Initialize the data latch based on the number of bytes expected.
|
// Initialize the data latch based on the number of bytes expected.
|
||||||
bootstrapEnv(length, 2, 1);
|
bootstrapEnv(length, 2, 1);
|
||||||
@ -292,7 +294,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
assertEquals(0, dataLatch.getCount());
|
assertEquals(0, dataLatch.getCount());
|
||||||
out.flush();
|
out.flush();
|
||||||
byte[] received = out.toByteArray();
|
byte[] received = out.toByteArray();
|
||||||
assertArrayEquals(bytes, received);
|
assertArrayEquals(data.array(), received);
|
||||||
} finally {
|
} finally {
|
||||||
data.release();
|
data.release();
|
||||||
out.close();
|
out.close();
|
||||||
@ -302,62 +304,80 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
@Test
|
@Test
|
||||||
public void stressTest() throws Exception {
|
public void stressTest() throws Exception {
|
||||||
final Http2Headers headers = dummyHeaders();
|
final Http2Headers headers = dummyHeaders();
|
||||||
final String text = "hello world";
|
|
||||||
final String pingMsg = "12345678";
|
final String pingMsg = "12345678";
|
||||||
final ByteBuf data = Unpooled.copiedBuffer(text, UTF_8);
|
int length = 10;
|
||||||
|
final ByteBuf data = randomBytes(length);
|
||||||
|
final String dataAsHex = ByteBufUtil.hexDump(data);
|
||||||
final ByteBuf pingData = Unpooled.copiedBuffer(pingMsg, UTF_8);
|
final ByteBuf pingData = Unpooled.copiedBuffer(pingMsg, UTF_8);
|
||||||
final int numStreams = 5000;
|
final int numStreams = 2000;
|
||||||
final List<String> receivedPingBuffers = Collections.synchronizedList(new ArrayList<String>(numStreams));
|
|
||||||
|
// Collect all the ping buffers as we receive them at the server.
|
||||||
|
final String[] receivedPings = new String[numStreams];
|
||||||
doAnswer(new Answer<Void>() {
|
doAnswer(new Answer<Void>() {
|
||||||
|
int nextIndex;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Void answer(InvocationOnMock in) throws Throwable {
|
public Void answer(InvocationOnMock in) throws Throwable {
|
||||||
receivedPingBuffers.add(((ByteBuf) in.getArguments()[1]).toString(UTF_8));
|
receivedPings[nextIndex++] = ((ByteBuf) in.getArguments()[1]).toString(UTF_8);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).when(serverListener).onPingRead(any(ChannelHandlerContext.class), eq(pingData));
|
}).when(serverListener).onPingRead(any(ChannelHandlerContext.class), any(ByteBuf.class));
|
||||||
final List<String> receivedDataBuffers = Collections.synchronizedList(new ArrayList<String>(numStreams));
|
|
||||||
doAnswer(new Answer<Void>() {
|
// Collect all the data buffers as we receive them at the server.
|
||||||
|
final StringBuilder[] receivedData = new StringBuilder[numStreams];
|
||||||
|
doAnswer(new Answer<Integer>() {
|
||||||
@Override
|
@Override
|
||||||
public Void answer(InvocationOnMock in) throws Throwable {
|
public Integer answer(InvocationOnMock in) throws Throwable {
|
||||||
receivedDataBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
|
int streamId = (Integer) in.getArguments()[1];
|
||||||
return null;
|
ByteBuf buf = (ByteBuf) in.getArguments()[2];
|
||||||
|
int padding = (Integer) in.getArguments()[3];
|
||||||
|
int processedBytes = buf.readableBytes() + padding;
|
||||||
|
|
||||||
|
int streamIndex = (streamId - 3) / 2;
|
||||||
|
StringBuilder builder = receivedData[streamIndex];
|
||||||
|
if (builder == null) {
|
||||||
|
builder = new StringBuilder(dataAsHex.length());
|
||||||
|
receivedData[streamIndex] = builder;
|
||||||
}
|
}
|
||||||
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), anyInt(), eq(data),
|
builder.append(ByteBufUtil.hexDump(buf));
|
||||||
eq(0), eq(false));
|
return processedBytes;
|
||||||
|
}
|
||||||
|
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), anyInt(),
|
||||||
|
any(ByteBuf.class), anyInt(), anyBoolean());
|
||||||
try {
|
try {
|
||||||
bootstrapEnv(numStreams * text.length(), numStreams * 4, numStreams);
|
bootstrapEnv(numStreams * length, numStreams * 4, numStreams);
|
||||||
runInChannel(clientChannel, new Http2Runnable() {
|
runInChannel(clientChannel, new Http2Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
for (int i = 0, nextStream = 3; i < numStreams; ++i, nextStream += 2) {
|
int upperLimit = 3 + 2 * numStreams;
|
||||||
http2Client.encoder().writeHeaders(ctx(), nextStream, headers, 0,
|
for (int streamId = 3; streamId < upperLimit; streamId += 2) {
|
||||||
(short) 16, false, 0, false, newPromise());
|
// Send a bunch of data on each stream.
|
||||||
|
http2Client.encoder().writeHeaders(ctx(), streamId, headers, 0, (short) 16,
|
||||||
|
false, 0, false, newPromise());
|
||||||
http2Client.encoder().writePing(ctx(), false, pingData.slice().retain(),
|
http2Client.encoder().writePing(ctx(), false, pingData.slice().retain(),
|
||||||
newPromise());
|
newPromise());
|
||||||
http2Client.encoder().writeData(ctx(), nextStream, data.slice().retain(),
|
http2Client.encoder().writeData(ctx(), streamId, data.slice().retain(), 0,
|
||||||
0, false, newPromise());
|
false, newPromise());
|
||||||
// Write trailers.
|
// Write trailers.
|
||||||
http2Client.encoder().writeHeaders(ctx(), nextStream, headers, 0,
|
http2Client.encoder().writeHeaders(ctx(), streamId, headers, 0, (short) 16,
|
||||||
(short) 16, false, 0, true, newPromise());
|
false, 0, true, newPromise());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Wait for all frames to be received.
|
// Wait for all frames to be received.
|
||||||
assertTrue(trailersLatch.await(30, SECONDS));
|
assertTrue(trailersLatch.await(60, SECONDS));
|
||||||
verify(serverListener, times(numStreams)).onHeadersRead(any(ChannelHandlerContext.class), anyInt(),
|
verify(serverListener, times(numStreams)).onHeadersRead(any(ChannelHandlerContext.class), anyInt(),
|
||||||
eq(headers), eq(0), eq((short) 16), eq(false), eq(0), eq(false));
|
eq(headers), eq(0), eq((short) 16), eq(false), eq(0), eq(false));
|
||||||
verify(serverListener, times(numStreams)).onHeadersRead(any(ChannelHandlerContext.class), anyInt(),
|
verify(serverListener, times(numStreams)).onHeadersRead(any(ChannelHandlerContext.class), anyInt(),
|
||||||
eq(headers), eq(0), eq((short) 16), eq(false), eq(0), eq(true));
|
eq(headers), eq(0), eq((short) 16), eq(false), eq(0), eq(true));
|
||||||
verify(serverListener, times(numStreams)).onPingRead(any(ChannelHandlerContext.class),
|
verify(serverListener, times(numStreams)).onPingRead(any(ChannelHandlerContext.class),
|
||||||
any(ByteBuf.class));
|
any(ByteBuf.class));
|
||||||
verify(serverListener, times(numStreams)).onDataRead(any(ChannelHandlerContext.class),
|
verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class),
|
||||||
anyInt(), any(ByteBuf.class), eq(0), eq(false));
|
anyInt(), any(ByteBuf.class), eq(0), eq(true));
|
||||||
assertEquals(numStreams, receivedPingBuffers.size());
|
for (StringBuilder builder : receivedData) {
|
||||||
assertEquals(numStreams, receivedDataBuffers.size());
|
assertEquals(dataAsHex, builder.toString());
|
||||||
for (String receivedData : receivedDataBuffers) {
|
|
||||||
assertEquals(text, receivedData);
|
|
||||||
}
|
}
|
||||||
for (String receivedPing : receivedPingBuffers) {
|
for (String receivedPing : receivedPings) {
|
||||||
assertEquals(pingMsg, receivedPing);
|
assertEquals(pingMsg, receivedPing);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -419,4 +439,27 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
|
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
|
||||||
.authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString());
|
.authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void mockFlowControl(Http2FrameListener listener) throws Http2Exception {
|
||||||
|
doAnswer(new Answer<Integer>() {
|
||||||
|
@Override
|
||||||
|
public Integer answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
ByteBuf buf = (ByteBuf) invocation.getArguments()[2];
|
||||||
|
int padding = (Integer) invocation.getArguments()[3];
|
||||||
|
int processedBytes = buf.readableBytes() + padding;
|
||||||
|
return processedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}).when(listener).onDataRead(any(ChannelHandlerContext.class), anyInt(),
|
||||||
|
any(ByteBuf.class), anyInt(), anyBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link ByteBuf} of the given length, filled with random bytes.
|
||||||
|
*/
|
||||||
|
private static ByteBuf randomBytes(int length) {
|
||||||
|
final byte[] bytes = new byte[length];
|
||||||
|
new Random().nextBytes(bytes);
|
||||||
|
return Unpooled.wrappedBuffer(bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
|
|
||||||
package io.netty.util.collection;
|
package io.netty.util.collection;
|
||||||
|
|
||||||
import java.lang.reflect.Array;
|
import java.util.AbstractCollection;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
@ -221,16 +222,34 @@ public class IntObjectHashMap<V> implements IntObjectMap<V>, Iterable<IntObjectM
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public V[] values(Class<V> clazz) {
|
public Collection<V> values() {
|
||||||
@SuppressWarnings("unchecked")
|
return new AbstractCollection<V>() {
|
||||||
V[] outValues = (V[]) Array.newInstance(clazz, size());
|
@Override
|
||||||
int targetIx = 0;
|
public Iterator<V> iterator() {
|
||||||
for (int i = 0; i < values.length; ++i) {
|
return new Iterator<V>() {
|
||||||
if (values[i] != null) {
|
final Iterator<Entry<V>> iter = IntObjectHashMap.this.iterator();
|
||||||
outValues[targetIx++] = values[i];
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return iter.hasNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V next() {
|
||||||
|
return iter.next().value();
|
||||||
}
|
}
|
||||||
return outValues;
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.util.collection;
|
package io.netty.util.collection;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a primitive map that uses {@code int}s as keys.
|
* Interface for a primitive map that uses {@code int}s as keys.
|
||||||
*
|
*
|
||||||
@ -112,5 +114,5 @@ public interface IntObjectMap<V> {
|
|||||||
/**
|
/**
|
||||||
* Gets the values contained in this map.
|
* Gets the values contained in this map.
|
||||||
*/
|
*/
|
||||||
V[] values(Class<V> clazz);
|
Collection<V> values();
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ package io.netty.util.collection;
|
|||||||
|
|
||||||
import io.netty.util.internal.EmptyArrays;
|
import io.netty.util.internal.EmptyArrays;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
@ -106,8 +107,8 @@ public final class PrimitiveCollections {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object[] values(Class<Object> clazz) {
|
public Collection<Object> values() {
|
||||||
return EmptyArrays.EMPTY_OBJECTS;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,8 +186,8 @@ public final class PrimitiveCollections {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public V[] values(Class<V> clazz) {
|
public Collection<V> values() {
|
||||||
return map.values(clazz);
|
return map.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,7 @@ import org.junit.Before;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
@ -280,8 +281,8 @@ public class IntObjectHashMapTest {
|
|||||||
map.put(4, new Value("v4"));
|
map.put(4, new Value("v4"));
|
||||||
map.remove(4);
|
map.remove(4);
|
||||||
|
|
||||||
Value[] values = map.values(Value.class);
|
Collection<Value> values = map.values();
|
||||||
assertEquals(3, values.length);
|
assertEquals(3, values.size());
|
||||||
|
|
||||||
Set<Value> expected = new HashSet<Value>();
|
Set<Value> expected = new HashSet<Value>();
|
||||||
expected.add(v1);
|
expected.add(v1);
|
||||||
|
Loading…
Reference in New Issue
Block a user