Skip to content

Commit

Permalink
Add unit tests for section 5.2
Browse files Browse the repository at this point in the history
Fix a bug where the end of stream flag was incorrectly set when flow control was applied to the response body after it had been fully written by the app but before it had been fully written to the client.

git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@1684910 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
markt-asf committed Jun 11, 2015
1 parent 53e722f commit d80e9ad
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 12 deletions.
4 changes: 2 additions & 2 deletions java/org/apache/coyote/http2/Http2UpgradeHandler.java
Expand Up @@ -453,7 +453,7 @@ private HpackEncoder getHpackEncoder() {
}


void writeBody(Stream stream, ByteBuffer data, int len) throws IOException {
void writeBody(Stream stream, ByteBuffer data, int len, boolean finished) throws IOException {
if (log.isDebugEnabled()) {
log.debug(sm.getString("upgradeHandler.writeBody", connectionId, stream.getIdentifier(),
Integer.toString(data.remaining())));
Expand All @@ -462,7 +462,7 @@ void writeBody(Stream stream, ByteBuffer data, int len) throws IOException {
byte[] header = new byte[9];
ByteUtil.setThreeBytes(header, 0, len);
header[3] = FrameType.DATA.getIdByte();
if (stream.getOutputBuffer().isFinished()) {
if (finished) {
header[4] = FLAG_END_OF_STREAM;
stream.sentEndOfStream();
if (!stream.isActive()) {
Expand Down
23 changes: 14 additions & 9 deletions java/org/apache/coyote/http2/Stream.java
Expand Up @@ -275,11 +275,11 @@ class StreamOutputBuffer implements OutputBuffer {

private final ByteBuffer buffer = ByteBuffer.allocate(8 * 1024);
private volatile long written = 0;
private volatile boolean finished = false;
private volatile boolean closed = false;

@Override
public int doWrite(ByteChunk chunk) throws IOException {
if (finished) {
if (closed) {
// TODO i18n
throw new IllegalStateException();
}
Expand All @@ -293,14 +293,18 @@ public int doWrite(ByteChunk chunk) throws IOException {
if (len > 0 && !buffer.hasRemaining()) {
// Only flush if we have more data to write and the buffer
// is full
flush();
flush(true);
}
}
written += offset;
return offset;
}

public void flush() throws IOException {
flush(false);
}

private void flush(boolean writeInProgress) throws IOException {
if (!coyoteResponse.isCommitted()) {
coyoteResponse.sendHeaders();
}
Expand Down Expand Up @@ -348,7 +352,8 @@ public void flush() throws IOException {
decrementWindowSize(thisWrite);

// Do the write
handler.writeBody(Stream.this, buffer, thisWrite);
handler.writeBody(Stream.this, buffer, thisWrite,
!writeInProgress && closed && left == thisWrite);
left -= thisWrite;
buffer.position(buffer.position() + thisWrite);
}
Expand All @@ -360,20 +365,20 @@ public long getBytesWritten() {
return written;
}

public void finished() {
finished = true;
public void close() {
closed = true;
}

public boolean isFinished() {
return finished;
public boolean isClosed() {
return closed;
}

/**
* @return <code>true</code> if it is certain that the associated
* response has no body.
*/
public boolean hasNoBody() {
return ((written == 0) && finished);
return ((written == 0) && closed);
}
}

Expand Down
2 changes: 1 addition & 1 deletion java/org/apache/coyote/http2/StreamProcessor.java
Expand Up @@ -76,7 +76,7 @@ public void action(ActionCode actionCode, Object param) {
}
case CLOSE: {
// Tell the output buffer there will be no more data
stream.getOutputBuffer().finished();
stream.getOutputBuffer().close();
// Then flush it
action(ActionCode.CLIENT_FLUSH, null);
break;
Expand Down
18 changes: 18 additions & 0 deletions test/org/apache/coyote/http2/Http2TestBase.java
Expand Up @@ -456,6 +456,24 @@ void sendPriority(int streamId, int streamDependencyId, int weight) throws IOExc
}


void sendSetting(int settingId, long value) throws IOException {
byte[] settingFrame = new byte[15];
// length
ByteUtil.setThreeBytes(settingFrame, 0, 6);
// type
settingFrame[3] = FrameType.SETTINGS.getIdByte();
// No flags
// Stream 0

// Payload
ByteUtil.setTwoBytes(settingFrame, 9, settingId);
ByteUtil.setFourBytes(settingFrame, 11, value);

os.write(settingFrame);
os.flush();
}


private static class TestInput implements Http2Parser.Input {

private final InputStream is;
Expand Down
116 changes: 116 additions & 0 deletions test/org/apache/coyote/http2/TestHttp2Section_5_2.java
@@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
* Unit tests for Section 5.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_2 extends Http2TestBase {

/*
* Get the connection to a point where 1k of 8k response body has been
* read and the flow control for the stream has no capacity left.
*/
@Override
@Before
public void setUp() throws Exception {
super.setUp();

http2Connect();

// Set the default window size to 1024 bytes
sendSetting(4, 1024);
// Wait for the ack
parser.readFrame(true);
output.clearTrace();

// Headers + 8k response
sendSimpleRequest(3);

// Headers
parser.readFrame(true);
// First 1k of body
parser.readFrame(true);
output.clearTrace();
}


@Test
public void testFlowControlLimits01() throws Exception {
readBytes(20);
clearRemainder();
}


@Test
public void testFlowControlLimits02() throws Exception {
readBytes(1);
readBytes(1);
readBytes(1024);
readBytes(1);
clearRemainder();
}


@Test
public void testFlowControlLimits03() throws Exception {
readBytes(8192,7168);
}


@Test
public void testFlowControlLimits04() throws Exception {
readBytes(7168, 7168, true);
}


private void readBytes(int len) throws Exception {
readBytes(len, len);
}


private void readBytes(int len, int expected) throws Exception {
readBytes(len, expected, len > expected);
}


private void readBytes(int len, int expected, boolean eos) throws Exception {
sendWindowUpdate(3, len);
parser.readFrame(true);
String expectedTrace = "3-Body-" + expected + "\n";
if (eos) {
expectedTrace += "3-EndOfStream\n";
}
Assert.assertEquals(expectedTrace, output.getTrace());
output.clearTrace();
}


private void clearRemainder() throws Exception {
// Remainder
sendWindowUpdate(3, 8192);
parser.readFrame(true);
}
}

0 comments on commit d80e9ad

Please sign in to comment.