255 lines
13 KiB
Diff
255 lines
13 KiB
Diff
|
From 1352fa5543638d421042f1d69c8fc2c6f4603968 Mon Sep 17 00:00:00 2001
|
||
|
From: Scott Mitchell <scott_mitchell@apple.com>
|
||
|
Date: Thu, 11 Jan 2024 11:24:07 -0800
|
||
|
Subject: [PATCH] h2: propagate stream close without read pending, avoid SOOE
|
||
|
if autoRead == false
|
||
|
|
||
|
Motivation:
|
||
|
AbstractHttp2StreamChannel requires that there is a channel.read()
|
||
|
pending in order to notify of channel inactive events. For use cases
|
||
|
that don't use auto read the application could have read the frame
|
||
|
corresponding to end of stream but not get the channel inactive.
|
||
|
If read is called from within channelReadComplete this creates
|
||
|
a reentrant loop into AbstractHttp2StreamChannel#doBeginRead that
|
||
|
may cause a StackOverflowException.
|
||
|
|
||
|
Modifications:
|
||
|
- propagate stream/channel closed event if possible (e.g. no pending
|
||
|
data) without invoking read().
|
||
|
- adjust readPending state to prevent StackOverflowException and
|
||
|
let the read loop continue.
|
||
|
---
|
||
|
.../http2/AbstractHttp2StreamChannel.java | 84 +++++++++++--------
|
||
|
.../codec/http2/Http2MultiplexTest.java | 73 +++++++++++++++-
|
||
|
2 files changed, 121 insertions(+), 36 deletions(-)
|
||
|
|
||
|
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java
|
||
|
index 49882acf1ad7..c768576d2aea 100644
|
||
|
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java
|
||
|
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java
|
||
|
@@ -605,7 +605,7 @@ void fireChildRead(Http2Frame frame) {
|
||
|
if (allocHandle.continueReading()) {
|
||
|
maybeAddChannelToReadCompletePendingQueue();
|
||
|
} else {
|
||
|
- unsafe.notifyReadComplete(allocHandle, true);
|
||
|
+ unsafe.notifyReadComplete(allocHandle, true, false);
|
||
|
}
|
||
|
} else {
|
||
|
if (inboundBuffer == null) {
|
||
|
@@ -618,7 +618,7 @@ void fireChildRead(Http2Frame frame) {
|
||
|
void fireChildReadComplete() {
|
||
|
assert eventLoop().inEventLoop();
|
||
|
assert readStatus != ReadStatus.IDLE || !readCompletePending;
|
||
|
- unsafe.notifyReadComplete(unsafe.recvBufAllocHandle(), false);
|
||
|
+ unsafe.notifyReadComplete(unsafe.recvBufAllocHandle(), false, false);
|
||
|
}
|
||
|
|
||
|
final void closeWithError(Http2Error error) {
|
||
|
@@ -851,35 +851,47 @@ private Object pollQueuedMessage() {
|
||
|
}
|
||
|
|
||
|
void doBeginRead() {
|
||
|
- // Process messages until there are none left (or the user stopped requesting) and also handle EOS.
|
||
|
- while (readStatus != ReadStatus.IDLE) {
|
||
|
- Object message = pollQueuedMessage();
|
||
|
- if (message == null) {
|
||
|
- if (readEOS) {
|
||
|
- unsafe.closeForcibly();
|
||
|
- }
|
||
|
- // We need to double check that there is nothing left to flush such as a
|
||
|
- // window update frame.
|
||
|
+ if (readStatus == ReadStatus.IDLE) {
|
||
|
+ // Don't wait for the user to request a read to notify of channel closure.
|
||
|
+ if (readEOS && (inboundBuffer == null || inboundBuffer.isEmpty())) {
|
||
|
+ // Double check there is nothing left to flush such as a window update frame.
|
||
|
flush();
|
||
|
- break;
|
||
|
- }
|
||
|
- final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
|
||
|
- allocHandle.reset(config());
|
||
|
- boolean continueReading = false;
|
||
|
- do {
|
||
|
- doRead0((Http2Frame) message, allocHandle);
|
||
|
- } while ((readEOS || (continueReading = allocHandle.continueReading()))
|
||
|
- && (message = pollQueuedMessage()) != null);
|
||
|
-
|
||
|
- if (continueReading && isParentReadInProgress() && !readEOS) {
|
||
|
- // Currently the parent and child channel are on the same EventLoop thread. If the parent is
|
||
|
- // currently reading it is possible that more frames will be delivered to this child channel. In
|
||
|
- // the case that this child channel still wants to read we delay the channelReadComplete on this
|
||
|
- // child channel until the parent is done reading.
|
||
|
- maybeAddChannelToReadCompletePendingQueue();
|
||
|
- } else {
|
||
|
- notifyReadComplete(allocHandle, true);
|
||
|
+ unsafe.closeForcibly();
|
||
|
}
|
||
|
+ } else {
|
||
|
+ do { // Process messages until there are none left (or the user stopped requesting) and also handle EOS.
|
||
|
+ Object message = pollQueuedMessage();
|
||
|
+ if (message == null) {
|
||
|
+ // Double check there is nothing left to flush such as a window update frame.
|
||
|
+ flush();
|
||
|
+ if (readEOS) {
|
||
|
+ unsafe.closeForcibly();
|
||
|
+ }
|
||
|
+ break;
|
||
|
+ }
|
||
|
+ final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
|
||
|
+ allocHandle.reset(config());
|
||
|
+ boolean continueReading = false;
|
||
|
+ do {
|
||
|
+ doRead0((Http2Frame) message, allocHandle);
|
||
|
+ } while ((readEOS || (continueReading = allocHandle.continueReading()))
|
||
|
+ && (message = pollQueuedMessage()) != null);
|
||
|
+
|
||
|
+ if (continueReading && isParentReadInProgress() && !readEOS) {
|
||
|
+ // Currently the parent and child channel are on the same EventLoop thread. If the parent is
|
||
|
+ // currently reading it is possible that more frames will be delivered to this child channel. In
|
||
|
+ // the case that this child channel still wants to read we delay the channelReadComplete on this
|
||
|
+ // child channel until the parent is done reading.
|
||
|
+ maybeAddChannelToReadCompletePendingQueue();
|
||
|
+ } else {
|
||
|
+ notifyReadComplete(allocHandle, true, true);
|
||
|
+
|
||
|
+ // While in the read loop reset the readState AFTER calling readComplete (or other pipeline
|
||
|
+ // callbacks) to prevents re-entry into this method (if autoRead is disabled and the user calls
|
||
|
+ // read on each readComplete) and StackOverflowException.
|
||
|
+ resetReadStatus();
|
||
|
+ }
|
||
|
+ } while (readStatus != ReadStatus.IDLE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@@ -908,17 +920,21 @@ private void updateLocalWindowIfNeeded() {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- void notifyReadComplete(RecvByteBufAllocator.Handle allocHandle, boolean forceReadComplete) {
|
||
|
+ private void resetReadStatus() {
|
||
|
+ readStatus = readStatus == ReadStatus.REQUESTED ? ReadStatus.IN_PROGRESS : ReadStatus.IDLE;
|
||
|
+ }
|
||
|
+
|
||
|
+ void notifyReadComplete(RecvByteBufAllocator.Handle allocHandle, boolean forceReadComplete,
|
||
|
+ boolean inReadLoop) {
|
||
|
if (!readCompletePending && !forceReadComplete) {
|
||
|
return;
|
||
|
}
|
||
|
// Set to false just in case we added the channel multiple times before.
|
||
|
readCompletePending = false;
|
||
|
|
||
|
- if (readStatus == ReadStatus.REQUESTED) {
|
||
|
- readStatus = ReadStatus.IN_PROGRESS;
|
||
|
- } else {
|
||
|
- readStatus = ReadStatus.IDLE;
|
||
|
+ if (!inReadLoop) {
|
||
|
+ // While in the read loop we reset the state after calling pipeline methods to prevent StackOverflow.
|
||
|
+ resetReadStatus();
|
||
|
}
|
||
|
|
||
|
allocHandle.readComplete();
|
||
|
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java
|
||
|
index f277dbb12ef1..b1f28b14b94b 100644
|
||
|
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java
|
||
|
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java
|
||
|
@@ -58,6 +58,7 @@
|
||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||
|
import java.util.concurrent.atomic.AtomicReference;
|
||
|
|
||
|
+import static io.netty.handler.codec.http2.Http2Error.NO_ERROR;
|
||
|
import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise;
|
||
|
import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings;
|
||
|
import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease;
|
||
|
@@ -477,6 +478,74 @@ public void channelReadShouldRespectAutoRead() {
|
||
|
verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 2);
|
||
|
}
|
||
|
|
||
|
+ @Test
|
||
|
+ public void noAutoReadWithReentrantReadDoesNotSOOE() {
|
||
|
+ final AtomicBoolean shouldRead = new AtomicBoolean();
|
||
|
+ Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
|
||
|
+ @Override
|
||
|
+ public void accept(ChannelHandlerContext obj) {
|
||
|
+ if (shouldRead.get()) {
|
||
|
+ obj.read();
|
||
|
+ }
|
||
|
+ }
|
||
|
+ };
|
||
|
+ LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
|
||
|
+ AtomicInteger maxReads = new AtomicInteger(1);
|
||
|
+ Http2StreamChannel childChannel = newInboundStream(3, false, maxReads, inboundHandler);
|
||
|
+ assertTrue(childChannel.config().isAutoRead());
|
||
|
+ Http2HeadersFrame headersFrame = inboundHandler.readInbound();
|
||
|
+ assertNotNull(headersFrame);
|
||
|
+
|
||
|
+ childChannel.config().setAutoRead(false);
|
||
|
+
|
||
|
+ final int maxWrites = 10000; // enough writes to generated SOOE.
|
||
|
+ for (int i = 0; i < maxWrites; ++i) {
|
||
|
+ frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(String.valueOf(i)), 0, false);
|
||
|
+ }
|
||
|
+ frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(String.valueOf(maxWrites)), 0, true);
|
||
|
+ shouldRead.set(true);
|
||
|
+ childChannel.read();
|
||
|
+
|
||
|
+ for (int i = 0; i < maxWrites; ++i) {
|
||
|
+ Http2DataFrame dataFrame0 = inboundHandler.readInbound();
|
||
|
+ assertNotNull(dataFrame0);
|
||
|
+ release(dataFrame0);
|
||
|
+ }
|
||
|
+ Http2DataFrame dataFrame0 = inboundHandler.readInbound();
|
||
|
+ assertTrue(dataFrame0.isEndStream());
|
||
|
+ release(dataFrame0);
|
||
|
+
|
||
|
+ verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 0);
|
||
|
+ }
|
||
|
+
|
||
|
+ @Test
|
||
|
+ public void readNotRequiredToEndStream() {
|
||
|
+ LastInboundHandler inboundHandler = new LastInboundHandler();
|
||
|
+ AtomicInteger maxReads = new AtomicInteger(1);
|
||
|
+ Http2StreamChannel childChannel = newInboundStream(3, false, maxReads, inboundHandler);
|
||
|
+ assertTrue(childChannel.config().isAutoRead());
|
||
|
+
|
||
|
+ childChannel.config().setAutoRead(false);
|
||
|
+
|
||
|
+ Http2HeadersFrame headersFrame = inboundHandler.readInbound();
|
||
|
+ assertNotNull(headersFrame);
|
||
|
+
|
||
|
+ assertNull(inboundHandler.readInbound());
|
||
|
+
|
||
|
+ frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), NO_ERROR.code());
|
||
|
+
|
||
|
+ assertFalse(inboundHandler.isChannelActive());
|
||
|
+ childChannel.closeFuture().syncUninterruptibly();
|
||
|
+
|
||
|
+ Http2ResetFrame resetFrame = useUserEventForResetFrame() ? inboundHandler.<Http2ResetFrame>readUserEvent() :
|
||
|
+ inboundHandler.<Http2ResetFrame>readInbound();
|
||
|
+
|
||
|
+ assertEquals(childChannel.stream(), resetFrame.stream());
|
||
|
+ assertEquals(NO_ERROR.code(), resetFrame.errorCode());
|
||
|
+
|
||
|
+ verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 0);
|
||
|
+ }
|
||
|
+
|
||
|
@Test
|
||
|
public void channelReadShouldRespectAutoReadAndNotProduceNPE() throws Exception {
|
||
|
LastInboundHandler inboundHandler = new LastInboundHandler();
|
||
|
@@ -1095,7 +1164,7 @@ public void channelReadComplete(ChannelHandlerContext ctx) {
|
||
|
childChannel.config().setAutoRead(true);
|
||
|
numReads.set(1);
|
||
|
|
||
|
- frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), Http2Error.NO_ERROR.code());
|
||
|
+ frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), NO_ERROR.code());
|
||
|
|
||
|
// Detecting EOS should flush all pending data regardless of read calls.
|
||
|
assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
|
||
|
@@ -1111,7 +1180,7 @@ public void channelReadComplete(ChannelHandlerContext ctx) {
|
||
|
inboundHandler.<Http2ResetFrame>readInbound();
|
||
|
|
||
|
assertEquals(childChannel.stream(), resetFrame.stream());
|
||
|
- assertEquals(Http2Error.NO_ERROR.code(), resetFrame.errorCode());
|
||
|
+ assertEquals(NO_ERROR.code(), resetFrame.errorCode());
|
||
|
|
||
|
assertNull(inboundHandler.readInbound());
|
||
|
|