Skip to content

Commit a26bb64

Browse files
DaveCTurnerjfreden
authored andcommitted
Clarify double-response exception (#124706)
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
1 parent 1428829 commit a26bb64

File tree

2 files changed

+54
-39
lines changed

2 files changed

+54
-39
lines changed

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.elasticsearch.core.RestApiVersion;
3535
import org.elasticsearch.core.Streams;
3636
import org.elasticsearch.core.TimeValue;
37+
import org.elasticsearch.core.UpdateForV10;
3738
import org.elasticsearch.http.HttpHeadersValidationException;
3839
import org.elasticsearch.http.HttpRouteStats;
3940
import org.elasticsearch.http.HttpRouteStatsTracker;
@@ -874,12 +875,17 @@ public void sendResponse(RestResponse response) {
874875
}
875876
}
876877

878+
// exposed for tests; marked as UpdateForV10 because this assertion should have flushed out all double-close bugs by the time v10 is
879+
// released so we should be able to drop the tests that check we behave reasonably in production on this impossible path
880+
@UpdateForV10(owner = UpdateForV10.Owner.DISTRIBUTED_COORDINATION)
881+
static boolean PERMIT_DOUBLE_RESPONSE = false;
882+
877883
private static final class ResourceHandlingHttpChannel extends DelegatingRestChannel {
878884
private final CircuitBreakerService circuitBreakerService;
879885
private final int contentLength;
880886
private final HttpRouteStatsTracker statsTracker;
881887
private final long startTime;
882-
private final AtomicBoolean closed = new AtomicBoolean();
888+
private final AtomicBoolean responseSent = new AtomicBoolean();
883889

884890
ResourceHandlingHttpChannel(
885891
RestChannel delegate,
@@ -898,7 +904,14 @@ private static final class ResourceHandlingHttpChannel extends DelegatingRestCha
898904
public void sendResponse(RestResponse response) {
899905
boolean success = false;
900906
try {
901-
close();
907+
// protect against double-response bugs
908+
if (responseSent.compareAndSet(false, true) == false) {
909+
final var message = "have already sent a response to this request, cannot send another";
910+
assert PERMIT_DOUBLE_RESPONSE : message;
911+
throw new IllegalStateException(message);
912+
}
913+
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(-contentLength);
914+
902915
statsTracker.addRequestStats(contentLength);
903916
statsTracker.addResponseTime(rawRelativeTimeInMillis() - startTime);
904917
if (response.isChunked() == false) {
@@ -929,14 +942,6 @@ public void sendResponse(RestResponse response) {
929942
private static long rawRelativeTimeInMillis() {
930943
return TimeValue.nsecToMSec(System.nanoTime());
931944
}
932-
933-
private void close() {
934-
// attempt to close once atomically
935-
if (closed.compareAndSet(false, true) == false) {
936-
throw new IllegalStateException("Channel is already closed");
937-
}
938-
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(-contentLength);
939-
}
940945
}
941946

942947
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
@@ -498,40 +498,50 @@ public void testDispatchRequestAddsAndFreesBytesOnError() {
498498
}
499499

500500
public void testDispatchRequestAddsAndFreesBytesOnlyOnceOnError() {
501-
int contentLength = BREAKER_LIMIT.bytesAsInt();
502-
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
503-
// we will produce an error in the rest handler and one more when sending the error response
504-
RestRequest request = testRestRequest("/error", content, XContentType.JSON);
505-
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, randomBoolean());
506-
507-
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
508-
509-
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
510-
assertEquals(0, inFlightRequestsBreaker.getUsed());
501+
try {
502+
RestController.PERMIT_DOUBLE_RESPONSE = true;
503+
int contentLength = BREAKER_LIMIT.bytesAsInt();
504+
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
505+
// we will produce an error in the rest handler and one more when sending the error response
506+
RestRequest request = testRestRequest("/error", content, XContentType.JSON);
507+
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, randomBoolean());
508+
509+
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
510+
511+
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
512+
assertEquals(0, inFlightRequestsBreaker.getUsed());
513+
} finally {
514+
RestController.PERMIT_DOUBLE_RESPONSE = false;
515+
}
511516
}
512517

513518
public void testDispatchRequestAddsAndFreesBytesOnlyOnceOnErrorDuringSend() {
514-
int contentLength = Math.toIntExact(BREAKER_LIMIT.getBytes());
515-
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
516-
// use a real recycler that tracks leaks and create some content bytes in the test handler to check for leaks
517-
final BytesRefRecycler recycler = new BytesRefRecycler(new MockPageCacheRecycler(Settings.EMPTY));
518-
restController.registerHandler(
519-
new Route(GET, "/foo"),
520-
(request, c, client) -> new RestToXContentListener<>(c).onResponse((b, p) -> b.startObject().endObject())
521-
);
522-
// we will produce an error in the rest handler and one more when sending the error response
523-
RestRequest request = testRestRequest("/foo", content, XContentType.JSON);
524-
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, randomBoolean()) {
525-
@Override
526-
protected BytesStream newBytesOutput() {
527-
return new RecyclerBytesStreamOutput(recycler);
528-
}
529-
};
519+
try {
520+
RestController.PERMIT_DOUBLE_RESPONSE = true;
521+
int contentLength = Math.toIntExact(BREAKER_LIMIT.getBytes());
522+
String content = randomAlphaOfLength((int) Math.round(contentLength / inFlightRequestsBreaker.getOverhead()));
523+
// use a real recycler that tracks leaks and create some content bytes in the test handler to check for leaks
524+
final BytesRefRecycler recycler = new BytesRefRecycler(new MockPageCacheRecycler(Settings.EMPTY));
525+
restController.registerHandler(
526+
new Route(GET, "/foo"),
527+
(request, c, client) -> new RestToXContentListener<>(c).onResponse((b, p) -> b.startObject().endObject())
528+
);
529+
// we will produce an error in the rest handler and one more when sending the error response
530+
RestRequest request = testRestRequest("/foo", content, XContentType.JSON);
531+
ExceptionThrowingChannel channel = new ExceptionThrowingChannel(request, randomBoolean()) {
532+
@Override
533+
protected BytesStream newBytesOutput() {
534+
return new RecyclerBytesStreamOutput(recycler);
535+
}
536+
};
530537

531-
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
538+
restController.dispatchRequest(request, channel, client.threadPool().getThreadContext());
532539

533-
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
534-
assertEquals(0, inFlightRequestsBreaker.getUsed());
540+
assertEquals(0, inFlightRequestsBreaker.getTrippedCount());
541+
assertEquals(0, inFlightRequestsBreaker.getUsed());
542+
} finally {
543+
RestController.PERMIT_DOUBLE_RESPONSE = false;
544+
}
535545
}
536546

537547
public void testDispatchRequestLimitsBytes() {

0 commit comments

Comments
 (0)