Skip to content

Commit 49f6863

Browse files
authored
Clarify double-response exception (#124809)
It is confusing to readers to report `Channel is already closed` in reaction to a double-response bug, and this may be interpreted as a networking issue. We're not really closing anything here, and it's a definite logic bug to call `sendResponse()` twice, so this commit clarifies the actual problem in the exception message. Relates ES-10996 Backport of #124706 to `8.x`
1 parent ae4bce0 commit 49f6863

File tree

2 files changed

+51
-39
lines changed

2 files changed

+51
-39
lines changed

server/src/main/java/org/elasticsearch/rest/RestController.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -909,12 +909,15 @@ public void sendResponse(RestResponse response) {
909909
}
910910
}
911911

912+
// exposed for tests
913+
static boolean PERMIT_DOUBLE_RESPONSE = false;
914+
912915
private static final class ResourceHandlingHttpChannel extends DelegatingRestChannel {
913916
private final CircuitBreakerService circuitBreakerService;
914917
private final int contentLength;
915918
private final HttpRouteStatsTracker statsTracker;
916919
private final long startTime;
917-
private final AtomicBoolean closed = new AtomicBoolean();
920+
private final AtomicBoolean responseSent = new AtomicBoolean();
918921

919922
ResourceHandlingHttpChannel(
920923
RestChannel delegate,
@@ -933,7 +936,14 @@ private static final class ResourceHandlingHttpChannel extends DelegatingRestCha
933936
public void sendResponse(RestResponse response) {
934937
boolean success = false;
935938
try {
936-
close();
939+
// protect against double-response bugs
940+
if (responseSent.compareAndSet(false, true) == false) {
941+
final var message = "have already sent a response to this request, cannot send another";
942+
assert PERMIT_DOUBLE_RESPONSE : message;
943+
throw new IllegalStateException(message);
944+
}
945+
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(-contentLength);
946+
937947
statsTracker.addRequestStats(contentLength);
938948
statsTracker.addResponseTime(rawRelativeTimeInMillis() - startTime);
939949
if (response.isChunked() == false) {
@@ -964,14 +974,6 @@ public void sendResponse(RestResponse response) {
964974
private static long rawRelativeTimeInMillis() {
965975
return TimeValue.nsecToMSec(System.nanoTime());
966976
}
967-
968-
private void close() {
969-
// attempt to close once atomically
970-
if (closed.compareAndSet(false, true) == false) {
971-
throw new IllegalStateException("Channel is already closed");
972-
}
973-
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(-contentLength);
974-
}
975977
}
976978

977979
private static class ResponseLengthRecorder extends AtomicReference<HttpRouteStatsTracker> implements Releasable {

server/src/test/java/org/elasticsearch/rest/RestControllerTests.java

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -468,40 +468,50 @@ public void testDispatchRequestAddsAndFreesBytesOnError() {
468468
}
469469

470470
public void testDispatchRequestAddsAndFreesBytesOnlyOnceOnError() {
471-
int contentLength = BREAKER_LIMIT.bytesAsInt();
472-
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
473-
// we will produce an error in the rest handler and one more when sending the error response
474-
RestRequest request = testRestRequest("/error", content, XContentType.JSON);
475-
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, true);
476-
477-
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
478-
479-
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
480-
assertEquals(0, inFlightRequestsBreaker.getUsed());
471+
try {
472+
RestController.PERMIT_DOUBLE_RESPONSE = true;
473+
int contentLength = BREAKER_LIMIT.bytesAsInt();
474+
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
475+
// we will produce an error in the rest handler and one more when sending the error response
476+
RestRequest request = testRestRequest("/error", content, XContentType.JSON);
477+
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, true);
478+
479+
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
480+
481+
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
482+
assertEquals(0, inFlightRequestsBreaker.getUsed());
483+
} finally {
484+
RestController.PERMIT_DOUBLE_RESPONSE = false;
485+
}
481486
}
482487

483488
public void testDispatchRequestAddsAndFreesBytesOnlyOnceOnErrorDuringSend() {
484-
int contentLength = Math.toIntExact(BREAKER_LIMIT.getBytes());
485-
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
486-
// use a real recycler that tracks leaks and create some content bytes in the test handler to check for leaks
487-
final BytesRefRecycler recycler = new BytesRefRecycler(new MockPageCacheRecycler(Settings.EMPTY));
488-
restController.registerHandler(
489-
new Route(GET, "/foo"),
490-
(request, c, client) -> new RestToXContentListener<>(c).onResponse((b, p) -> b.startObject().endObject())
491-
);
492-
// we will produce an error in the rest handler and one more when sending the error response
493-
RestRequest request = testRestRequest("/foo", content, XContentType.JSON);
494-
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, true) {
495-
@Override
496-
protected BytesStream newBytesOutput() {
497-
return new RecyclerBytesStreamOutput(recycler);
498-
}
499-
};
489+
try {
490+
RestController.PERMIT_DOUBLE_RESPONSE = true;
491+
int contentLength = Math.toIntExact(BREAKER_LIMIT.getBytes());
492+
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
493+
// use a real recycler that tracks leaks and create some content bytes in the test handler to check for leaks
494+
final BytesRefRecycler recycler = new BytesRefRecycler(new MockPageCacheRecycler(Settings.EMPTY));
495+
restController.registerHandler(
496+
new Route(GET, "/foo"),
497+
(request, c, client) -> new RestToXContentListener<>(c).onResponse((b, p) -> b.startObject().endObject())
498+
);
499+
// we will produce an error in the rest handler and one more when sending the error response
500+
RestRequest request = testRestRequest("/foo", content, XContentType.JSON);
501+
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, true) {
502+
@Override
503+
protected BytesStream newBytesOutput() {
504+
return new RecyclerBytesStreamOutput(recycler);
505+
}
506+
};
500507

501-
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
508+
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
502509

503-
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
504-
assertEquals(0, inFlightRequestsBreaker.getUsed());
510+
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
511+
assertEquals(0, inFlightRequestsBreaker.getUsed());
512+
} finally {
513+
RestController.PERMIT_DOUBLE_RESPONSE = false;
514+
}
505515
}
506516

507517
public void testDispatchRequestLimitsBytes() {

0 commit comments

Comments
 (0)