java-topology/test/jdk/java/net/httpclient/PlainConnectionLockTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

379 lines
16 KiB
Java

/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsServer;
import jdk.httpclient.test.lib.common.HttpServerAdapters;
import jdk.httpclient.test.lib.common.TestServerConfigurator;
import jdk.test.lib.net.SimpleSSLContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.Version.HTTP_1_1;
/*
* @test id=default
* @bug 8372198
* @requires os.family != "windows"
* @summary Attempt to check that no deadlock occurs when
* connections are closed by the ConnectionPool.
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @run junit/othervm
* -Djdk.httpclient.HttpClient.log=errors
* -Djdk.httpclient.connectionPoolSize=1
* ${test.main.class}
*/
/*
* @test id=windows
* @bug 8372198
* @requires os.family == "windows"
* @summary Attempt to check that no deadlock occurs when
* connections are closed by the ConnectionPool.
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.common.HttpServerAdapters
* @comment A special jtreg id for windows allows for experimentation
* with different configuration - for instance, specifying
* -Djdk.internal.httpclient.tcp.selector.useVirtualThreads=<always|never>
* on the run command line, or specifying a different request count
* with -DrequestCount=<integer>.
* On windows, it seems important to set the backlog for the HTTP/1.1
* server to at least the number of concurrent request. This is done
* in the beforeTest() method.
* If the test fails waiting for avalaible permits, due to system limitations,
* even with the backlog correctly configure, adding a margin to the backlog
* or reducing the requestCount could be envisaged.
* @run junit/othervm
* -Djdk.httpclient.HttpClient.log=errors
* -Djdk.httpclient.connectionPoolSize=1
* ${test.main.class}
*/
// -Djava.security.debug=all
class PlainConnectionLockTest implements HttpServerAdapters {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private HttpTestServer http1Server;
private HttpTestServer https1Server;
private String http1URI;
private String https1URI;
private ExecutorService serverExecutor;
private Semaphore responseSemaphore;
private Semaphore requestSemaphore;
private boolean successfulCompletion;
private static final int MANY = Integer.getInteger("requestCount", 100);
static {
HttpServerAdapters.enableServerLogging();
}
private boolean blockResponse(Semaphore request, Semaphore response) {
try {
request.release();
response.acquire();
return true;
} catch (InterruptedException x) {
Thread.currentThread().interrupt(); // Restore the interrupt
return false;
}
}
@BeforeEach
synchronized void beforeTest() throws Exception {
requestSemaphore = new Semaphore(0);
responseSemaphore = new Semaphore(0);
successfulCompletion = false;
serverExecutor = Executors.newThreadPerTaskExecutor(
Thread.ofVirtual().name("Http1Server", 0).factory());
// On Windows, sending 100 concurrent requests may
// fail if the server's connection backlog is less than 100.
// The default backlog is 50. Just make sure the backlog is
// big enough.
int backlog = Math.max(MANY, 50);
// create a https server for HTTP/1.1
var loopback = InetAddress.getLoopbackAddress();
var wrappedHttps1Server = HttpsServer.create(new InetSocketAddress(loopback, 0), backlog);
wrappedHttps1Server.setHttpsConfigurator(new TestServerConfigurator(loopback, sslContext));
https1Server = HttpTestServer.of(wrappedHttps1Server, serverExecutor);
https1Server.addHandler((exchange) -> {
if (blockResponse(requestSemaphore, responseSemaphore)) {
exchange.sendResponseHeaders(200, 0);
} else {
exchange.sendResponseHeaders(500, 0);
}
}, "/PlainConnectionLockTest/");
https1Server.start();
System.out.println("HTTPS Server started at " + https1Server.getAddress());
https1URI = "https://" + https1Server.serverAuthority() + "/PlainConnectionLockTest/https1";
// create a plain http server for HTTP/1.1
var wrappedHttp1Server = HttpServer.create(new InetSocketAddress(loopback, 0), backlog);
http1Server = HttpTestServer.of(wrappedHttp1Server, serverExecutor);
http1Server.addHandler((exchange) -> {
if (blockResponse(requestSemaphore, responseSemaphore)) {
exchange.sendResponseHeaders(200, 0);
} else {
exchange.sendResponseHeaders(500, 0);
}
}, "/PlainConnectionLockTest/");
http1Server.start();
System.out.println("HTTP Server started at " + http1Server.getAddress());
http1URI = "http://" + http1Server.serverAuthority() + "/PlainConnectionLockTest/http1";
}
@AfterEach
synchronized void afterTest() throws Exception {
if (http1Server != null) {
System.out.println("Stopping HTTP server " + http1Server.getAddress());
http1Server.stop();
}
if (https1Server != null) {
System.out.println("Stopping HTTPS server " + https1Server.getAddress());
https1Server.stop();
}
if (serverExecutor != null) {
System.out.println("Closing server executor");
if (successfulCompletion) {
serverExecutor.close();
} else {
// server handlers may be wedged.
serverExecutor.shutdownNow();
}
}
requestSemaphore = null;
responseSemaphore = null;
serverExecutor = null;
http1Server = null;
https1Server = null;
http1URI = null;
https1URI = null;
System.out.println("done\n");
}
@Test
void sendManyHttpRequestsNoShutdown() throws Exception {
try {
sendManyRequests(http1URI, MANY, false);
} catch (Throwable t) {
t.printStackTrace(System.out);
throw t;
}
}
@Test
void sendManyHttpRequestsShutdownNow() throws Exception {
try {
sendManyRequests(http1URI, MANY, true);
} catch (Throwable t) {
t.printStackTrace(System.out);
throw t;
}
}
@Test
void sendManyHttpsRequestsNoShutdown() throws Exception {
try {
sendManyRequests(https1URI, MANY, false);
} catch (Throwable t) {
t.printStackTrace(System.out);
throw t;
}
}
@Test
void sendManyHttpsRequestsShutdownNow() throws Exception {
try {
sendManyRequests(https1URI, MANY, true);
} catch (Throwable t) {
t.printStackTrace(System.out);
throw t;
}
}
private static void throwCause(CompletionException x) throws Exception {
var cause = x.getCause();
if (cause instanceof Exception ex) throw ex;
if (cause instanceof Error err) throw err;
throw x;
}
static final long start = System.nanoTime();
public static String now() {
long now = System.nanoTime() - start;
long secs = now / 1000_000_000;
long mill = (now % 1000_000_000) / 1000_000;
long nan = now % 1000_000;
return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
}
static Throwable getCause(Throwable exception) {
if (exception instanceof IOException) return exception;
if (exception instanceof CancellationException) return exception;
if (exception instanceof CompletionException) return getCause(exception.getCause());
if (exception instanceof ExecutionException) return getCause(exception.getCause());
return exception;
}
private synchronized void sendManyRequests(final String requestURI, final int many, boolean shutdown) throws Exception {
System.out.println("\n%sSending %s requests to %s, shutdown=%s\n".formatted(now(), many, requestURI, shutdown));
System.err.println("\n%sSending %s requests to %s, shutdown=%s\n".formatted(now(), many, requestURI, shutdown));
assert many > 0;
try (final HttpClient client = HttpClient.newBuilder()
.proxy(NO_PROXY)
.sslContext(sslContext).build()) {
List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder().version(HTTP_1_1);
for (int i = 0; i < many; i++) {
// GET
final URI reqURI = new URI(requestURI + "?i=" + i);
final HttpRequest req = reqBuilder.copy().uri(reqURI).GET().build();
System.out.println(now() + "Issuing request: " + req);
var cf = client.sendAsync(req, BodyHandlers.ofString());
futures.add(cf);
}
System.out.printf("\n%sWaiting for %s requests to be handled on the server%n", now(), many);
int count = 0;
// wait for all exchanges to be handled
while (!requestSemaphore.tryAcquire(many, 5, TimeUnit.SECONDS)) {
count++;
System.out.printf("%sFailed to obtain %s permits after %ss - only %s available%n",
now(), many, (count * 5), requestSemaphore.availablePermits());
for (var cf : futures) {
if (cf.isDone() || cf.isCancelled()) {
System.out.printf("%sFound some completed cf: %s%n", now(), cf);
if (cf.isCancelled()) {
System.out.printf("%scf is cancelled: %s%n", now(), cf);
client.shutdownNow(); // make sure HttpCient::close won't block waiting for server
var error = new AssertionError(now() + "A request cf was cancelled" + cf);
System.out.printf("%s throwing: %s%n", now(), error);
throw error;
}
if (cf.isCompletedExceptionally()) {
System.out.printf("%scf is completed exceptionally: %s%n", now(), cf);
var exception = getCause(cf.exceptionNow());
System.out.printf("%sexception is: %s%n", now(), exception);
client.shutdownNow(); // make sure HttpCient::close won't block waiting for server
exception.printStackTrace(System.out);
var error = new AssertionError(now() + "A request failed prematurely", exception);
System.out.printf("%s throwing: %s%n", now(), error);
throw error;
}
System.out.printf("%scf is completed prematurely: %s%n", now(), cf);
client.shutdownNow(); // make sure HttpCient::close won't block waiting for server
var error = new AssertionError(now() + "A request succeeded prematurely: " + cf.join());
System.out.printf("%s throwing: %s%n", now(), error);
throw error;
}
}
System.out.printf("%sCouldn't acquire %s permits, only %s available - keep on waiting%n",
now(), many, requestSemaphore.availablePermits());
}
System.out.println(now() + "All requests reached the server: releasing one response");
// allow one request to proceed
responseSemaphore.release();
try {
// wait for the first response.
System.out.println(now() + "Waiting for the first response");
CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println(now() + "Got first response: " + futures.stream().filter(CompletableFuture::isDone)
.findFirst().map(CompletableFuture::join));
if (shutdown) {
System.out.println(now() + "Calling HttpClient::shutdownNow");
client.shutdownNow();
client.awaitTermination(Duration.ofSeconds(1));
}
} finally {
System.out.printf("%s Releasing %s remaining responses%n", now(), many - 1);
// now release the others.
responseSemaphore.release(many - 1);
}
// wait for all responses
System.out.printf("%sWaiting for all %s responses to complete%n", now(), many);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).exceptionally(t -> null).join();
// check
System.out.printf("%sAll %s responses completed. Checking...%n%n", now(), many);
Set<String> conns = new HashSet<>();
int exceptionCount = 0;
int success = 0;
for (var respCF : futures) {
try {
var resp = respCF.join();
Assertions.assertEquals(200, resp.statusCode(),
now() + "unexpected response code for GET request: " + resp);
Assertions.assertTrue(conns.add(resp.connectionLabel().get()),
now() + "unexepected reuse of connection: "
+ resp.connectionLabel().get() + " found in " + conns);
success++;
} catch (CompletionException x) {
if (shutdown) exceptionCount++;
else throwCause(x);
}
}
if (shutdown) {
if (success == 0) {
throw new AssertionError(("%s%s: shutdownNow=%s: Expected at least one response, " +
"got success=%s, exceptions=%s").formatted(now(), requestURI, shutdown, success, exceptionCount));
}
}
System.out.println("%sSuccess: %s: shutdownNow:%s, success=%s, exceptions:%s\n"
.formatted(now(), requestURI, shutdown, success, exceptionCount));
successfulCompletion = true;
}
}
}