| /* | |
| * Copyright (c) 2015, 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. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * 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. | |
| */ | |
| package jdk.internal.net.http; | |
| import javax.net.ssl.SSLContext; | |
| import javax.net.ssl.SSLException; | |
| import javax.net.ssl.SSLHandshakeException; | |
| import javax.net.ssl.SSLParameters; | |
| import java.io.IOException; | |
| import java.io.UncheckedIOException; | |
| import java.lang.ref.Reference; | |
| import java.lang.ref.WeakReference; | |
| import java.net.Authenticator; | |
| import java.net.ConnectException; | |
| import java.net.CookieHandler; | |
| import java.net.InetAddress; | |
| import java.net.ProtocolException; | |
| import java.net.ProxySelector; | |
| import java.net.http.HttpConnectTimeoutException; | |
| import java.net.http.HttpTimeoutException; | |
| import java.nio.ByteBuffer; | |
| import java.nio.channels.CancelledKeyException; | |
| import java.nio.channels.ClosedChannelException; | |
| import java.nio.channels.ClosedSelectorException; | |
| import java.nio.channels.SelectableChannel; | |
| import java.nio.channels.SelectionKey; | |
| import java.nio.channels.Selector; | |
| import java.security.NoSuchAlgorithmException; | |
| import java.time.Duration; | |
| import java.time.temporal.ChronoUnit; | |
| import java.util.ArrayList; | |
| import java.util.HashSet; | |
| import java.util.Iterator; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import java.util.TreeSet; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.ConcurrentSkipListSet; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.Executor; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.RejectedExecutionException; | |
| import java.util.concurrent.ThreadFactory; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.concurrent.atomic.AtomicLong; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.concurrent.locks.ReentrantLock; | |
| import java.util.function.BiConsumer; | |
| import java.util.function.BooleanSupplier; | |
| import java.util.stream.Stream; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.net.http.HttpResponse.BodyHandler; | |
| import java.net.http.HttpResponse.PushPromiseHandler; | |
| import java.net.http.WebSocket; | |
| import jdk.internal.net.http.common.BufferSupplier; | |
| import jdk.internal.net.http.common.Deadline; | |
| import jdk.internal.net.http.common.HttpBodySubscriberWrapper; | |
| import jdk.internal.net.http.common.Log; | |
| import jdk.internal.net.http.common.Logger; | |
| import jdk.internal.net.http.common.MinimalFuture; | |
| import jdk.internal.net.http.common.Pair; | |
| import jdk.internal.net.http.common.TimeSource; | |
| import jdk.internal.net.http.common.Utils; | |
| import jdk.internal.net.http.common.OperationTrackers.Trackable; | |
| import jdk.internal.net.http.common.OperationTrackers.Tracker; | |
| import jdk.internal.net.http.websocket.BuilderImpl; | |
| /** | |
| * Client implementation. Contains all configuration information and also | |
| * the selector manager thread which allows async events to be registered | |
| * and delivered when they occur. See AsyncEvent. | |
| */ | |
| final class HttpClientImpl extends HttpClient implements Trackable { | |
| static final boolean DEBUGELAPSED = Utils.TESTING || Utils.DEBUG; // dev flag | |
| static final boolean DEBUGTIMEOUT = false; // dev flag | |
| final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG); | |
| final Logger debugelapsed = Utils.getDebugLogger(this::dbgString, DEBUGELAPSED); | |
| final Logger debugtimeout = Utils.getDebugLogger(this::dbgString, DEBUGTIMEOUT); | |
| static final AtomicLong CLIENT_IDS = new AtomicLong(); | |
| private final AtomicLong CONNECTION_IDS = new AtomicLong(); | |
| static final int DEFAULT_KEEP_ALIVE_TIMEOUT = 30; | |
| static final long KEEP_ALIVE_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout", DEFAULT_KEEP_ALIVE_TIMEOUT); | |
| // Defaults to value used for HTTP/1 Keep-Alive Timeout. Can be overridden by jdk.httpclient.keepalive.timeout.h2 property. | |
| static final long IDLE_CONNECTION_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); | |
| // Define the default factory as a static inner class | |
| // that embeds all the necessary logic to avoid | |
| // the risk of using a lambda that might keep a reference on the | |
| // HttpClient instance from which it was created (helps with | |
| // heapdump analysis). | |
| private static final class DefaultThreadFactory implements ThreadFactory { | |
| private final String namePrefix; | |
| private final AtomicInteger nextId = new AtomicInteger(); | |
| DefaultThreadFactory(long clientID) { | |
| namePrefix = "HttpClient-" + clientID + "-Worker-"; | |
| } | |
| @Override | |
| public Thread newThread(Runnable r) { | |
| String name = namePrefix + nextId.getAndIncrement(); | |
| Thread t = new Thread(null, r, name, 0, false); | |
| t.setDaemon(true); | |
| return t; | |
| } | |
| } | |
| /** | |
| * A DelegatingExecutor is an executor that delegates tasks to | |
| * a wrapped executor when it detects that the current thread | |
| * is the SelectorManager thread. If the current thread is not | |
| * the selector manager thread the given task is executed inline. | |
| */ | |
| static final class DelegatingExecutor implements Executor { | |
| private final BooleanSupplier isInSelectorThread; | |
| private final Executor delegate; | |
| private final BiConsumer<Runnable, Throwable> errorHandler; | |
| DelegatingExecutor(BooleanSupplier isInSelectorThread, | |
| Executor delegate, | |
| BiConsumer<Runnable, Throwable> errorHandler) { | |
| this.isInSelectorThread = isInSelectorThread; | |
| this.delegate = delegate; | |
| this.errorHandler = errorHandler; | |
| } | |
| Executor delegate() { | |
| return delegate; | |
| } | |
| @Override | |
| public void execute(Runnable command) { | |
| if (isInSelectorThread.getAsBoolean()) { | |
| ensureExecutedAsync(command); | |
| } else { | |
| command.run(); | |
| } | |
| } | |
| public void ensureExecutedAsync(Runnable command) { | |
| try { | |
| delegate.execute(command); | |
| } catch (RejectedExecutionException t) { | |
| errorHandler.accept(command, t); | |
| ASYNC_POOL.execute(command); | |
| } | |
| } | |
| void shutdown() { | |
| if (delegate instanceof ExecutorService service) { | |
| service.shutdown(); | |
| } | |
| } | |
| } | |
| // We maintain a list of pending requests that will be aborted if ever | |
| // the selector manager thread exists abnormally. | |
| // The request, its id, and its completable future, are stored in a record-like | |
| // PendingRequest object added to the pending requests set (pendingRequests). | |
| // | |
| // When the request's cf completes, either normally or abnormally, a dependent action | |
| // will remove the PendingRequest object from the pending requests set. | |
| // If the SelectorManager threads exits abnormally, all pending requests in the | |
| // pending requests set will be completed exceptionally, in the ASYNC_POOL. | |
| // | |
| // HttpClientImpl::registerPending(id, req, cf, client) is called from sendAsync | |
| // to register the pending request in the pending requests set before returning | |
| // the completable future to the caller. | |
| // | |
| // HttpClientImpl::abortPendingRequests(client, Throwable reason) is called from | |
| // the SelectorManager when a throwable is caught just before exiting. | |
| // | |
| // A dependent action is registered with the pending request's cf to make sure | |
| // that the pending request will be removed from the pending requests set if, | |
| // or after, the cf is completed. | |
| // | |
| private final Set<PendingRequest> pendingRequests; | |
| // an id to ensure total order of pending request objects | |
| private final AtomicLong pendingRequestId = new AtomicLong(); | |
| private static final class PendingRequest implements Comparable<PendingRequest> { | |
| final long id; | |
| final HttpRequest request; | |
| final CompletableFuture<?> cf; | |
| final HttpClientImpl client; | |
| final MultiExchange<?> mex; | |
| Object ref; | |
| private PendingRequest(long id, | |
| HttpRequest request, | |
| CompletableFuture<?> cf, | |
| MultiExchange<?> mex, | |
| HttpClientImpl client) { | |
| this.id = id; | |
| this.request = request; | |
| this.cf = cf; | |
| this.mex = mex; | |
| this.client = client; | |
| } | |
| public void abort(Throwable t) { | |
| try { | |
| if (client.isSelectorThread()) { | |
| var done = cf.exceptionally((e) -> null); | |
| ASYNC_POOL.execute(() -> completeExceptionally(t)); | |
| // special case for when this method is called in the SelectorManager thread: | |
| // we want to wait until all futures are completed before proceeding to | |
| // shutdown. This ensures that the caller receive the actual `reason` | |
| // and not something like "HTTP/2 client closed"... | |
| done.join(); | |
| } else { | |
| cf.completeExceptionally(t); | |
| } | |
| } finally { | |
| mex.cancel(Utils.getIOException(t)); | |
| } | |
| } | |
| private void completeExceptionally(Throwable t) { | |
| if (client.debug.on()) { | |
| client.debug.log("aborting %s with %s", this, t); | |
| } | |
| try { cf.completeExceptionally(t); } catch (Throwable e) { | |
| client.debug.log("Failed to complete cf for [%s]: %s", this, e); | |
| } | |
| } | |
| @Override | |
| public int compareTo(PendingRequest o) { | |
| if (o == null) return 1; | |
| return Long.compare(id, o.id); | |
| } | |
| public String toString() { | |
| return id + ": " + request.toString(); | |
| } | |
| } | |
| static <T> CompletableFuture<T> registerPending(PendingRequest pending, CompletableFuture<T> res) { | |
| // shortcut if cf is already completed: no need to go through the trouble of | |
| // registering it | |
| if (pending.cf.isDone()) return res; | |
| var client = pending.client; | |
| var cf = pending.cf; | |
| var id = pending.id; | |
| boolean added = client.pendingRequests.add(pending); | |
| // this may immediately remove `pending` from the set is the cf is already completed | |
| var ref = res.whenComplete((r,t) -> client.pendingRequests.remove(pending)); | |
| pending.ref = ref; | |
| assert added : "request %d was already added".formatted(id); | |
| // should not happen, unless the selector manager has already | |
| // exited abnormally | |
| if (client.selmgr.isClosed()) { | |
| pending.abort(client.selmgr.selectorClosedException()); | |
| } | |
| return ref; | |
| } | |
| static void abortPendingRequests(HttpClientImpl client, Throwable reason) { | |
| reason = Utils.getCompletionCause(reason); | |
| if (client.debug.on()) { | |
| var msg = reason instanceof RejectedExecutionException | |
| ? reason.getClass() : reason; | |
| client.debug.log("aborting pending requests due to: %s", msg); | |
| } | |
| closeSubscribers(client, reason); | |
| var pendingRequests = client.pendingRequests; | |
| while (!pendingRequests.isEmpty()) { | |
| var pendings = pendingRequests.iterator(); | |
| while (pendings.hasNext()) { | |
| var pending = pendings.next(); | |
| try { | |
| pending.abort(reason); | |
| } finally { | |
| pendings.remove(); | |
| } | |
| } | |
| } | |
| } | |
| private final CookieHandler cookieHandler; | |
| private final Duration connectTimeout; | |
| private final Redirect followRedirects; | |
| private final ProxySelector userProxySelector; | |
| private final ProxySelector proxySelector; | |
| private final Authenticator authenticator; | |
| private final Version version; | |
| private final ConnectionPool connections; | |
| private final DelegatingExecutor delegatingExecutor; | |
| private final boolean isDefaultExecutor; | |
| private final SSLContext sslContext; | |
| private final SSLParameters sslParams; | |
| private final SelectorManager selmgr; | |
| private final FilterFactory filters; | |
| private final Http2ClientImpl client2; | |
| private final long id; | |
| private final String dbgTag; | |
| private final InetAddress localAddr; | |
| // The SSL DirectBuffer Supplier provides the ability to recycle | |
| // buffers used between the socket reader and the SSLEngine, or | |
| // more precisely between the SocketTube publisher and the | |
| // SSLFlowDelegate reader. | |
| private final SSLDirectBufferSupplier sslBufferSupplier | |
| = new SSLDirectBufferSupplier(this); | |
| // This reference is used to keep track of the facade HttpClient | |
| // that was returned to the application code. | |
| // It makes it possible to know when the application no longer | |
| // holds any reference to the HttpClient. | |
| // Unfortunately, this information is not enough to know when | |
| // to exit the SelectorManager thread. Because of the asynchronous | |
| // nature of the API, we also need to wait until all pending operations | |
| // have completed. | |
| private final WeakReference<HttpClientFacade> facadeRef; | |
| private final WeakReference<HttpClientImpl> implRef; | |
| private final ConcurrentSkipListSet<PlainHttpConnection> openedConnections | |
| = new ConcurrentSkipListSet<>(HttpConnection.COMPARE_BY_ID); | |
| private final ConcurrentSkipListSet<HttpBodySubscriberWrapper<?>> subscribers | |
| = new ConcurrentSkipListSet<>(HttpBodySubscriberWrapper.COMPARE_BY_ID); | |
| // This counter keeps track of the number of operations pending | |
| // on the HttpClient. The SelectorManager thread will wait | |
| // until there are no longer any pending operations and the | |
| // facadeRef is cleared before exiting. | |
| // | |
| // The pendingOperationCount is incremented every time a send/sendAsync | |
| // operation is invoked on the HttpClient, and is decremented when | |
| // the HttpResponse<T> object is returned to the user. | |
| // However, at this point, the body may not have been fully read yet. | |
| // This is the case when the response T is implemented as a streaming | |
| // subscriber (such as an InputStream). | |
| // | |
| // To take care of this issue the pendingOperationCount will additionally | |
| // be incremented/decremented in the following cases: | |
| // | |
| // 1. For HTTP/2 it is incremented when a stream is added to the | |
| // Http2Connection streams map, and decreased when the stream is removed | |
| // from the map. This should also take care of push promises. | |
| // 2. For WebSocket the count is increased when creating a | |
| // DetachedConnectionChannel for the socket, and decreased | |
| // when the channel is closed. | |
| // In addition, the HttpClient facade is passed to the WebSocket builder, | |
| // (instead of the client implementation delegate). | |
| // 3. For HTTP/1.1 the count is incremented before starting to parse the body | |
| // response, and decremented when the parser has reached the end of the | |
| // response body flow. | |
| // | |
| // This should ensure that the selector manager thread remains alive until | |
| // the response has been fully received or the web socket is closed. | |
| private final AtomicLong pendingOperationCount = new AtomicLong(); | |
| private final AtomicLong pendingWebSocketCount = new AtomicLong(); | |
| private final AtomicLong pendingHttpOperationsCount = new AtomicLong(); | |
| private final AtomicLong pendingHttpRequestCount = new AtomicLong(); | |
| private final AtomicLong pendingHttp2StreamCount = new AtomicLong(); | |
| private final AtomicLong pendingTCPConnectionCount = new AtomicLong(); | |
| private final AtomicLong pendingSubscribersCount = new AtomicLong(); | |
| private final AtomicBoolean isAlive = new AtomicBoolean(); | |
| private final AtomicBoolean isStarted = new AtomicBoolean(); | |
| private volatile boolean shutdownRequested; | |
| /** A Set of, deadline first, ordered timeout events. */ | |
| private final TreeSet<TimeoutEvent> timeouts; | |
| /** | |
| * This is a bit tricky: | |
| * 1. an HttpClientFacade has a final HttpClientImpl field. | |
| * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field, | |
| * where the referent is the facade created for that instance. | |
| * 3. We cannot just create the HttpClientFacade in the HttpClientImpl | |
| * constructor, because it would be only weakly referenced and could | |
| * be GC'ed before we can return it. | |
| * The solution is to use an instance of SingleFacadeFactory which will | |
| * allow the caller of new HttpClientImpl(...) to retrieve the facade | |
| * after the HttpClientImpl has been created. | |
| */ | |
| private static final class SingleFacadeFactory { | |
| HttpClientFacade facade; | |
| HttpClientFacade createFacade(HttpClientImpl impl) { | |
| assert facade == null; | |
| return (facade = new HttpClientFacade(impl)); | |
| } | |
| } | |
| static HttpClientFacade create(HttpClientBuilderImpl builder) { | |
| SingleFacadeFactory facadeFactory = new SingleFacadeFactory(); | |
| HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory); | |
| impl.start(); | |
| assert facadeFactory.facade != null; | |
| assert impl.facadeRef.get() == facadeFactory.facade; | |
| return facadeFactory.facade; | |
| } | |
| private HttpClientImpl(HttpClientBuilderImpl builder, | |
| SingleFacadeFactory facadeFactory) { | |
| id = CLIENT_IDS.incrementAndGet(); | |
| dbgTag = "HttpClientImpl(" + id +")"; | |
| localAddr = builder.localAddr; | |
| if (builder.sslContext == null) { | |
| try { | |
| sslContext = SSLContext.getDefault(); | |
| } catch (NoSuchAlgorithmException ex) { | |
| throw new UncheckedIOException(new IOException(ex)); | |
| } | |
| } else { | |
| sslContext = builder.sslContext; | |
| } | |
| Executor ex = builder.executor; | |
| if (ex == null) { | |
| ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id)); | |
| isDefaultExecutor = true; | |
| } else { | |
| isDefaultExecutor = false; | |
| } | |
| pendingRequests = new ConcurrentSkipListSet<>(); | |
| delegatingExecutor = new DelegatingExecutor(this::isSelectorThread, ex, | |
| this::onSubmitFailure); | |
| facadeRef = new WeakReference<>(facadeFactory.createFacade(this)); | |
| implRef = new WeakReference<>(this); | |
| client2 = new Http2ClientImpl(this); | |
| cookieHandler = builder.cookieHandler; | |
| connectTimeout = builder.connectTimeout; | |
| followRedirects = builder.followRedirects == null ? | |
| Redirect.NEVER : builder.followRedirects; | |
| this.userProxySelector = builder.proxy; | |
| this.proxySelector = Optional.ofNullable(userProxySelector) | |
| .orElseGet(ProxySelector::getDefault); | |
| if (debug.on()) | |
| debug.log("proxySelector is %s (user-supplied=%s)", | |
| this.proxySelector, userProxySelector != null); | |
| authenticator = builder.authenticator; | |
| if (builder.version == null) { | |
| version = HttpClient.Version.HTTP_2; | |
| } else { | |
| version = builder.version; | |
| } | |
| if (builder.sslParams == null) { | |
| sslParams = getDefaultParams(sslContext); | |
| } else { | |
| sslParams = builder.sslParams; | |
| } | |
| connections = new ConnectionPool(id); | |
| connections.start(); | |
| timeouts = new TreeSet<>(); | |
| try { | |
| selmgr = new SelectorManager(this); | |
| } catch (IOException e) { | |
| // unlikely | |
| throw new UncheckedIOException(e); | |
| } | |
| selmgr.setDaemon(true); | |
| filters = new FilterFactory(); | |
| initFilters(); | |
| assert facadeRef.get() != null; | |
| } | |
| // called when the facade is GC'ed. | |
| // Just wakes up the selector to cleanup... | |
| void facadeCleanup() { | |
| SelectorManager selmgr = this.selmgr; | |
| if (selmgr != null) selmgr.wakeupSelector(); | |
| } | |
| void onSubmitFailure(Runnable command, Throwable failure) { | |
| selmgr.abort(failure); | |
| } | |
| private void start() { | |
| try { | |
| selmgr.start(); | |
| } catch (Throwable t) { | |
| isStarted.set(true); | |
| throw t; | |
| } | |
| } | |
| // Called from the SelectorManager thread, just before exiting. | |
| // Clears the HTTP/1.1 and HTTP/2 cache, ensuring that the connections | |
| // that may be still lingering there are properly closed (and their | |
| // possibly still opened SocketChannel released). | |
| private void stop() { | |
| // Clears HTTP/1.1 cache and close its connections | |
| connections.stop(); | |
| // Clears HTTP/2 cache and close its connections. | |
| client2.stop(); | |
| // make sure all subscribers are completed | |
| closeSubscribers(); | |
| // close TCP connection if any are still opened | |
| openedConnections.forEach(this::closeConnection); | |
| // shutdown the executor if needed | |
| if (isDefaultExecutor) delegatingExecutor.shutdown(); | |
| } | |
| private void closeSubscribers() { | |
| if (subscribers.isEmpty()) return; | |
| IOException io = selmgr.selectorClosedException(); | |
| closeSubscribers(this, io); | |
| } | |
| private static void closeSubscribers(HttpClientImpl client, Throwable t) { | |
| client.subscribers.forEach(s -> s.onError(t)); | |
| } | |
| /** | |
| * Adds the given subscriber to the subscribers list, or call | |
| * its {@linkplain HttpBodySubscriberWrapper#onError onError} | |
| * method if the client is shutting down. | |
| * @param subscriber the subscriber | |
| * @return true if the subscriber was added to the list. | |
| */ | |
| public boolean registerSubscriber(HttpBodySubscriberWrapper<?> subscriber) { | |
| if (!selmgr.isClosed()) { | |
| selmgr.lock(); | |
| try { | |
| if (!selmgr.isClosed()) { | |
| if (subscribers.add(subscriber)) { | |
| long count = pendingSubscribersCount.incrementAndGet(); | |
| if (debug.on()) { | |
| debug.log("body subscriber registered: " + count); | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| } finally { | |
| selmgr.unlock(); | |
| } | |
| } | |
| subscriber.onError(selmgr.selectorClosedException()); | |
| return false; | |
| } | |
| /** | |
| * Remove the given subscriber from the subscribers list. | |
| * @param subscriber the subscriber | |
| * @return true if the subscriber was found and removed from the list. | |
| */ | |
| public boolean unregisterSubscriber(HttpBodySubscriberWrapper<?> subscriber) { | |
| if (subscribers.remove(subscriber)) { | |
| long count = pendingSubscribersCount.decrementAndGet(); | |
| if (debug.on()) { | |
| debug.log("body subscriber unregistered: " + count); | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| private void closeConnection(HttpConnection conn) { | |
| try { conn.close(); } catch (Throwable e) { | |
| if (Log.channel()) { | |
| Log.logChannel("Failed to close connection: " + e); | |
| } | |
| } | |
| } | |
| @Override | |
| public void shutdown() { | |
| shutdownRequested = true; | |
| selmgr.wakeupSelector(); | |
| } | |
| @Override | |
| public void shutdownNow() { | |
| shutdown(); | |
| selmgr.abort(new IOException("shutdownNow")); | |
| } | |
| @Override | |
| public boolean awaitTermination(Duration duration) throws InterruptedException { | |
| // Implicit NPE will be thrown if duration is null | |
| return selmgr.join(duration); | |
| } | |
| @Override | |
| public boolean isTerminated() { | |
| return isStarted.get() && !isAlive.get(); | |
| } | |
| private static SSLParameters getDefaultParams(SSLContext ctx) { | |
| SSLParameters params = ctx.getDefaultSSLParameters(); | |
| return params; | |
| } | |
| // Returns the facade that was returned to the application code. | |
| // May be null if that facade is no longer referenced. | |
| final HttpClientFacade facade() { | |
| return facadeRef.get(); | |
| } | |
| public long newConnectionId() { | |
| return CONNECTION_IDS.incrementAndGet(); | |
| } | |
| // Increments the pendingTCPConnectionCount | |
| public void connectionOpened(PlainHttpConnection plainHttpConnection) { | |
| if (!finished()) { | |
| if (openedConnections.add(plainHttpConnection)) { | |
| pendingTCPConnectionCount.incrementAndGet(); | |
| } | |
| } else closeConnection(plainHttpConnection); | |
| } | |
| // Decrements the pendingTCPConnectionCount | |
| public void connectionClosed(PlainHttpConnection plainHttpConnection) { | |
| if (openedConnections.remove(plainHttpConnection)) { | |
| pendingTCPConnectionCount.decrementAndGet(); | |
| } | |
| } | |
| // Increments the pendingHttpRequestCount and pendingOperationCount. | |
| final long requestReference() { | |
| pendingHttpRequestCount.incrementAndGet(); | |
| return reference(); | |
| } | |
| // Decrements the pendingHttpRequestCount and pendingOperationCount. | |
| final long requestUnreference() { | |
| pendingHttpRequestCount.decrementAndGet(); | |
| return unreference(); | |
| } | |
| // Increments the pendingHttpOperationsCount and pendingOperationCount. | |
| final long reference() { | |
| pendingHttpOperationsCount.incrementAndGet(); | |
| return pendingOperationCount.incrementAndGet(); | |
| } | |
| // Decrements the pendingHttpOperationsCount and pendingOperationCount. | |
| final long unreference() { | |
| final long count = pendingOperationCount.decrementAndGet(); | |
| final long httpCount = pendingHttpOperationsCount.decrementAndGet(); | |
| final long http2Count = pendingHttp2StreamCount.get(); | |
| final long webSocketCount = pendingWebSocketCount.get(); | |
| if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { | |
| selmgr.wakeupSelector(); | |
| } | |
| assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; | |
| assert http2Count >= 0 : "count of HTTP/2 operations < 0"; | |
| assert webSocketCount >= 0 : "count of WS operations < 0"; | |
| assert count >= 0 : "count of pending operations < 0"; | |
| return count; | |
| } | |
| // Increments the pendingHttp2StreamCount and pendingOperationCount. | |
| final long streamReference() { | |
| pendingHttp2StreamCount.incrementAndGet(); | |
| return pendingOperationCount.incrementAndGet(); | |
| } | |
| // Decrements the pendingHttp2StreamCount and pendingOperationCount. | |
| final long streamUnreference() { | |
| final long count = pendingOperationCount.decrementAndGet(); | |
| final long http2Count = pendingHttp2StreamCount.decrementAndGet(); | |
| final long httpCount = pendingHttpOperationsCount.get(); | |
| final long webSocketCount = pendingWebSocketCount.get(); | |
| if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { | |
| selmgr.wakeupSelector(); | |
| } | |
| assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; | |
| assert http2Count >= 0 : "count of HTTP/2 operations < 0"; | |
| assert webSocketCount >= 0 : "count of WS operations < 0"; | |
| assert count >= 0 : "count of pending operations < 0"; | |
| return count; | |
| } | |
| // Increments the pendingWebSocketCount and pendingOperationCount. | |
| final long webSocketOpen() { | |
| pendingWebSocketCount.incrementAndGet(); | |
| return pendingOperationCount.incrementAndGet(); | |
| } | |
| // Decrements the pendingWebSocketCount and pendingOperationCount. | |
| final long webSocketClose() { | |
| final long count = pendingOperationCount.decrementAndGet(); | |
| final long webSocketCount = pendingWebSocketCount.decrementAndGet(); | |
| final long httpCount = pendingHttpOperationsCount.get(); | |
| final long http2Count = pendingHttp2StreamCount.get(); | |
| if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { | |
| selmgr.wakeupSelector(); | |
| } | |
| assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; | |
| assert http2Count >= 0 : "count of HTTP/2 operations < 0"; | |
| assert webSocketCount >= 0 : "count of WS operations < 0"; | |
| assert count >= 0 : "count of pending operations < 0"; | |
| return count; | |
| } | |
| // Returns the pendingOperationCount. | |
| // Incremented with any operation, whether it's HTTP/1.1, HTTP/2, or WebSocket | |
| final long referenceCount() { | |
| return pendingOperationCount.get(); | |
| } | |
| // Trackers are used in test to verify that an instance of | |
| // HttpClient has shutdown correctly, and that all operations | |
| // have terminated. | |
| static final class HttpClientTracker implements Tracker { | |
| final AtomicLong requestCount; | |
| final AtomicLong httpCount; | |
| final AtomicLong http2Count; | |
| final AtomicLong websocketCount; | |
| final AtomicLong operationsCount; | |
| final AtomicLong connnectionsCount; | |
| final AtomicLong subscribersCount; | |
| final Reference<?> reference; | |
| final Reference<?> implRef; | |
| final AtomicBoolean isAlive; | |
| final AtomicBoolean isStarted; | |
| final String name; | |
| HttpClientTracker(AtomicLong request, | |
| AtomicLong http, | |
| AtomicLong http2, | |
| AtomicLong ws, | |
| AtomicLong ops, | |
| AtomicLong conns, | |
| AtomicLong subscribers, | |
| Reference<?> ref, | |
| Reference<?> implRef, | |
| AtomicBoolean isAlive, | |
| AtomicBoolean isStarted, | |
| String name) { | |
| this.requestCount = request; | |
| this.httpCount = http; | |
| this.http2Count = http2; | |
| this.websocketCount = ws; | |
| this.operationsCount = ops; | |
| this.connnectionsCount = conns; | |
| this.subscribersCount = subscribers; | |
| this.reference = ref; | |
| this.implRef = implRef; | |
| this.isAlive = isAlive; | |
| this.isStarted = isStarted; | |
| this.name = name; | |
| } | |
| @Override | |
| public long getOutstandingSubscribers() { | |
| return subscribersCount.get(); | |
| } | |
| @Override | |
| public long getOutstandingOperations() { | |
| return operationsCount.get(); | |
| } | |
| @Override | |
| public long getOutstandingHttpRequests() { | |
| return requestCount.get(); | |
| } | |
| @Override | |
| public long getOutstandingTcpConnections() { return connnectionsCount.get();} | |
| @Override | |
| public long getOutstandingHttpOperations() { | |
| return httpCount.get(); | |
| } | |
| @Override | |
| public long getOutstandingHttp2Streams() { return http2Count.get(); } | |
| @Override | |
| public long getOutstandingWebSocketOperations() { | |
| return websocketCount.get(); | |
| } | |
| @Override | |
| public boolean isFacadeReferenced() { | |
| return !reference.refersTo(null); | |
| } | |
| public boolean isImplementationReferenced() { | |
| return !implRef.refersTo(null); | |
| } | |
| // The selector is considered alive if it's not yet started | |
| @Override | |
| public boolean isSelectorAlive() { return isAlive.get() || !isStarted.get(); } | |
| @Override | |
| public String getName() { | |
| return name; | |
| } | |
| } | |
| public Tracker getOperationsTracker() { | |
| return new HttpClientTracker( | |
| pendingHttpRequestCount, | |
| pendingHttpOperationsCount, | |
| pendingHttp2StreamCount, | |
| pendingWebSocketCount, | |
| pendingOperationCount, | |
| pendingTCPConnectionCount, | |
| pendingSubscribersCount, | |
| facadeRef, | |
| implRef, | |
| isAlive, | |
| isStarted, | |
| dbgTag); | |
| } | |
| // Called by the SelectorManager thread to figure out whether it's time | |
| // to terminate. | |
| boolean finished() { | |
| if (referenceCount() > 0) return false; | |
| if (shutdownRequested) { | |
| synchronized (this) { | |
| // avoid race with sendAsync | |
| if (referenceCount() == 0) return true; | |
| } | |
| } | |
| return !isReferenced(); | |
| } | |
| private boolean isReferenced() { | |
| return !facadeRef.refersTo(null) || referenceCount() > 0; | |
| } | |
| /** | |
| * Wait for activity on given exchange. | |
| * The following occurs in the SelectorManager thread. | |
| * | |
| * 1) add to selector | |
| * 2) If selector fires for this exchange then | |
| * call AsyncEvent.handle() | |
| * | |
| * If exchange needs to change interest ops, then call registerEvent() again. | |
| */ | |
| void registerEvent(AsyncEvent exchange) throws IOException { | |
| selmgr.register(exchange); | |
| } | |
| /** | |
| * Allows an AsyncEvent to modify its interestOps. | |
| * @param event The modified event. | |
| */ | |
| void eventUpdated(AsyncEvent event) throws ClosedChannelException { | |
| assert !(event instanceof AsyncTriggerEvent); | |
| selmgr.eventUpdated(event); | |
| } | |
| boolean isSelectorThread() { | |
| return Thread.currentThread() == selmgr; | |
| } | |
| boolean isSelectorClosed() { | |
| return selmgr.isClosed(); | |
| } | |
| IOException selectorClosedException() { | |
| return selmgr.selectorClosedException(); | |
| } | |
| Http2ClientImpl client2() { | |
| return client2; | |
| } | |
| private void debugCompleted(String tag, long startNanos, HttpRequest req) { | |
| if (debugelapsed.on()) { | |
| debugelapsed.log(tag + " elapsed " | |
| + (System.nanoTime() - startNanos)/1000_000L | |
| + " millis for " + req.method() | |
| + " to " + req.uri()); | |
| } | |
| } | |
| @Override | |
| public <T> HttpResponse<T> | |
| send(HttpRequest req, BodyHandler<T> responseHandler) | |
| throws IOException, InterruptedException | |
| { | |
| CompletableFuture<HttpResponse<T>> cf = null; | |
| // if the thread is already interrupted no need to go further. | |
| // cf.get() would throw anyway. | |
| if (Thread.interrupted()) throw new InterruptedException(); | |
| try { | |
| cf = sendAsync(req, responseHandler, null, null); | |
| return cf.get(); | |
| } catch (InterruptedException ie) { | |
| if (cf != null) { | |
| cf.cancel(true); | |
| } | |
| throw ie; | |
| } catch (ExecutionException e) { | |
| final Throwable throwable = e.getCause(); | |
| final String msg = throwable.getMessage(); | |
| if (throwable instanceof IllegalArgumentException) { | |
| throw new IllegalArgumentException(msg, throwable); | |
| } else if (throwable instanceof SecurityException) { | |
| throw new SecurityException(msg, throwable); | |
| } else if (throwable instanceof HttpConnectTimeoutException) { | |
| HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg); | |
| hcte.initCause(throwable); | |
| throw hcte; | |
| } else if (throwable instanceof HttpTimeoutException) { | |
| throw new HttpTimeoutException(msg); | |
| } else if (throwable instanceof ConnectException) { | |
| ConnectException ce = new ConnectException(msg); | |
| ce.initCause(throwable); | |
| throw ce; | |
| } else if (throwable instanceof SSLHandshakeException) { | |
| // special case for SSLHandshakeException | |
| throw new SSLHandshakeException(msg, throwable); | |
| } else if (throwable instanceof SSLException) { | |
| // any other SSLException is wrapped in a plain | |
| // SSLException | |
| throw new SSLException(msg, throwable); | |
| } else if (throwable instanceof ProtocolException) { | |
| ProtocolException pe = new ProtocolException(msg); | |
| pe.initCause(throwable); | |
| throw pe; | |
| } else if (throwable instanceof IOException) { | |
| throw new IOException(msg, throwable); | |
| } else { | |
| throw new IOException(msg, throwable); | |
| } | |
| } | |
| } | |
| private static final Executor ASYNC_POOL = new CompletableFuture<Void>().defaultExecutor(); | |
| @Override | |
| public <T> CompletableFuture<HttpResponse<T>> | |
| sendAsync(HttpRequest userRequest, BodyHandler<T> responseHandler) | |
| { | |
| return sendAsync(userRequest, responseHandler, null); | |
| } | |
| @Override | |
| public <T> CompletableFuture<HttpResponse<T>> | |
| sendAsync(HttpRequest userRequest, | |
| BodyHandler<T> responseHandler, | |
| PushPromiseHandler<T> pushPromiseHandler) { | |
| return sendAsync(userRequest, responseHandler, pushPromiseHandler, delegatingExecutor.delegate); | |
| } | |
| private <T> CompletableFuture<HttpResponse<T>> | |
| sendAsync(HttpRequest userRequest, | |
| BodyHandler<T> responseHandler, | |
| PushPromiseHandler<T> pushPromiseHandler, | |
| Executor exchangeExecutor) { | |
| Objects.requireNonNull(userRequest); | |
| Objects.requireNonNull(responseHandler); | |
| if (shutdownRequested) { | |
| return MinimalFuture.failedFuture(new IOException("closed")); | |
| } | |
| // should not happen, unless the selector manager has | |
| // exited abnormally | |
| if (selmgr.isClosed()) { | |
| return MinimalFuture.failedFuture(selmgr.selectorClosedException()); | |
| } | |
| // Clone the possibly untrusted HttpRequest | |
| HttpRequestImpl requestImpl = new HttpRequestImpl(userRequest, proxySelector); | |
| if (requestImpl.method().equals("CONNECT")) | |
| throw new IllegalArgumentException("Unsupported method CONNECT"); | |
| long id = pendingRequestId.incrementAndGet(); | |
| long start = DEBUGELAPSED ? System.nanoTime() : 0; | |
| synchronized (this) { | |
| if (shutdownRequested) { | |
| return MinimalFuture.failedFuture(new IOException("closed")); | |
| } | |
| requestReference(); | |
| } | |
| try { | |
| if (debugelapsed.on()) | |
| debugelapsed.log("ClientImpl (async) send %s", userRequest); | |
| // When using sendAsync(...) we explicitly pass the | |
| // executor's delegate as exchange executor to force | |
| // asynchronous scheduling of the exchange. | |
| // When using send(...) we don't specify any executor | |
| // and default to using the client's delegating executor | |
| // which only spawns asynchronous tasks if it detects | |
| // that the current thread is the selector manager | |
| // thread. This will cause everything to execute inline | |
| // until we need to schedule some event with the selector. | |
| Executor executor = exchangeExecutor == null | |
| ? this.delegatingExecutor : exchangeExecutor; | |
| MultiExchange<T> mex = new MultiExchange<>(userRequest, | |
| requestImpl, | |
| this, | |
| responseHandler, | |
| pushPromiseHandler); | |
| CompletableFuture<HttpResponse<T>> mexCf = mex.responseAsync(executor); | |
| CompletableFuture<HttpResponse<T>> res = mexCf.whenComplete((b,t) -> requestUnreference()); | |
| if (DEBUGELAPSED) { | |
| res = res.whenComplete( | |
| (b,t) -> debugCompleted("ClientImpl (async)", start, userRequest)); | |
| } | |
| // The mexCf is the Cf we need to abort if the SelectorManager thread | |
| // is aborted. | |
| PendingRequest pending = new PendingRequest(id, requestImpl, mexCf, mex, this); | |
| res = registerPending(pending, res); | |
| if (exchangeExecutor != null) { | |
| // makes sure that any dependent actions happen in the CF default | |
| // executor. This is only needed for sendAsync(...), when | |
| // exchangeExecutor is non-null. | |
| return res.isDone() ? res | |
| : res.whenCompleteAsync((r, t) -> { /* do nothing */}, ASYNC_POOL); | |
| } else { | |
| // make a defensive copy that can be safely canceled | |
| // by the caller | |
| return res.isDone() ? res : res.copy(); | |
| } | |
| } catch (Throwable t) { | |
| requestUnreference(); | |
| debugCompleted("ClientImpl (async)", start, userRequest); | |
| throw t; | |
| } | |
| } | |
| // Main loop for this client's selector | |
| private static final class SelectorManager extends Thread { | |
| // For testing purposes we have an internal System property that | |
| // can control the frequency at which the selector manager will wake | |
| // up when there are no pending operations. | |
| // Increasing the frequency (shorter delays) might allow the selector | |
| // to observe that the facade is no longer referenced and might allow | |
| // the selector thread to terminate more timely - for when nothing is | |
| // ongoing it will only check for that condition every NODEADLINE ms. | |
| // To avoid misuse of the property, the delay that can be specified | |
| // is comprised between [MIN_NODEADLINE, MAX_NODEADLINE], and its default | |
| // value if unspecified (or <= 0) is DEF_NODEADLINE = 3000ms | |
| // The property is -Djdk.internal.httpclient.selectorTimeout=<millis> | |
| private static final int MIN_NODEADLINE = 1000; // ms | |
| private static final int MAX_NODEADLINE = 1000 * 1200; // ms | |
| private static final int DEF_NODEADLINE = 3000; // ms | |
| private static final long NODEADLINE; // default is DEF_NODEADLINE ms | |
| static { | |
| // ensure NODEADLINE is initialized with some valid value. | |
| long deadline = Utils.getIntegerProperty( | |
| "jdk.internal.httpclient.selectorTimeout", | |
| DEF_NODEADLINE); // millis | |
| if (deadline <= 0) deadline = DEF_NODEADLINE; | |
| deadline = Math.max(deadline, MIN_NODEADLINE); | |
| NODEADLINE = Math.min(deadline, MAX_NODEADLINE); | |
| } | |
| private final Selector selector; | |
| private volatile boolean closed; | |
| private final List<AsyncEvent> registrations; | |
| private final List<AsyncTriggerEvent> deregistrations; | |
| private final Logger debug; | |
| private final Logger debugtimeout; | |
| private final HttpClientImpl owner; | |
| private final ConnectionPool pool; | |
| private final AtomicReference<Throwable> errorRef = new AtomicReference<>(); | |
| private final ReentrantLock lock = new ReentrantLock(); | |
| SelectorManager(HttpClientImpl ref) throws IOException { | |
| super(null, null, | |
| "HttpClient-" + ref.id + "-SelectorManager", | |
| 0, false); | |
| owner = ref; | |
| debug = ref.debug; | |
| debugtimeout = ref.debugtimeout; | |
| pool = ref.connectionPool(); | |
| registrations = new ArrayList<>(); | |
| deregistrations = new ArrayList<>(); | |
| selector = Selector.open(); | |
| } | |
| IOException selectorClosedException() { | |
| var io = new IOException("selector manager closed"); | |
| var cause = errorRef.get(); | |
| if (cause != null) { | |
| io.initCause(cause); | |
| } | |
| return io; | |
| } | |
| void eventUpdated(AsyncEvent e) throws ClosedChannelException { | |
| if (Thread.currentThread() == this) { | |
| SelectionKey key = e.channel().keyFor(selector); | |
| if (key != null && key.isValid()) { | |
| SelectorAttachment sa = (SelectorAttachment) key.attachment(); | |
| sa.register(e); | |
| } else if (e.interestOps() != 0) { | |
| // We don't care about paused events. | |
| // These are actually handled by | |
| // SelectorAttachment::resetInterestOps later on. | |
| // But if we reach here when trying to resume an | |
| // event then it's better to fail fast. | |
| if (debug.on()) debug.log("No key for channel"); | |
| e.abort(new IOException("No key for channel")); | |
| } | |
| } else { | |
| register(e); | |
| } | |
| } | |
| // This returns immediately. So caller not allowed to send/receive | |
| // on connection. | |
| void register(AsyncEvent e) { | |
| var closed = this.closed; | |
| if (!closed) { | |
| lock.lock(); | |
| try { | |
| closed = this.closed; | |
| if (!closed) { | |
| registrations.add(e); | |
| } | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| if (closed) { | |
| e.abort(selectorClosedException()); | |
| } else { | |
| selector.wakeup(); | |
| } | |
| } | |
| void wakeupSelector() { | |
| selector.wakeup(); | |
| } | |
| void abort(Throwable t) { | |
| boolean closed = this.closed; | |
| errorRef.compareAndSet(null, t); | |
| if (debug.on()) { | |
| debug.log("aborting selector manager(closed=%s): " + t, closed); | |
| } | |
| t = errorRef.get(); | |
| boolean inSelectorThread = owner.isSelectorThread(); | |
| if (!inSelectorThread) { | |
| // abort anything pending, then close | |
| abortPendingRequests(owner, t); | |
| } | |
| Set<SelectionKey> keys = new HashSet<>(); | |
| Set<AsyncEvent> toAbort = new HashSet<>(); | |
| lock.lock(); | |
| try { | |
| if (closed = this.closed) return; | |
| this.closed = true; | |
| try { | |
| keys.addAll(selector.keys()); | |
| } catch (ClosedSelectorException ce) { | |
| // OK - nothing to do... | |
| } | |
| toAbort.addAll(this.registrations); | |
| toAbort.addAll(this.deregistrations); | |
| this.registrations.clear(); | |
| this.deregistrations.clear(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| // double check after closing | |
| abortPendingRequests(owner, t); | |
| IOException io = toAbort.isEmpty() | |
| ? null : selectorClosedException(); | |
| for (AsyncEvent e : toAbort) { | |
| try { | |
| e.abort(io); | |
| } catch (Throwable x) { | |
| debug.log("Failed to abort event: " + x); | |
| } | |
| } | |
| if (!inSelectorThread) selector.wakeup(); | |
| } | |
| // Only called by the selector manager thread | |
| private void shutdown() { | |
| try { | |
| lock.lock(); | |
| try { | |
| Log.logTrace("{0}: shutting down", getName()); | |
| if (debug.on()) debug.log("SelectorManager shutting down"); | |
| closed = true; | |
| selector.close(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } catch (IOException ignored) { | |
| } finally { | |
| owner.stop(); | |
| } | |
| } | |
| boolean isClosed() { | |
| return closed; | |
| } | |
| @Override | |
| public void run() { | |
| List<Pair<AsyncEvent,IOException>> errorList = new ArrayList<>(); | |
| List<AsyncEvent> readyList = new ArrayList<>(); | |
| List<Runnable> resetList = new ArrayList<>(); | |
| owner.isAlive.set(true); // goes back to false when run exits | |
| owner.isStarted.set(true); // never goes back to false | |
| try { | |
| if (Log.channel()) Log.logChannel(getName() + ": starting"); | |
| while (!Thread.currentThread().isInterrupted() && !closed) { | |
| lock.lock(); | |
| try { | |
| assert errorList.isEmpty(); | |
| assert readyList.isEmpty(); | |
| assert resetList.isEmpty(); | |
| for (AsyncTriggerEvent event : deregistrations) { | |
| event.handle(); | |
| } | |
| deregistrations.clear(); | |
| for (AsyncEvent event : registrations) { | |
| if (event instanceof AsyncTriggerEvent) { | |
| readyList.add(event); | |
| continue; | |
| } | |
| SelectableChannel chan = event.channel(); | |
| SelectionKey key = null; | |
| try { | |
| key = chan.keyFor(selector); | |
| SelectorAttachment sa; | |
| if (key == null || !key.isValid()) { | |
| if (key != null) { | |
| // key is canceled. | |
| // invoke selectNow() to purge it | |
| // before registering the new event. | |
| selector.selectNow(); | |
| } | |
| sa = new SelectorAttachment(chan, selector); | |
| } else { | |
| sa = (SelectorAttachment) key.attachment(); | |
| } | |
| // may throw IOE if channel closed: that's OK | |
| sa.register(event); | |
| if (!chan.isOpen()) { | |
| throw new ClosedChannelException(); | |
| } | |
| } catch (IOException e) { | |
| Log.logTrace("{0}: {1}", getName(), e); | |
| if (debug.on()) | |
| debug.log("Got " + e.getClass().getName() | |
| + " while handling registration events"); | |
| chan.close(); | |
| // let the event abort deal with it | |
| errorList.add(new Pair<>(event, e)); | |
| if (key != null) { | |
| key.cancel(); | |
| selector.selectNow(); | |
| } | |
| } | |
| } | |
| registrations.clear(); | |
| selector.selectedKeys().clear(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| for (AsyncEvent event : readyList) { | |
| assert event instanceof AsyncTriggerEvent; | |
| event.handle(); | |
| } | |
| readyList.clear(); | |
| for (Pair<AsyncEvent,IOException> error : errorList) { | |
| // an IOException was raised and the channel closed. | |
| handleEvent(error.first, error.second); | |
| } | |
| errorList.clear(); | |
| // Check whether client is still alive, and if not, | |
| // gracefully stop this thread | |
| if (owner.finished()) { | |
| Log.logTrace("{0}: {1}", | |
| getName(), | |
| "HttpClient finished. Exiting..."); | |
| return; | |
| } | |
| // Timeouts will have milliseconds granularity. It is important | |
| // to handle them in a timely fashion. | |
| long nextTimeout = owner.purgeTimeoutsAndReturnNextDeadline(); | |
| if (debugtimeout.on()) | |
| debugtimeout.log("next timeout: %d", nextTimeout); | |
| // Keep-alive have seconds granularity. It's not really an | |
| // issue if we keep connections linger a bit more in the keep | |
| // alive cache. | |
| long nextExpiry = pool.purgeExpiredConnectionsAndReturnNextDeadline(); | |
| if (debugtimeout.on()) | |
| debugtimeout.log("next expired: %d", nextExpiry); | |
| assert nextTimeout >= 0; | |
| assert nextExpiry >= 0; | |
| // Don't wait for ever as it might prevent the thread to | |
| // stop gracefully. millis will be 0 if no deadline was found. | |
| if (nextTimeout <= 0) nextTimeout = NODEADLINE; | |
| // Clip nextExpiry at NODEADLINE limit. The default | |
| // keep alive is 1200 seconds (half an hour) - we don't | |
| // want to wait that long. | |
| if (nextExpiry <= 0) nextExpiry = NODEADLINE; | |
| else nextExpiry = Math.min(NODEADLINE, nextExpiry); | |
| // takes the least of the two. | |
| long millis = Math.min(nextExpiry, nextTimeout); | |
| if (debugtimeout.on()) | |
| debugtimeout.log("Next deadline is %d", | |
| (millis == 0 ? NODEADLINE : millis)); | |
| //debugPrint(selector); | |
| int n = selector.select(millis == 0 ? NODEADLINE : millis); | |
| if (n == 0) { | |
| // Check whether client is still alive, and if not, | |
| // gracefully stop this thread | |
| if (owner.finished()) { | |
| Log.logTrace("{0}: {1}", | |
| getName(), | |
| "HttpClient finished. Exiting..."); | |
| return; | |
| } | |
| owner.purgeTimeoutsAndReturnNextDeadline(); | |
| continue; | |
| } | |
| Set<SelectionKey> keys = selector.selectedKeys(); | |
| assert errorList.isEmpty(); | |
| for (SelectionKey key : keys) { | |
| SelectorAttachment sa = (SelectorAttachment) key.attachment(); | |
| if (!key.isValid()) { | |
| IOException ex = sa.chan.isOpen() | |
| ? new IOException("Invalid key") | |
| : new ClosedChannelException(); | |
| sa.pending.forEach(e -> errorList.add(new Pair<>(e,ex))); | |
| sa.pending.clear(); | |
| continue; | |
| } | |
| int eventsOccurred; | |
| try { | |
| eventsOccurred = key.readyOps(); | |
| } catch (CancelledKeyException ex) { | |
| IOException io = Utils.getIOException(ex); | |
| sa.pending.forEach(e -> errorList.add(new Pair<>(e,io))); | |
| sa.pending.clear(); | |
| continue; | |
| } | |
| sa.events(eventsOccurred).forEach(readyList::add); | |
| resetList.add(() -> sa.resetInterestOps(eventsOccurred)); | |
| } | |
| selector.selectNow(); // complete cancellation | |
| selector.selectedKeys().clear(); | |
| // handle selected events | |
| IOException ioe = closed ? selectorClosedException() : null; | |
| readyList.forEach((e) -> handleEvent(e, ioe)); | |
| readyList.clear(); | |
| // handle errors (closed channels etc...) | |
| errorList.forEach((p) -> handleEvent(p.first, p.second)); | |
| errorList.clear(); | |
| // reset interest ops for selected channels | |
| resetList.forEach(r -> r.run()); | |
| resetList.clear(); | |
| } | |
| } catch (Throwable e) { | |
| errorRef.compareAndSet(null, e); | |
| if (!closed) { | |
| closed = true; // set closed early so that new requests are rejected | |
| // This terminates thread. So, better just print stack trace | |
| String err = Utils.stackTrace(e); | |
| Log.logError("{0}: {1}: {2}", getName(), | |
| "HttpClientImpl shutting down due to fatal error", err); | |
| } | |
| abortPendingRequests(owner, selectorClosedException()); | |
| if (debug.on()) debug.log("shutting down", e); | |
| if (Utils.ASSERTIONSENABLED && !debug.on()) { | |
| e.printStackTrace(System.err); // always print the stack | |
| } | |
| } finally { | |
| if (Log.channel()) Log.logChannel(getName() + ": stopping"); | |
| try { | |
| shutdown(); | |
| } finally { | |
| owner.isAlive.set(false); | |
| } | |
| } | |
| } | |
| // void debugPrint(Selector selector) { | |
| // System.err.println("Selector: debugprint start"); | |
| // Set<SelectionKey> keys = selector.keys(); | |
| // for (SelectionKey key : keys) { | |
| // SelectableChannel c = key.channel(); | |
| // int ops = key.interestOps(); | |
| // System.err.printf("selector chan:%s ops:%d\n", c, ops); | |
| // } | |
| // System.err.println("Selector: debugprint end"); | |
| // } | |
| /** Handles the given event. The given ioe may be null. */ | |
| void handleEvent(AsyncEvent event, IOException ioe) { | |
| if (ioe == null && closed) { | |
| ioe = selectorClosedException(); | |
| } | |
| if (ioe != null) { | |
| event.abort(ioe); | |
| } else { | |
| event.handle(); | |
| } | |
| } | |
| void lock() { | |
| lock.lock(); | |
| } | |
| void unlock() { | |
| lock.unlock(); | |
| } | |
| } | |
| final String debugInterestOps(SelectableChannel channel) { | |
| try { | |
| SelectionKey key = channel.keyFor(selmgr.selector); | |
| if (key == null) return "channel not registered with selector"; | |
| String keyInterestOps = key.isValid() | |
| ? "key.interestOps=" + Utils.interestOps(key) : "invalid key"; | |
| return String.format("channel registered with selector, %s, sa.interestOps=%s", | |
| keyInterestOps, | |
| Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); | |
| } catch (Throwable t) { | |
| return String.valueOf(t); | |
| } | |
| } | |
| /** | |
| * Tracks multiple user level registrations associated with one NIO | |
| * registration (SelectionKey). In this implementation, registrations | |
| * are one-off and when an event is posted the registration is cancelled | |
| * until explicitly registered again. | |
| * | |
| * <p> No external synchronization required as this class is only used | |
| * by the SelectorManager thread. One of these objects required per | |
| * connection. | |
| */ | |
| private static class SelectorAttachment { | |
| private final SelectableChannel chan; | |
| private final Selector selector; | |
| private final Set<AsyncEvent> pending; | |
| private static final Logger debug = | |
| Utils.getDebugLogger("SelectorAttachment"::toString, Utils.DEBUG); | |
| private int interestOps; | |
| SelectorAttachment(SelectableChannel chan, Selector selector) { | |
| this.pending = new HashSet<>(); | |
| this.chan = chan; | |
| this.selector = selector; | |
| } | |
| void register(AsyncEvent e) throws ClosedChannelException { | |
| int newOps = e.interestOps(); | |
| // re register interest if we are not already interested | |
| // in the event. If the event is paused, then the pause will | |
| // be taken into account later when resetInterestOps is called. | |
| boolean reRegister = (interestOps & newOps) != newOps; | |
| interestOps |= newOps; | |
| pending.add(e); | |
| if (debug.on()) | |
| debug.log("Registering %s for %s (%s)", | |
| e, Utils.describeOps(newOps), reRegister); | |
| if (reRegister) { | |
| // first time registration happens here also | |
| try { | |
| chan.register(selector, interestOps, this); | |
| } catch (Throwable x) { | |
| abortPending(x); | |
| } | |
| } else if (!chan.isOpen()) { | |
| abortPending(new ClosedChannelException()); | |
| } | |
| } | |
| /** | |
| * Returns a Stream<AsyncEvents> containing only events that are | |
| * registered with the given {@code interestOps}. | |
| */ | |
| Stream<AsyncEvent> events(int interestOps) { | |
| return pending.stream() | |
| .filter(ev -> (ev.interestOps() & interestOps) != 0); | |
| } | |
| /** | |
| * Removes any events with the given {@code interestOps}, and if no | |
| * events remaining, cancels the associated SelectionKey. | |
| */ | |
| void resetInterestOps(int interestOps) { | |
| int newOps = 0; | |
| Iterator<AsyncEvent> itr = pending.iterator(); | |
| while (itr.hasNext()) { | |
| AsyncEvent event = itr.next(); | |
| int evops = event.interestOps(); | |
| if (event.repeating()) { | |
| newOps |= evops; | |
| continue; | |
| } | |
| if ((evops & interestOps) != 0) { | |
| itr.remove(); | |
| } else { | |
| newOps |= evops; | |
| } | |
| } | |
| this.interestOps = newOps; | |
| SelectionKey key = chan.keyFor(selector); | |
| if (newOps == 0 && key != null && pending.isEmpty()) { | |
| key.cancel(); | |
| } else { | |
| try { | |
| if (key == null || !key.isValid()) { | |
| throw new CancelledKeyException(); | |
| } | |
| key.interestOps(newOps); | |
| // double check after | |
| if (!chan.isOpen()) { | |
| abortPending(new ClosedChannelException()); | |
| return; | |
| } | |
| assert key.interestOps() == newOps; | |
| } catch (CancelledKeyException x) { | |
| // channel may have been closed | |
| if (debug.on()) debug.log("key cancelled for " + chan); | |
| abortPending(x); | |
| } | |
| } | |
| } | |
| void abortPending(Throwable x) { | |
| if (!pending.isEmpty()) { | |
| AsyncEvent[] evts = pending.toArray(new AsyncEvent[0]); | |
| pending.clear(); | |
| IOException io = Utils.getIOException(x); | |
| for (AsyncEvent event : evts) { | |
| event.abort(io); | |
| } | |
| } | |
| } | |
| } | |
| /*package-private*/ SSLContext theSSLContext() { | |
| return sslContext; | |
| } | |
| @Override | |
| public SSLContext sslContext() { | |
| return sslContext; | |
| } | |
| @Override | |
| public SSLParameters sslParameters() { | |
| return Utils.copySSLParameters(sslParams); | |
| } | |
| @Override | |
| public Optional<Authenticator> authenticator() { | |
| return Optional.ofNullable(authenticator); | |
| } | |
| /*package-private*/ final DelegatingExecutor theExecutor() { | |
| return delegatingExecutor; | |
| } | |
| @Override | |
| public final Optional<Executor> executor() { | |
| return isDefaultExecutor | |
| ? Optional.empty() | |
| : Optional.of(delegatingExecutor.delegate()); | |
| } | |
| ConnectionPool connectionPool() { | |
| return connections; | |
| } | |
| @Override | |
| public Redirect followRedirects() { | |
| return followRedirects; | |
| } | |
| @Override | |
| public Optional<CookieHandler> cookieHandler() { | |
| return Optional.ofNullable(cookieHandler); | |
| } | |
| @Override | |
| public Optional<Duration> connectTimeout() { | |
| return Optional.ofNullable(connectTimeout); | |
| } | |
| Optional<Duration> idleConnectionTimeout() { | |
| return Optional.ofNullable(getIdleConnectionTimeout()); | |
| } | |
| @Override | |
| public Optional<ProxySelector> proxy() { | |
| return Optional.ofNullable(userProxySelector); | |
| } | |
| // Return the effective proxy that this client uses. | |
| ProxySelector proxySelector() { | |
| return proxySelector; | |
| } | |
| @Override | |
| public WebSocket.Builder newWebSocketBuilder() { | |
| // Make sure to pass the HttpClientFacade to the WebSocket builder. | |
| // This will ensure that the facade is not released before the | |
| // WebSocket has been created, at which point the pendingOperationCount | |
| // will have been incremented by the RawChannelTube. | |
| // See RawChannelTube. | |
| return new BuilderImpl(this.facade(), proxySelector); | |
| } | |
| @Override | |
| public Version version() { | |
| return version; | |
| } | |
| InetAddress localAddress() { | |
| return localAddr; | |
| } | |
| String dbgString() { | |
| return dbgTag; | |
| } | |
| @Override | |
| public String toString() { | |
| // Used by tests to get the client's id and compute the | |
| // name of the SelectorManager thread. | |
| return super.toString() + ("(" + id + ")"); | |
| } | |
| private void initFilters() { | |
| addFilter(AuthenticationFilter.class); | |
| addFilter(RedirectFilter.class); | |
| if (this.cookieHandler != null) { | |
| addFilter(CookieFilter.class); | |
| } | |
| } | |
| private void addFilter(Class<? extends HeaderFilter> f) { | |
| filters.addFilter(f); | |
| } | |
| final List<HeaderFilter> filterChain() { | |
| return filters.getFilterChain(); | |
| } | |
| // Timer controls. | |
| // Timers are implemented through timed Selector.select() calls. | |
| void registerTimer(TimeoutEvent event) { | |
| Log.logTrace("Registering timer {0}", event); | |
| synchronized (this) { | |
| timeouts.add(event); | |
| selmgr.wakeupSelector(); | |
| } | |
| } | |
| void cancelTimer(TimeoutEvent event) { | |
| Log.logTrace("Canceling timer {0}", event); | |
| synchronized (this) { | |
| timeouts.remove(event); | |
| } | |
| } | |
| /** | |
| * Purges ( handles ) timer events that have passed their deadline, and | |
| * returns the amount of time, in milliseconds, until the next earliest | |
| * event. A return value of 0 means that there are no events. | |
| */ | |
| private long purgeTimeoutsAndReturnNextDeadline() { | |
| long diff = 0L; | |
| List<TimeoutEvent> toHandle = null; | |
| int remaining = 0; | |
| // enter critical section to retrieve the timeout event to handle | |
| synchronized (this) { | |
| if (timeouts.isEmpty()) return 0L; | |
| Deadline now = TimeSource.now(); | |
| Iterator<TimeoutEvent> itr = timeouts.iterator(); | |
| while (itr.hasNext()) { | |
| TimeoutEvent event = itr.next(); | |
| diff = now.until(event.deadline(), ChronoUnit.MILLIS); | |
| if (diff <= 0) { | |
| itr.remove(); | |
| toHandle = (toHandle == null) ? new ArrayList<>() : toHandle; | |
| toHandle.add(event); | |
| } else { | |
| break; | |
| } | |
| } | |
| remaining = timeouts.size(); | |
| } | |
| // can be useful for debugging | |
| if (toHandle != null && Log.trace()) { | |
| Log.logTrace("purgeTimeoutsAndReturnNextDeadline: handling " | |
| + toHandle.size() + " events, " | |
| + "remaining " + remaining | |
| + ", next deadline: " + (diff < 0 ? 0L : diff)); | |
| } | |
| // handle timeout events out of critical section | |
| if (toHandle != null) { | |
| Throwable failed = null; | |
| for (TimeoutEvent event : toHandle) { | |
| try { | |
| Log.logTrace("Firing timer {0}", event); | |
| event.handle(); | |
| } catch (Error | RuntimeException e) { | |
| // Not expected. Handle remaining events then throw... | |
| // If e is an OOME or SOE it might simply trigger a new | |
| // error from here - but in this case there's not much we | |
| // could do anyway. Just let it flow... | |
| if (failed == null) failed = e; | |
| else failed.addSuppressed(e); | |
| Log.logTrace("Failed to handle event {0}: {1}", event, e); | |
| } | |
| } | |
| if (failed instanceof Error) throw (Error) failed; | |
| if (failed instanceof RuntimeException) throw (RuntimeException) failed; | |
| } | |
| // return time to wait until next event. 0L if there's no more events. | |
| return diff < 0 ? 0L : diff; | |
| } | |
| // used for the connection window | |
| int getReceiveBufferSize() { | |
| return Utils.getIntegerNetProperty( | |
| "jdk.httpclient.receiveBufferSize", | |
| 0 // only set the size if > 0 | |
| ); | |
| } | |
| // used for testing | |
| int getSendBufferSize() { | |
| return Utils.getIntegerNetProperty( | |
| "jdk.httpclient.sendBufferSize", | |
| 0 // only set the size if > 0 | |
| ); | |
| } | |
| // Optimization for reading SSL encrypted data | |
| // -------------------------------------------- | |
| // Returns a BufferSupplier that can be used for reading | |
| // encrypted bytes of the channel. These buffers can then | |
| // be recycled by the SSLFlowDelegate::Reader after their | |
| // content has been copied in the SSLFlowDelegate::Reader | |
| // readBuf. | |
| // Because allocating, reading, copying, and recycling | |
| // all happen in the SelectorManager thread, | |
| // then this BufferSupplier can be shared between all | |
| // the SSL connections managed by this client. | |
| BufferSupplier getSSLBufferSupplier() { | |
| return sslBufferSupplier; | |
| } | |
| private Duration getIdleConnectionTimeout() { | |
| if (IDLE_CONNECTION_TIMEOUT >= 0) | |
| return Duration.ofSeconds(IDLE_CONNECTION_TIMEOUT); | |
| return null; | |
| } | |
| private static long getTimeoutProp(String prop, long def) { | |
| String s = Utils.getNetProperty(prop); | |
| try { | |
| if (s != null) { | |
| long timeoutVal = Long.parseLong(s); | |
| if (timeoutVal >= 0) return timeoutVal; | |
| } | |
| } catch (NumberFormatException ignored) { | |
| Log.logTrace("Invalid value set for " + prop + " property: " + ignored); | |
| } | |
| return def; | |
| } | |
| // An implementation of BufferSupplier that manages a pool of | |
| // maximum 3 direct byte buffers (SocketTube.MAX_BUFFERS) that | |
| // are used for reading encrypted bytes off the channel before | |
| // copying and subsequent unwrapping. | |
| private static final class SSLDirectBufferSupplier implements BufferSupplier { | |
| private static final int POOL_SIZE = SocketTube.MAX_BUFFERS; | |
| private final ByteBuffer[] pool = new ByteBuffer[POOL_SIZE]; | |
| private final HttpClientImpl client; | |
| private final Logger debug; | |
| private int tail, count; // no need for volatile: only accessed in SM thread. | |
| SSLDirectBufferSupplier(HttpClientImpl client) { | |
| this.client = Objects.requireNonNull(client); | |
| this.debug = client.debug; | |
| } | |
| // Gets a buffer from the pool, or allocates a new one if needed. | |
| @Override | |
| public ByteBuffer get() { | |
| assert client.isSelectorThread(); | |
| assert tail <= POOL_SIZE : "allocate tail is " + tail; | |
| ByteBuffer buf; | |
| if (tail == 0) { | |
| if (debug.on()) { | |
| // should not appear more than SocketTube.MAX_BUFFERS | |
| debug.log("ByteBuffer.allocateDirect(%d)", Utils.BUFSIZE); | |
| } | |
| assert count++ < POOL_SIZE : "trying to allocate more than " | |
| + POOL_SIZE + " buffers"; | |
| buf = ByteBuffer.allocateDirect(Utils.BUFSIZE); | |
| } else { | |
| assert tail > 0 : "non positive tail value: " + tail; | |
| tail--; | |
| buf = pool[tail]; | |
| pool[tail] = null; | |
| } | |
| assert buf.isDirect(); | |
| assert buf.position() == 0; | |
| assert buf.hasRemaining(); | |
| assert buf.limit() == Utils.BUFSIZE; | |
| assert tail < POOL_SIZE; | |
| assert tail >= 0; | |
| return buf; | |
| } | |
| // Returns the given buffer to the pool. | |
| @Override | |
| public void recycle(ByteBuffer buffer) { | |
| assert client.isSelectorThread(); | |
| assert buffer.isDirect(); | |
| assert !buffer.hasRemaining(); | |
| assert tail < POOL_SIZE : "recycle tail is " + tail; | |
| assert tail >= 0; | |
| buffer.position(0); | |
| buffer.limit(buffer.capacity()); | |
| // don't fail if assertions are off. we have asserted above. | |
| if (tail < POOL_SIZE) { | |
| pool[tail] = buffer; | |
| tail++; | |
| } | |
| assert tail <= POOL_SIZE; | |
| assert tail > 0; | |
| } | |
| } | |
| } |