Skip to content

Commit

Permalink
Fix memory leak when double invoking RestChannel.sendResponse (#89873) (
Browse files Browse the repository at this point in the history
#89881)

When using the resource handling channel we must make sure that
if we (by what is IMO a bug) try to double invoke it after
having already sent a response (or tried to do so) we at least
release the memory in the channel's outbound buffer.
Otherwise we will leak any memory from it that was used to create
the now failing to send `RestResponse`.
  • Loading branch information
original-brownbear committed Sep 7, 2022
1 parent 0b969c6 commit 3665655
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 8 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/89873.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 89873
summary: Fix memory leak when double invoking `RestChannel.sendResponse`
area: Network
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,8 @@ public final BytesStream bytesOutput() {
return bytesOut;
}

/**
* Releases the current output buffer for this channel. Must be called after the buffer derived from {@link #bytesOutput} is no longer
* needed.
*/
protected final void releaseOutputBuffer() {
@Override
public final void releaseOutputBuffer() {
if (bytesOut != null) {
try {
bytesOut.close();
Expand Down
6 changes: 6 additions & 0 deletions server/src/main/java/org/elasticsearch/rest/RestChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ XContentBuilder newBuilder(@Nullable XContentType xContentType, @Nullable XConte

BytesStream bytesOutput();

/**
* Releases the current output buffer for this channel. Must be called after the buffer derived from {@link #bytesOutput} is no longer
* needed.
*/
void releaseOutputBuffer();

RestRequest request();

/**
Expand Down
17 changes: 15 additions & 2 deletions server/src/main/java/org/elasticsearch/rest/RestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,11 @@ public BytesStream bytesOutput() {
return delegate.bytesOutput();
}

@Override
public void releaseOutputBuffer() {
delegate.releaseOutputBuffer();
}

@Override
public RestRequest request() {
return delegate.request();
Expand All @@ -726,8 +731,16 @@ public boolean detailedErrorsEnabled() {

@Override
public void sendResponse(RestResponse response) {
close();
delegate.sendResponse(response);
boolean success = false;
try {
close();
delegate.sendResponse(response);
success = true;
} finally {
if (success == false) {
releaseOutputBuffer();
}
}
}

private void close() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.io.stream.BytesStream;
import org.elasticsearch.common.io.stream.RecyclerBytesStreamOutput;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.BoundTransportAddress;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.util.MockPageCacheRecycler;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.IOUtils;
import org.elasticsearch.core.RestApiVersion;
Expand All @@ -28,11 +31,13 @@
import org.elasticsearch.http.HttpStats;
import org.elasticsearch.indices.breaker.HierarchyCircuitBreakerService;
import org.elasticsearch.rest.RestHandler.Route;
import org.elasticsearch.rest.action.RestToXContentListener;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.client.NoOpNodeClient;
import org.elasticsearch.test.rest.FakeRestRequest;
import org.elasticsearch.tracing.Tracer;
import org.elasticsearch.transport.BytesRefRecycler;
import org.elasticsearch.usage.UsageService;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.XContentBuilder;
Expand Down Expand Up @@ -389,6 +394,30 @@ public void testDispatchRequestAddsAndFreesBytesOnlyOnceOnError() {
assertEquals(0, inFlightRequestsBreaker.getUsed());
}

public void testDispatchRequestAddsAndFreesBytesOnlyOnceOnErrorDuringSend() {
int contentLength = Math.toIntExact(BREAKER_LIMIT.getBytes());
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
// use a real recycler that tracks leaks and create some content bytes in the test handler to check for leaks
final BytesRefRecycler recycler = new BytesRefRecycler(new MockPageCacheRecycler(Settings.EMPTY));
restController.registerHandler(
new Route(GET, "/foo"),
(request, c, client) -> new RestToXContentListener<>(c).onResponse((b, p) -> b.startObject().endObject())
);
// we will produce an error in the rest handler and one more when sending the error response
RestRequest request = testRestRequest("/foo", content, XContentType.JSON);
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, true) {
@Override
protected BytesStream newBytesOutput() {
return new RecyclerBytesStreamOutput(recycler);
}
};

restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());

assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
assertEquals(0, inFlightRequestsBreaker.getUsed());
}

public void testDispatchRequestLimitsBytes() {
int contentLength = BREAKER_LIMIT.bytesAsInt() + 1;
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
Expand Down Expand Up @@ -988,7 +1017,7 @@ boolean getSendResponseCalled() {

}

private static final class ExceptionThrowingChannel extends AbstractRestChannel {
private static class ExceptionThrowingChannel extends AbstractRestChannel {

protected ExceptionThrowingChannel(RestRequest request, boolean detailedErrorsEnabled) {
super(request, detailedErrorsEnabled);
Expand Down

0 comments on commit 3665655

Please sign in to comment.