Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix memory leak when handing empty content request in Http2 #11

Merged
merged 1 commit into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,10 @@ void setTrailers(HttpHeaders trailers) {
}

void appendPartialContent(ByteBuf partialContent) {
if (partialContent.isReadable()) {
if (content == null) {
content = ctx.alloc().compositeBuffer(MAX_COMPOSITE_BUFFER_COMPONENTS);
}
content.addComponent(true, partialContent);
if (content == null) {
content = ctx.alloc().compositeBuffer(MAX_COMPOSITE_BUFFER_COMPONENTS);
}
content.addComponent(true, partialContent);
}

void release() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ public boolean isEnded() {
public abstract BaseResponse<? extends BaseRequestHandle> response();

void handleContent(ByteBuf buf) {
// ignore unreadable data
// 1. it is no need to pass empty content to multipart decoder and aggregator
// 2.keep the onData() behavior of Http1 and Htt2 consistent, because empty content in
// LastHttpContent#EMPTY_LAST_CONTENT will be ignored in Http1Handler but will be passed in Http2Handler
if (!buf.isReadable()) {
return;
}

if (multipart != null) {
multipart.onData(buf.duplicate());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public int onDataRead(ChannelHandlerContext ctx,
final int readableBytes = data.readableBytes();

// The req.bytes is logically divided to positive one, negative one and zero
// 1. The positive one presenting the chunk size left, which mean we can only read num of req.bytes data
// 1. The positive one presenting the chunk size left, which means we can only read num of req.bytes data
// limited by content length, and the oversize bytes will be discarded.
// 2. The negative one presenting the num of bytes that we have read, which would be limited by the
// ServerOptions#maxContentLength, and the exceeded bytes will be discarded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,19 @@ void testSetAndCallOnDataHandler() {
final Req req = plainReq();
final List<ByteBuf> bufs = new LinkedList<>();
assertSame(req, req.onData(bufs::add));
final ByteBuf buf0 = Unpooled.copiedBuffer(new byte[0]);
final ByteBuf buf1 = Unpooled.copiedBuffer(new byte[]{1, 2});
final ByteBuf buf2 = Unpooled.copiedBuffer(new byte[]{3, 4});
final ByteBuf buf3 = Unpooled.copiedBuffer(new byte[]{5, 6});
req.handleContent(buf0);
req.handleContent(buf1);
req.handleContent(buf2);
req.handleContent(buf3);
assertEquals(3, bufs.size());
assertSame(buf1, bufs.get(0));
assertSame(buf2, bufs.get(1));
assertSame(buf3, bufs.get(2));
assertEquals(1, buf0.refCnt());
assertEquals(1, buf1.refCnt());
assertEquals(1, buf2.refCnt());
assertEquals(1, buf3.refCnt());
Expand Down Expand Up @@ -241,6 +244,8 @@ void testMultipartExpect() throws IOException {
assertTrue(req.multipart().attributes().isEmpty());
assertTrue(req.multipart().uploadFiles().isEmpty());

final String body0 = "";

final String body1 =
"--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"tmp-0.txt\"\r\n" +
Expand All @@ -254,8 +259,13 @@ void testMultipartExpect() throws IOException {
"bar\r\n" +
"--" + boundary + "--\r\n";

req.handleContent(Unpooled.copiedBuffer(body1, StandardCharsets.UTF_8));
req.handleContent(Unpooled.copiedBuffer(body2, StandardCharsets.UTF_8));
final ByteBuf buf0 = Unpooled.copiedBuffer(body0, StandardCharsets.UTF_8);
final ByteBuf buf1 = Unpooled.copiedBuffer(body1, StandardCharsets.UTF_8);
final ByteBuf buf2 = Unpooled.copiedBuffer(body2, StandardCharsets.UTF_8);

req.handleContent(buf0);
req.handleContent(buf1);
req.handleContent(buf2);

final CompletableFuture<Void> cf = new CompletableFuture<>();
req.onEnd(p -> {
Expand All @@ -275,6 +285,9 @@ void testMultipartExpect() throws IOException {

cf.complete(null);
assertNull(upload.getByteBuf());
assertEquals(1, buf0.refCnt());
assertEquals(1, buf1.refCnt());
assertEquals(1, buf2.refCnt());
}

@Test
Expand All @@ -289,17 +302,26 @@ void testMultipartExpectCleared() {
assertSame(req, req.multipart(true));
assertNotSame(MultipartHandle.EMPTY, req.multipart());

final String body0 = "";
final String body1 =
"--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"tmp-0.txt\"\r\n" +
"Content-Type: image/gif\r\n" +
"\r\n" +
"foo" + "\r\n" +
"--" + boundary;
req.handleContent(Unpooled.copiedBuffer(body1, StandardCharsets.UTF_8));
final ByteBuf buf0 = Unpooled.copiedBuffer(body0, StandardCharsets.UTF_8);
final ByteBuf buf1 = Unpooled.copiedBuffer(body1, StandardCharsets.UTF_8);
req.handleContent(buf0);
req.handleContent(buf1);

assertEquals(1, buf0.refCnt());
assertEquals(1, buf1.refCnt());

req.multipart(false);
assertSame(MultipartHandle.EMPTY, req.multipart());
assertEquals(1, buf0.refCnt());
assertEquals(1, buf1.refCnt());
}

@Test
Expand Down Expand Up @@ -333,21 +355,28 @@ void testAggregated() {
return p;
});

final ByteBuf buf0 = Unpooled.copiedBuffer("".getBytes(StandardCharsets.UTF_8));
final ByteBuf buf1 = Unpooled.copiedBuffer("123".getBytes(StandardCharsets.UTF_8));
final ByteBuf buf2 = Unpooled.copiedBuffer("456".getBytes(StandardCharsets.UTF_8));
req.handleContent(buf0);
req.handleContent(buf1);
req.handleContent(buf2);

final HttpHeaders trailers = new Http1HeadersImpl();
req.handleTrailer(trailers);
req.handleEnd();

assertEquals("123456", req.aggregated().body().toString(StandardCharsets.UTF_8));
assertEquals(1, req.aggregated().body().refCnt());

// empty content will be ignored
assertEquals(1, buf0.refCnt());
// retained
assertEquals(2, buf1.refCnt());
assertEquals(2, buf2.refCnt());
assertSame(trailers, req.aggregated().trailers());
end.complete(null);
assertEquals(1, buf0.refCnt());
assertEquals(1, buf1.refCnt());
assertEquals(1, buf2.refCnt());
assertSame(AggregationHandle.EMPTY, req.aggregated());
Expand Down