fix(cloudfront): release origin connections when viewers disconnect (#5564)
(cherry picked from commit ee229af97569d5876f756f65ce7956726e2ed696)
This commit is contained in:
+4
@@ -32,6 +32,7 @@ import jakarta.ws.rs.core.Response;
|
|||||||
import jakarta.ws.rs.core.StreamingOutput;
|
import jakarta.ws.rs.core.StreamingOutput;
|
||||||
import jakarta.ws.rs.core.UriInfo;
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.resteasy.reactive.server.core.CurrentRequestManager;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -954,6 +955,9 @@ public class CloudFrontServingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (includeBody && origin.stream() != null) {
|
if (includeBody && origin.stream() != null) {
|
||||||
|
// A disconnected viewer can skip StreamingOutput entirely, so its finally block
|
||||||
|
// alone cannot release the origin connection.
|
||||||
|
CurrentRequestManager.get().registerCompletionCallback(ignored -> origin.closeStream());
|
||||||
InputStream body = origin.stream();
|
InputStream body = origin.stream();
|
||||||
StreamingOutput entity = output -> {
|
StreamingOutput entity = output -> {
|
||||||
try (InputStream in = body) {
|
try (InputStream in = body) {
|
||||||
|
|||||||
+61
@@ -39,7 +39,11 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -390,6 +395,45 @@ class CloudFrontCustomOriginServingTest {
|
|||||||
assertArrayEquals(upload, received.get().body());
|
assertArrayEquals(upload, received.get().body());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void releasesOriginConnectionsWhenViewersDisconnectBeforeOriginHeaders() throws Exception {
|
||||||
|
BlockingQueue<CountDownLatch> pendingResponses = new LinkedBlockingQueue<>();
|
||||||
|
originServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
originServer.createContext("/", exchange -> {
|
||||||
|
CountDownLatch sendResponse = new CountDownLatch(1);
|
||||||
|
pendingResponses.add(sendResponse);
|
||||||
|
try {
|
||||||
|
if (!sendResponse.await(10, TimeUnit.SECONDS)) {
|
||||||
|
throw new IOException("Timed out waiting for viewer disconnect");
|
||||||
|
}
|
||||||
|
byte[] body = "origin-ok".getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200, body.length);
|
||||||
|
exchange.getResponseBody().write(body);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Origin response interrupted", e);
|
||||||
|
} finally {
|
||||||
|
exchange.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
originServer.createContext("/after-disconnects", exchange -> {
|
||||||
|
byte[] body = "origin-ok".getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200, body.length);
|
||||||
|
exchange.getResponseBody().write(body);
|
||||||
|
exchange.close();
|
||||||
|
});
|
||||||
|
originServer.start();
|
||||||
|
Distribution created = cloudFrontService.createDistribution(
|
||||||
|
customOriginDistribution(allMethodsBehavior()), Map.of());
|
||||||
|
|
||||||
|
for (int i = 0; i < CloudFrontOriginHttpClient.MAX_CONNECTIONS + 10; i++) {
|
||||||
|
disconnectViewerBeforeOriginHeaders(created.getDomainName(), i, pendingResponses);
|
||||||
|
}
|
||||||
|
|
||||||
|
given().header("Host", created.getDomainName()).when().get("/after-disconnects")
|
||||||
|
.then().statusCode(200).body(equalTo("origin-ok"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void releasesTheOriginConnectionOfEveryResponseItDoesNotSend() throws Exception {
|
void releasesTheOriginConnectionOfEveryResponseItDoesNotSend() throws Exception {
|
||||||
// The origin client keeps a fixed number of connections. An error response replaced by a custom
|
// The origin client keeps a fixed number of connections. An error response replaced by a custom
|
||||||
@@ -789,6 +833,23 @@ class CloudFrontCustomOriginServingTest {
|
|||||||
assertEquals("127.0.0.1:" + originServer.getAddress().getPort(), request.header("Host"));
|
assertEquals("127.0.0.1:" + originServer.getAddress().getPort(), request.header("Host"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void disconnectViewerBeforeOriginHeaders(String host, int requestIndex,
|
||||||
|
BlockingQueue<CountDownLatch> pendingResponses)
|
||||||
|
throws Exception {
|
||||||
|
CountDownLatch sendResponse = null;
|
||||||
|
try (Socket viewer = new Socket("127.0.0.1", RestAssured.port)) {
|
||||||
|
viewer.setSoLinger(true, 0);
|
||||||
|
viewer.getOutputStream().write(("GET /disconnect/" + requestIndex + " HTTP/1.1\r\nHost: "
|
||||||
|
+ host + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII));
|
||||||
|
sendResponse = pendingResponses.poll(5, TimeUnit.SECONDS);
|
||||||
|
assertNotNull(sendResponse, "No origin connection for viewer " + requestIndex);
|
||||||
|
} finally {
|
||||||
|
if (sendResponse != null) {
|
||||||
|
sendResponse.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void startRecordingOrigin(AtomicReference<ReceivedRequest> received, AtomicInteger hits)
|
private void startRecordingOrigin(AtomicReference<ReceivedRequest> received, AtomicInteger hits)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
originServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
originServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user