Skip to content

Commit c020496

Browse files
authored
Http/2 revamp (helidon-io#9520)
9273 Http/2 revamp * Add h2spec test * Consuming request trailers * Larger frame splitting fix * Flow control update timeout * Streamed payload larger than content length discovery Signed-off-by: Daniel Kec <[email protected]>
1 parent 8399b14 commit c020496

File tree

19 files changed

+1076
-126
lines changed

19 files changed

+1076
-126
lines changed

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

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 2024 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.
@@ -30,7 +30,6 @@
3030
public class Http2ConnectionWriter implements Http2StreamWriter {
3131
private final DataWriter writer;
3232

33-
// todo replace with prioritized lock (stream priority + connection writes have highest prio)
3433
private final Lock streamLock = new ReentrantLock(true);
3534
private final SocketContext ctx;
3635
private final Http2FrameListener listener;
@@ -143,24 +142,14 @@ public int writeHeaders(Http2Headers headers,
143142
Http2Flag.HeaderFlags flags,
144143
Http2FrameData dataFrame,
145144
FlowControl.Outbound flowControl) {
146-
// this is executing in the thread of the stream
147-
// we must enforce parallelism of exactly 1, to make sure the dynamic table is updated
148-
// and then immediately written
149-
150-
lock();
151-
try {
152-
int bytesWritten = 0;
153-
154-
bytesWritten += writeHeaders(headers, streamId, flags, flowControl);
155-
156-
writeData(dataFrame, flowControl);
157-
bytesWritten += Http2FrameHeader.LENGTH;
158-
bytesWritten += dataFrame.header().length();
159-
160-
return bytesWritten;
161-
} finally {
162-
streamLock.unlock();
163-
}
145+
// Executed on stream thread
146+
int bytesWritten = 0;
147+
bytesWritten += writeHeaders(headers, streamId, flags, flowControl);
148+
writeData(dataFrame, flowControl);
149+
bytesWritten += Http2FrameHeader.LENGTH;
150+
bytesWritten += dataFrame.header().length();
151+
152+
return bytesWritten;
164153
}
165154

166155
/**
@@ -227,32 +216,10 @@ private void splitAndWrite(Http2FrameData frame, FlowControl.Outbound flowContro
227216
} else if (splitFrames.length == 2) {
228217
// write send-able part and block until window update with the rest
229218
lockedWrite(splitFrames[0]);
230-
flowControl.decrementWindowSize(currFrame.header().length());
219+
flowControl.decrementWindowSize(splitFrames[0].header().length());
231220
flowControl.blockTillUpdate();
232221
currFrame = splitFrames[1];
233222
}
234223
}
235224
}
236-
237-
// TODO use for fastpath
238-
// private void noLockWrite(Http2FrameData... frames) {
239-
// List<BufferData> toWrite = new LinkedList<>();
240-
//
241-
// for (Http2FrameData frame : frames) {
242-
// BufferData headerData = frame.header().write();
243-
//
244-
// listener.frameHeader(ctx, frame.header());
245-
// listener.frameHeader(ctx, headerData);
246-
//
247-
// toWrite.add(headerData);
248-
//
249-
// BufferData data = frame.data();
250-
//
251-
// if (data.available() != 0) {
252-
// toWrite.add(data);
253-
// }
254-
// }
255-
//
256-
// writer.write(toWrite.toArray(new BufferData[0]));
257-
// }
258225
}

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 2024 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.
@@ -15,12 +15,9 @@
1515
*/
1616
package io.helidon.http.http2;
1717

18-
import java.util.concurrent.CompletableFuture;
19-
import java.util.concurrent.ExecutionException;
18+
import java.util.concurrent.Semaphore;
2019
import java.util.concurrent.TimeUnit;
21-
import java.util.concurrent.TimeoutException;
2220
import java.util.concurrent.atomic.AtomicInteger;
23-
import java.util.concurrent.atomic.AtomicReference;
2421
import java.util.function.BiConsumer;
2522

2623
import static java.lang.System.Logger.Level.DEBUG;
@@ -124,7 +121,9 @@ public long incrementWindowSize(int increment) {
124121
*/
125122
static final class Outbound extends WindowSizeImpl implements WindowSize.Outbound {
126123

127-
private final AtomicReference<CompletableFuture<Void>> updated = new AtomicReference<>(new CompletableFuture<>());
124+
private static final int BACKOFF_MIN = 50;
125+
private static final int BACKOFF_MAX = 5000;
126+
private final Semaphore updatedSemaphore = new Semaphore(1);
128127
private final ConnectionFlowControl.Type type;
129128
private final int streamId;
130129
private final long timeoutMillis;
@@ -146,21 +145,52 @@ public long incrementWindowSize(int increment) {
146145
return remaining;
147146
}
148147

148+
@Override
149+
public void resetWindowSize(int size) {
150+
super.resetWindowSize(size);
151+
triggerUpdate();
152+
}
153+
154+
@Override
155+
public int decrementWindowSize(int decrement) {
156+
int n = super.decrementWindowSize(decrement);
157+
triggerUpdate();
158+
return n;
159+
}
160+
149161
@Override
150162
public void triggerUpdate() {
151-
updated.getAndSet(new CompletableFuture<>()).complete(null);
163+
updatedSemaphore.release();
152164
}
153165

154166
@Override
155167
public void blockTillUpdate() {
168+
var startTime = System.currentTimeMillis();
169+
int backoff = BACKOFF_MIN;
156170
while (getRemainingWindowSize() < 1) {
157171
try {
158-
updated.get().get(timeoutMillis, TimeUnit.MILLISECONDS);
159-
} catch (InterruptedException | ExecutionException | TimeoutException e) {
160-
if (LOGGER_OUTBOUND.isLoggable(DEBUG)) {
161-
LOGGER_OUTBOUND.log(DEBUG,
162-
String.format("%s OFC STR %d: Window depleted, waiting for update.", type, streamId));
163-
}
172+
updatedSemaphore.drainPermits();
173+
var ignored = updatedSemaphore.tryAcquire(backoff, TimeUnit.MILLISECONDS);
174+
// linear deterministic backoff
175+
backoff = Math.min(backoff * 2, BACKOFF_MAX);
176+
} catch (InterruptedException e) {
177+
debugLog("%s OFC STR %d: Window depleted, waiting for update interrupted.", e);
178+
throw new Http2Exception(Http2ErrorCode.FLOW_CONTROL, "Flow control update wait interrupted.");
179+
}
180+
if (System.currentTimeMillis() - startTime > timeoutMillis) {
181+
debugLog("%s OFC STR %d: Window depleted, waiting for update time-out.", null);
182+
throw new Http2Exception(Http2ErrorCode.FLOW_CONTROL, "Flow control update wait time-out.");
183+
}
184+
debugLog("%s OFC STR %d: Window depleted, waiting for update.", null);
185+
}
186+
}
187+
188+
private void debugLog(String message, Exception e) {
189+
if (LOGGER_OUTBOUND.isLoggable(DEBUG)) {
190+
if (e != null) {
191+
LOGGER_OUTBOUND.log(DEBUG, String.format(message, type, streamId), e);
192+
} else {
193+
LOGGER_OUTBOUND.log(DEBUG, String.format(message, type, streamId));
164194
}
165195
}
166196
}

http/tests/media/multipart/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
<groupId>io.helidon.http.media</groupId>
3737
<artifactId>helidon-http-media-multipart</artifactId>
3838
</dependency>
39+
<dependency>
40+
<groupId>io.helidon.logging</groupId>
41+
<artifactId>helidon-logging-jul</artifactId>
42+
</dependency>
3943
<dependency>
4044
<groupId>io.helidon.webserver.testing.junit5</groupId>
4145
<artifactId>helidon-webserver-testing-junit5</artifactId>

http/tests/media/multipart/src/test/resources/logging-test.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2022, 2023 Oracle and/or its affiliates.
2+
# Copyright (c) 2022, 2024 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.
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
handlers=java.util.logging.ConsoleHandler
16+
handlers=io.helidon.logging.jul.HelidonConsoleHandler
1717
java.util.logging.ConsoleHandler.level=FINEST
1818
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
1919
java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %4$s %3$s %5$s%6$s%n
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#
2+
# Copyright (c) 2024 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+
17+
FROM container-registry.oracle.com/os/oraclelinux:9-slim AS build
18+
ENV GO111MODULE=on
19+
ENV GOPROXY=https://proxy.golang.org
20+
ENV CGO_ENABLED=0
21+
ENV VERSION=2.6.1-SNAPSHOT
22+
ENV COMMIT=af83a65f0b6273ef38bf778d400d98892e7653d8
23+
24+
RUN microdnf install go-toolset git -y
25+
26+
WORKDIR /workspace
27+
RUN git clone https://github.com/summerwind/h2spec.git
28+
29+
WORKDIR /workspace/h2spec
30+
RUN git checkout ${COMMIT}
31+
RUN go build -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" ./cmd/h2spec
32+
33+
FROM container-registry.oracle.com/os/oraclelinux:9-slim
34+
ARG PORT=8080
35+
ARG HOST=localhost
36+
ENV PORT=${PORT}
37+
ENV HOST=${HOST}
38+
COPY --from=build /workspace/h2spec/h2spec /usr/local/bin/h2spec
39+
CMD ["/usr/local/bin/h2spec", "-h", "${HOST}", "-p", "${PORT}"]

tests/integration/h2spec/pom.xml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright (c) 2024 Oracle and/or its affiliates.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<project xmlns="http://maven.apache.org/POM/4.0.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<parent>
23+
<groupId>io.helidon.applications</groupId>
24+
<artifactId>helidon-mp</artifactId>
25+
<version>4.2.0-SNAPSHOT</version>
26+
<relativePath>../../../applications/mp/pom.xml</relativePath>
27+
</parent>
28+
<groupId>io.helidon.tests.integration.h2spec</groupId>
29+
<artifactId>helidon-tests-integration-h2spec</artifactId>
30+
<name>Helidon Tests Integration Http/2 h2spec</name>
31+
32+
<properties>
33+
<mainClass>io.helidon.webserver.h2spec.Main</mainClass>
34+
<redirectTestOutputToFile>true</redirectTestOutputToFile>
35+
</properties>
36+
37+
<dependencies>
38+
<dependency>
39+
<groupId>io.helidon.webserver</groupId>
40+
<artifactId>helidon-webserver</artifactId>
41+
</dependency>
42+
<dependency>
43+
<groupId>io.helidon.webserver</groupId>
44+
<artifactId>helidon-webserver-http2</artifactId>
45+
</dependency>
46+
<dependency>
47+
<groupId>io.helidon.config</groupId>
48+
<artifactId>helidon-config-yaml</artifactId>
49+
</dependency>
50+
<dependency>
51+
<groupId>io.helidon.logging</groupId>
52+
<artifactId>helidon-logging-jul</artifactId>
53+
</dependency>
54+
<dependency>
55+
<groupId>org.slf4j</groupId>
56+
<artifactId>slf4j-jdk14</artifactId>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.junit.jupiter</groupId>
60+
<artifactId>junit-jupiter-api</artifactId>
61+
<scope>test</scope>
62+
</dependency>
63+
<dependency>
64+
<groupId>org.junit.jupiter</groupId>
65+
<artifactId>junit-jupiter-params</artifactId>
66+
<scope>test</scope>
67+
</dependency>
68+
<dependency>
69+
<groupId>org.hamcrest</groupId>
70+
<artifactId>hamcrest-all</artifactId>
71+
<scope>test</scope>
72+
</dependency>
73+
<!--suppress VulnerableLibrariesLocal -->
74+
<dependency>
75+
<groupId>org.testcontainers</groupId>
76+
<artifactId>junit-jupiter</artifactId>
77+
<scope>test</scope>
78+
</dependency>
79+
<dependency>
80+
<groupId>io.helidon.webserver.testing.junit5</groupId>
81+
<artifactId>helidon-webserver-testing-junit5</artifactId>
82+
<scope>test</scope>
83+
</dependency>
84+
</dependencies>
85+
86+
<build>
87+
<plugins>
88+
<plugin>
89+
<groupId>org.apache.maven.plugins</groupId>
90+
<artifactId>maven-dependency-plugin</artifactId>
91+
<executions>
92+
<execution>
93+
<id>copy-libs</id>
94+
</execution>
95+
</executions>
96+
</plugin>
97+
<plugin>
98+
<groupId>org.apache.maven.plugins</groupId>
99+
<artifactId>maven-surefire-plugin</artifactId>
100+
<configuration>
101+
<excludes>
102+
<exclude>**/*IT</exclude>
103+
</excludes>
104+
<systemPropertyVariables>
105+
<java.util.logging.config.file>
106+
${project.build.outputDirectory}/logging.properties
107+
</java.util.logging.config.file>
108+
</systemPropertyVariables>
109+
</configuration>
110+
</plugin>
111+
<plugin>
112+
<groupId>org.apache.maven.plugins</groupId>
113+
<artifactId>maven-failsafe-plugin</artifactId>
114+
<configuration>
115+
<systemPropertyVariables>
116+
<java.util.logging.config.file>
117+
${project.build.outputDirectory}/logging.properties
118+
</java.util.logging.config.file>
119+
</systemPropertyVariables>
120+
<redirectTestOutputToFile>${redirectTestOutputToFile}</redirectTestOutputToFile>
121+
</configuration>
122+
<executions>
123+
<execution>
124+
<goals>
125+
<goal>integration-test</goal>
126+
<goal>verify</goal>
127+
</goals>
128+
</execution>
129+
</executions>
130+
</plugin>
131+
</plugins>
132+
</build>
133+
</project>

0 commit comments

Comments
 (0)