Skip to content

Commit ab5cbd2

Browse files
authored
Fix dynamic table cpu burn #10472 (#10806)
* Fix dynamic table cpu burn #10472 Signed-off-by: Daniel Kec <[email protected]> * H2Spec logging over jul Signed-off-by: Daniel Kec <[email protected]> * Validate dyn table add after eviction is done Signed-off-by: Daniel Kec <[email protected]> --------- Signed-off-by: Daniel Kec <[email protected]>
1 parent 7d73ffc commit ab5cbd2

File tree

5 files changed

+79
-12
lines changed

5 files changed

+79
-12
lines changed

http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ void write(BufferData buffer) {
11241124

11251125
/**
11261126
* There is one dynamic table for inbound headers and one for outbound headers for each connection.
1127-
* This is to minimize size of headers on the transport.
1127+
* This is to minimize the size of headers on the transport.
11281128
* The table caches header names and values and then uses indexes only when transferring headers over network.
11291129
*/
11301130
public static class DynamicTable {
@@ -1175,8 +1175,8 @@ void maxTableSize(long number) {
11751175
}
11761176
this.maxTableSize = number;
11771177
if (maxTableSize == 0) {
1178-
currentTableSize = 0;
11791178
headers.clear();
1179+
currentTableSize = 0;
11801180
}
11811181
while (maxTableSize < currentTableSize) {
11821182
evict();
@@ -1188,18 +1188,21 @@ int add(HeaderName headerName, String headerValue) {
11881188
int size = name.length() + headerValue.getBytes(StandardCharsets.US_ASCII).length + 32;
11891189

11901190
if (currentTableSize + size <= maxTableSize) {
1191+
// fast path
11911192
return add(headerName, headerValue, size);
11921193
}
11931194

11941195
while ((currentTableSize + size) > maxTableSize) {
11951196
evict();
1196-
if (currentTableSize <= 0) {
1197-
throw new Http2Exception(Http2ErrorCode.COMPRESSION,
1198-
"Cannot add header record, max table size too low. "
1199-
+ "current size: " + currentTableSize + ", max size: " + maxTableSize + ","
1200-
+ " header size: " + size);
1201-
}
12021197
}
1198+
1199+
if (maxTableSize - currentTableSize < size) {
1200+
throw new Http2Exception(Http2ErrorCode.COMPRESSION,
1201+
"Cannot add header record, max table size too low. "
1202+
+ "current size: " + currentTableSize + ", max size: " + maxTableSize + ","
1203+
+ " header size: " + size);
1204+
}
1205+
12031206
return add(headerName, headerValue, size);
12041207
}
12051208

@@ -1244,6 +1247,7 @@ private IndexedHeaderRecord find(HeaderName headerName, String headerValue) {
12441247

12451248
private void evict() {
12461249
if (headers.isEmpty()) {
1250+
currentTableSize = 0;
12471251
return;
12481252
}
12491253
DynamicHeader removed = headers.remove(headers.size() - 1);

http/http2/src/test/java/io/helidon/http/http2/DynamicTableTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,46 @@ void testIssue10472() {
6767
table.maxTableSize(0L);
6868
}
6969

70+
@Test
71+
void testIssue10472addOnlyOneFit() {
72+
Http2Settings settings = Http2Settings.builder()
73+
.add(Http2Setting.HEADER_TABLE_SIZE, 45L)
74+
.build();
75+
76+
Http2Headers.DynamicTable table = Http2Headers.DynamicTable.create(settings);
77+
table.add(HeaderNames.create("a"), "aa"); // 35
78+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 1, "a", "aa");
79+
table.add(HeaderNames.create("b"), "b"); // 34
80+
81+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 1, "b", "b");
82+
83+
table.add(HeaderNames.create("c"), "c"); // 34
84+
table.add(HeaderNames.create("dddd"), "ddddd"); // 41
85+
86+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 1, "dddd", "ddddd");
87+
}
88+
89+
@Test
90+
void testIssue10472addOnlyTwoFit() {
91+
Http2Settings settings = Http2Settings.builder()
92+
.add(Http2Setting.HEADER_TABLE_SIZE, 75L)
93+
.build();
94+
95+
Http2Headers.DynamicTable table = Http2Headers.DynamicTable.create(settings);
96+
table.add(HeaderNames.create("a"), "aa"); // 35
97+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 1, "a", "aa");
98+
table.add(HeaderNames.create("b"), "b"); // 34
99+
100+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 1, "b", "b");
101+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 2, "a", "aa");
102+
103+
table.add(HeaderNames.create("c"), "c"); // 34
104+
table.add(HeaderNames.create("dddd"), "ddddd"); // 41
105+
106+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 1, "dddd", "ddddd");
107+
testRecord(table, Http2Headers.StaticHeader.MAX_INDEX + 2, "c", "c");
108+
}
109+
70110
private void testRecord(Http2Headers.DynamicTable table,
71111
int index,
72112
String expectedName,

tests/integration/h2spec/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
</excludes>
104104
<systemPropertyVariables>
105105
<java.util.logging.config.file>
106-
${project.build.outputDirectory}/logging.properties
106+
${project.build.testOutputDirectory}/logging-test.properties
107107
</java.util.logging.config.file>
108108
</systemPropertyVariables>
109109
</configuration>
@@ -114,7 +114,7 @@
114114
<configuration>
115115
<systemPropertyVariables>
116116
<java.util.logging.config.file>
117-
${project.build.outputDirectory}/logging.properties
117+
${project.build.testOutputDirectory}/logging-test.properties
118118
</java.util.logging.config.file>
119119
</systemPropertyVariables>
120120
<redirectTestOutputToFile>${redirectTestOutputToFile}</redirectTestOutputToFile>

tests/integration/h2spec/src/test/java/H2SpecIT.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2024, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222

2323
import javax.xml.parsers.DocumentBuilderFactory;
2424

25+
import io.helidon.logging.common.LogConfig;
2526
import io.helidon.webserver.WebServer;
2627
import io.helidon.webserver.http.HttpRouting;
2728
import io.helidon.webserver.http2.Http2Config;
@@ -35,6 +36,7 @@
3536
import org.slf4j.Logger;
3637
import org.slf4j.LoggerFactory;
3738
import org.testcontainers.containers.GenericContainer;
39+
import org.testcontainers.containers.output.Slf4jLogConsumer;
3840
import org.testcontainers.containers.wait.strategy.Wait;
3941
import org.testcontainers.images.PullPolicy;
4042
import org.testcontainers.images.builder.ImageFromDockerfile;
@@ -48,6 +50,10 @@
4850
@Testcontainers(disabledWithoutDocker = true)
4951
class H2SpecIT {
5052

53+
static {
54+
LogConfig.configureRuntime();
55+
}
56+
5157
private static final Logger LOGGER = LoggerFactory.getLogger(H2SpecIT.class);
5258

5359
@ParameterizedTest(name = "{0}: {1}")
@@ -88,8 +94,8 @@ private static Stream<Arguments> runH2Spec() {
8894
try (var cont = new GenericContainer<>(
8995
new ImageFromDockerfile().withDockerfile(Path.of("./Dockerfile")))
9096
.withAccessToHost(true)
97+
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("h2spec")))
9198
.withImagePullPolicy(PullPolicy.ageBased(Duration.ofDays(365)))
92-
.withLogConsumer(outputFrame -> LOGGER.info(outputFrame.getUtf8StringWithoutLineEnding()))
9399
.waitingFor(Wait.forLogMessage(".*Finished in.*", 1))) {
94100

95101
org.testcontainers.Testcontainers.exposeHostPorts(port);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#
2+
# Copyright (c) 2025 Oracle and/or its affiliates.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
handlers = java.util.logging.ConsoleHandler
17+
java.util.logging.ConsoleHandler.level = INFO

0 commit comments

Comments
 (0)