/* * Copyright (c) 2023, 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.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.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.internal.net.http.common.Utils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_2; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; /* * @test * @bug 8312433 * @summary verify that the HttpClient's HTTP2 idle connection management doesn't close a connection * when that connection has been handed out from the pool to a caller * @library /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters * * @run junit/othervm -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.keepalive.timeout.h2=3 * IdlePooledConnectionTest */ public class IdlePooledConnectionTest { private static final String ALL_OK_PATH = "/allOK"; private static HttpTestServer h2Server; private static URI allOKUri; private static final String H2_KEEPALIVE_TIMEOUT_PROP = "jdk.httpclient.keepalive.timeout.h2"; private static final String KEEPALIVE_TIMEOUT_PROP = "jdk.httpclient.keepalive.timeout"; @BeforeAll static void beforeAll() throws Exception { h2Server = HttpTestServer.create(HTTP_2); h2Server.addHandler(new AllOKHandler(), ALL_OK_PATH); h2Server.start(); System.err.println("Started H2 server at " + h2Server.serverAuthority()); allOKUri = new URI("http://" + h2Server.serverAuthority() + ALL_OK_PATH); } @AfterAll static void afterAll() throws Exception { if (h2Server != null) { System.err.println("Stopping h2 server: " + h2Server.serverAuthority()); h2Server.stop(); } } // just returns a 200 HTTP response for all requests private static final class AllOKHandler implements HttpServerAdapters.HttpTestHandler { @Override public void handle(final HttpTestExchange exchange) throws IOException { System.err.println("Responding with 200 response code for request " + exchange.getRequestURI()); exchange.sendResponseHeaders(200, 0); } } /* * Issues a HTTP2 request against a server and expects it to succeed. * The connection that was used is internally pooled by the HttpClient implementation. * Then waits for the H2 idle connection timeout, before again firing several concurrent HTTP2 * requests against the same server. It is expected that all these requests complete * successfully without running into a race condition where the H2 idle connection management * closes the (pooled) connection during the time connection has been handed out to a caller * and a new stream hasn't yet been created. */ @Test public void testPooledConnection() throws Exception { final Duration h2TimeoutDuration = getEffectiveH2IdleTimeoutDuration(); assertNotNull(h2TimeoutDuration, "H2 idle connection timeout cannot be null"); // the wait time, which represents the time to wait before firing off additional requests, // is intentionally a few milliseconds smaller than the h2 idle connection timeout, // to allow for the requests to reach the place where connection checkout from the pool // happens and thus allow the code to race with the idle connection timer task // closing the connection. final long waitTimeMillis = TimeUnit.of(ChronoUnit.MILLIS).convert(h2TimeoutDuration) - 5; try (final HttpClient client = HttpClient.newBuilder().proxy(NO_PROXY).build()) { final HttpRequest request = HttpRequest.newBuilder(allOKUri) .GET().version(HTTP_2).build(); // keep ready the additional concurrent requests that we will fire later. // we do this now so that when it's time to fire off these additional requests, // this main thread does as little work as possible to increase the chances of a // race condition in idle connection management closing a pooled connection // and new requests being fired final Callable> task = () -> client.send(request, BodyHandlers.discarding()); final List>> tasks = new ArrayList<>(); final int numAdditionalReqs = 20; for (int i = 0; i < numAdditionalReqs; i++) { tasks.add(task); } // issue the first request System.err.println("issuing first request: " + request); final HttpResponse firstResp = client.send(request, BodyHandlers.discarding()); assertEquals(200, firstResp.statusCode(), "unexpected response code for request " + request); System.err.println("waiting for " + waitTimeMillis + " milli seconds" + " before issuing additional requests"); Thread.sleep(waitTimeMillis); // issue additional concurrent requests final List>> responses; try (final ExecutorService executor = Executors.newFixedThreadPool(numAdditionalReqs)) { responses = executor.invokeAll(tasks); } System.err.println("All " + responses.size() + " requests completed, now" + " verifying each response"); // verify all requests succeeded for (final Future> future : responses) { final HttpResponse rsp = future.get(); assertEquals(200, rsp.statusCode(), "unexpected response code for request " + request); } } } // returns the effective idle timeout duration of a HTTP2 connection private static Duration getEffectiveH2IdleTimeoutDuration() { final long keepAliveTimeoutInSecs = getNetProp(KEEPALIVE_TIMEOUT_PROP, 30); final long h2TimeoutInSecs = getNetProp(H2_KEEPALIVE_TIMEOUT_PROP, keepAliveTimeoutInSecs); return Duration.of(h2TimeoutInSecs, ChronoUnit.SECONDS); } private static long getNetProp(final String prop, final long def) { final String s = Utils.getNetProperty(prop); if (s == null) { return def; } try { final long timeoutVal = Long.parseLong(s); return timeoutVal >= 0 ? timeoutVal : def; } catch (NumberFormatException ignored) { return def; } } }