This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new 90344a236a Review "Fix flow control over-allocation when releasing the
HTTP/2..."
90344a236a is described below
commit 90344a236a34f3729912eb7a9ada0dfd21798304
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Nov 2 16:14:46 2026 +0000
Review "Fix flow control over-allocation when releasing the HTTP/2..."
Fix allocation leak
Co-authored by: Claude / Sonnet 5.5
---
.../apache/coyote/http2/Http2UpgradeHandler.java | 24 ++++
test/org/apache/coyote/http2/TestRfc9218.java | 134 ++++++++++++++-------
webapps/docs/changelog.xml | 4 +-
3 files changed, 117 insertions(+), 45 deletions(-)
diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
index 7c1ca9a4aa..89dd0e87bf 100644
--- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java
+++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java
@@ -1408,6 +1408,30 @@ class Http2UpgradeHandler extends AbstractStream
implements InternalHttpUpgradeH
}
}
}
+
+ /*
+ * Rounding down the proportional shares may leave part of the
increment unallocated. The allocations
+ * on this path are the only record of the connection window
the client made available so allocate
+ * what is left, in priority order, to the streams that are
still waiting. The streams in the back log
+ * are still waiting for at least the remaining increment (the
back log is larger than the increment)
+ * so all of it can be allocated. Otherwise the client could
be waiting for data the server will not
+ * send until it receives a further window update the client
has no reason to send.
+ */
+ if (remaining > 0) {
+ for (Stream s : orderedStreams) {
+ if (s.getConnectionAllocationRequested() < 1) {
+ continue;
+ }
+ remaining = allocate(s, remaining);
+ result.add(s);
+ if (s.getConnectionAllocationRequested() == 0) {
+ backLogStreams.remove(s);
+ }
+ if (remaining < 1) {
+ break;
+ }
+ }
+ }
}
return result;
} finally {
diff --git a/test/org/apache/coyote/http2/TestRfc9218.java
b/test/org/apache/coyote/http2/TestRfc9218.java
index 9e434dbd81..cd23a5df2e 100644
--- a/test/org/apache/coyote/http2/TestRfc9218.java
+++ b/test/org/apache/coyote/http2/TestRfc9218.java
@@ -25,52 +25,43 @@ import org.junit.Test;
public class TestRfc9218 extends Http2TestBase {
@Test
- public void testPriority() throws Exception {
- http2Connect();
+ public void testBackLogRoundingRemainderAllocated() throws Exception {
+ blockStreams();
+
+ // Make all three streams incremental with the same urgency so the
increment is shared proportionally
+ sendPriorityUpdate(17, 3, true);
+ sendPriorityUpdate(19, 3, true);
+ sendPriorityUpdate(21, 3, true);
/*
- * This test uses small window updates and data frames that will
trigger the excessive overhead protection so
- * disable it.
+ * Add 10 bytes to the connection window. The proportional shares are
3.04, 3.47 and 3.47 bytes. Rounding each
+ * down allocates 9 bytes. The remaining byte is part of the window
the client made available so it must be
+ * allocated rather than lost. It goes to 17 since that is first in
priority order.
*/
- http2Protocol.setOverheadWindowUpdateThreshold(0);
- http2Protocol.setOverheadDataThreshold(0);
-
- // Default connection window size is 64k - 1. Initial request will
have used 8k (56k -1). Increase it to 57k.
- sendWindowUpdate(0, 1 + 1024);
-
- // Consume 56k of the connection window
- for (int i = 3; i < 17; i += 2) {
- sendSimpleGetRequest(i);
- readSimpleGetResponse();
- }
+ sendWindowUpdate(0, 10);
+ parser.readFrame();
+ parser.readFrame();
+ parser.readFrame();
String trace = output.getTrace();
System.out.println(trace);
+ Assert.assertTrue(trace, trace.contains("17-Body-4\n"));
+ trace = trace.replace("17-Body-4\n", "");
+ Assert.assertTrue(trace, trace.contains("19-Body-3\n"));
+ trace = trace.replace("19-Body-3\n", "");
+ Assert.assertTrue(trace, trace.contains("21-Body-3\n"));
+ trace = trace.replace("21-Body-3\n", "");
+ Assert.assertEquals(0, trace.length());
output.clearTrace();
+ }
- // At this point the connection window should be 1k
-
- // Process a request on stream 17. This should consume the connection
window.
- sendSimpleGetRequest(17);
- // 17-headers, 17-1k-body
- parser.readFrame();
- parser.readFrame();
- trace = output.getTrace();
- System.out.println(trace);
- output.clearTrace();
- // Send additional requests. Connection window is empty so only
headers will be returned.
- sendSimpleGetRequest(19);
- sendSimpleGetRequest(21);
+ @Test
+ public void testPriority() throws Exception {
+ blockStreams();
- // 19-headers, 21-headers
- parser.readFrame();
- parser.readFrame();
- trace = output.getTrace();
- System.out.println(trace);
- output.clearTrace();
+ String trace;
- // At this point 17, 19 and 21 are all blocked because the connection
window is zero.
// 17 - 7k body left
// 19 - 8k body left
// 21 - 8k body left
@@ -117,8 +108,8 @@ public class TestRfc9218 extends Http2TestBase {
trace = output.getTrace();
System.out.println(trace);
- Assert.assertTrue(trace.contains("17-Body-877\n"));
- trace = trace.replace("17-Body-877\n", "");
+ Assert.assertTrue(trace.contains("17-Body-878\n"));
+ trace = trace.replace("17-Body-878\n", "");
Assert.assertTrue(trace.contains("19-Body-1170\n"));
trace = trace.replace("19-Body-1170\n", "");
Assert.assertTrue(trace.contains("21-Body-1024\n"));
@@ -126,7 +117,7 @@ public class TestRfc9218 extends Http2TestBase {
Assert.assertEquals(0, trace.length());
output.clearTrace();
- // 17 - 5267 body left
+ // 17 - 5266 body left
// 19 - 7022 body left
// 21 - 6144 body left
@@ -141,7 +132,7 @@ public class TestRfc9218 extends Http2TestBase {
Assert.assertEquals("17-Body-1\n", trace);
output.clearTrace();
- // 17 - 5266 body left
+ // 17 - 5265 body left
// 19 - 7022 body left
// 21 - 6144 body left
@@ -169,17 +160,17 @@ public class TestRfc9218 extends Http2TestBase {
trace = output.getTrace();
System.out.println(trace);
- Assert.assertTrue(trace.contains("17-Body-5266\n"));
- trace = trace.replace("17-Body-5266\n", "");
+ Assert.assertTrue(trace.contains("17-Body-5265\n"));
+ trace = trace.replace("17-Body-5265\n", "");
Assert.assertTrue(trace.contains("17-EndOfStream\n"));
trace = trace.replace("17-EndOfStream\n", "");
- Assert.assertTrue(trace.contains("19-Body-1560\n"));
- trace = trace.replace("19-Body-1560\n", "");
+ Assert.assertTrue(trace.contains("19-Body-1562\n"));
+ trace = trace.replace("19-Body-1562\n", "");
Assert.assertTrue(trace.contains("21-Body-1365\n"));
trace = trace.replace("21-Body-1365\n", "");
Assert.assertEquals(0, trace.length());
- // 19 - 5462 body left
+ // 19 - 5460 body left
// 21 - 4779 body left
// Add 16k to the connection window. Should fully allocate 19 and 21.
@@ -195,6 +186,61 @@ public class TestRfc9218 extends Http2TestBase {
}
+ /*
+ * Opens the connection and leaves streams 17, 19 and 21 blocked because
the connection window is zero.
+ *
+ * 17 - 7k body left
+ * 19 - 8k body left
+ * 21 - 8k body left
+ */
+ private void blockStreams() throws Exception {
+ http2Connect();
+
+ /*
+ * This test uses small window updates and data frames that will
trigger the excessive overhead protection so
+ * disable it.
+ */
+ http2Protocol.setOverheadWindowUpdateThreshold(0);
+ http2Protocol.setOverheadDataThreshold(0);
+
+ // Default connection window size is 64k - 1. Initial request will
have used 8k (56k -1). Increase it to 57k.
+ sendWindowUpdate(0, 1 + 1024);
+
+ // Consume 56k of the connection window
+ for (int i = 3; i < 17; i += 2) {
+ sendSimpleGetRequest(i);
+ readSimpleGetResponse();
+ }
+
+ String trace = output.getTrace();
+ System.out.println(trace);
+ output.clearTrace();
+
+ // At this point the connection window should be 1k
+
+ // Process a request on stream 17. This should consume the connection
window.
+ sendSimpleGetRequest(17);
+ // 17-headers, 17-1k-body
+ parser.readFrame();
+ parser.readFrame();
+ trace = output.getTrace();
+ System.out.println(trace);
+ output.clearTrace();
+
+ // Send additional requests. Connection window is empty so only
headers will be returned.
+ sendSimpleGetRequest(19);
+ sendSimpleGetRequest(21);
+
+ // 19-headers, 21-headers
+ parser.readFrame();
+ parser.readFrame();
+ trace = output.getTrace();
+ System.out.println(trace);
+ output.clearTrace();
+
+ // At this point 17, 19 and 21 are all blocked because the connection
window is zero.
+ }
+
private void sendInvalidPriorityUpdate(int streamId) throws IOException {
byte[] payload = "u=1:i".getBytes(StandardCharsets.US_ASCII);
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 21dfafba35..7f10041dd8 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -366,7 +366,9 @@
compliant client must treat as a connection error of type
<code>FLOW_CONTROL_ERROR</code>. Streams that are left without an
allocation remain in the back log and are considered again when the
- next window increment arrives. (remm)
+ next window increment arrives. Any part of the increment left
+ unallocated by the rounding of the proportional shares is allocated to
+ the waiting streams in priority order rather than being lost. (remm)
</fix>
<fix>
Permit the <code>WINDOW_UPDATE</code> and <code>RST_STREAM</code>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]