| /* | |
| * Copyright (c) 1996, 2024, 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 sun.net.www.http; | |
| import java.io.IOException; | |
| import java.io.NotSerializableException; | |
| import java.io.ObjectInputStream; | |
| import java.io.ObjectOutputStream; | |
| import java.net.URL; | |
| import java.util.ArrayDeque; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.concurrent.locks.ReentrantLock; | |
| import jdk.internal.misc.InnocuousThread; | |
| import sun.net.www.protocol.http.HttpURLConnection; | |
| import sun.util.logging.PlatformLogger; | |
| /** | |
| * A class that implements a cache of idle Http connections for keep-alive | |
| * | |
| * @author Stephen R. Pietrowicz (NCSA) | |
| * @author Dave Brown | |
| */ | |
| public class KeepAliveCache | |
| extends HashMap<KeepAliveKey, KeepAliveCache.ClientVector> | |
| implements Runnable { | |
| @java.io.Serial | |
| private static final long serialVersionUID = -2937172892064557949L; | |
| // Keep alive time set according to priority specified here: | |
| // 1. If server specifies a time with a Keep-Alive header | |
| // 2. If user specifies a time with system property below | |
| // 3. Default values which depend on proxy vs server and whether | |
| // a Connection: keep-alive header was sent by server | |
| // name suffixed with "server" or "proxy" | |
| private static final String keepAliveProp = "http.keepAlive.time."; | |
| private static final int userKeepAliveServer; | |
| private static final int userKeepAliveProxy; | |
| static final PlatformLogger logger = HttpURLConnection.getHttpLogger(); | |
| static int getUserKeepAliveSeconds(String type) { | |
| int v = Integer.getInteger(keepAliveProp+type, -1); | |
| return v < -1 ? -1 : v; | |
| } | |
| static { | |
| userKeepAliveServer = getUserKeepAliveSeconds("server"); | |
| userKeepAliveProxy = getUserKeepAliveSeconds("proxy"); | |
| } | |
| /* maximum # keep-alive connections to maintain at once | |
| * This should be 2 by the HTTP spec, but because we don't support pipe-lining | |
| * a larger value is more appropriate. So we now set a default of 5, and the value | |
| * refers to the number of idle connections per destination (in the cache) only. | |
| * It can be reset by setting system property "http.maxConnections". | |
| */ | |
| static final int MAX_CONNECTIONS = 5; | |
| static int result = -1; | |
| static int getMaxConnections() { | |
| if (result == -1) { | |
| result = Integer.getInteger("http.maxConnections", MAX_CONNECTIONS); | |
| if (result <= 0) { | |
| result = MAX_CONNECTIONS; | |
| } | |
| } | |
| return result; | |
| } | |
| static final int LIFETIME = 5000; | |
| // This class is never serialized (see writeObject/readObject). | |
| private final ReentrantLock cacheLock = new ReentrantLock(); | |
| @SuppressWarnings("serial") // Type of field is not Serializable | |
| private Thread keepAliveTimer = null; | |
| /** | |
| * Constructor | |
| */ | |
| public KeepAliveCache() {} | |
| /** | |
| * Register this URL and HttpClient (that supports keep-alive) with the cache | |
| * @param url The URL contains info about the host and port | |
| * @param http The HttpClient to be cached | |
| */ | |
| public void put(final URL url, Object obj, HttpClient http) { | |
| // this method may need to close an HttpClient, either because | |
| // it is not cacheable, or because the cache is at its capacity. | |
| // In the latter case, we close the least recently used client. | |
| // The client to close is stored in oldClient, and is closed | |
| // after cacheLock is released. | |
| HttpClient oldClient = null; | |
| cacheLock.lock(); | |
| try { | |
| boolean startThread = (keepAliveTimer == null); | |
| if (!startThread) { | |
| if (!keepAliveTimer.isAlive()) { | |
| startThread = true; | |
| } | |
| } | |
| if (startThread) { | |
| clear(); | |
| /* Unfortunately, we can't always believe the keep-alive timeout we got | |
| * back from the server. If I'm connected through a Netscape proxy | |
| * to a server that sent me a keep-alive | |
| * time of 15 sec, the proxy unilaterally terminates my connection | |
| * The robustness to get around this is in HttpClient.parseHTTP() | |
| */ | |
| final KeepAliveCache cache = this; | |
| keepAliveTimer = InnocuousThread.newSystemThread("Keep-Alive-Timer", cache); | |
| keepAliveTimer.setDaemon(true); | |
| keepAliveTimer.setPriority(Thread.MAX_PRIORITY - 2); | |
| keepAliveTimer.start(); | |
| } | |
| KeepAliveKey key = new KeepAliveKey(url, obj); | |
| ClientVector v = super.get(key); | |
| if (v == null) { | |
| int keepAliveTimeout = http.getKeepAliveTimeout(); | |
| if (keepAliveTimeout == 0) { | |
| keepAliveTimeout = getUserKeepAlive(http.getUsingProxy()); | |
| if (keepAliveTimeout == -1) { | |
| // same default for server and proxy | |
| keepAliveTimeout = 5; | |
| } | |
| } else if (keepAliveTimeout == -1) { | |
| keepAliveTimeout = getUserKeepAlive(http.getUsingProxy()); | |
| if (keepAliveTimeout == -1) { | |
| // different default for server and proxy | |
| keepAliveTimeout = http.getUsingProxy() ? 60 : 5; | |
| } | |
| } else if (keepAliveTimeout == -2) { | |
| keepAliveTimeout = 0; | |
| } | |
| // at this point keepAliveTimeout is the number of seconds to keep | |
| // alive, which could be 0, if the user specified 0 for the property | |
| assert keepAliveTimeout >= 0; | |
| if (keepAliveTimeout == 0) { | |
| oldClient = http; | |
| } else { | |
| v = new ClientVector(keepAliveTimeout * 1000); | |
| v.put(http); | |
| super.put(key, v); | |
| } | |
| } else { | |
| oldClient = v.put(http); | |
| } | |
| } finally { | |
| cacheLock.unlock(); | |
| } | |
| // close after releasing locks | |
| if (oldClient != null) { | |
| oldClient.closeServer(); | |
| } | |
| } | |
| // returns the keep alive set by user in system property or -1 if not set | |
| private static int getUserKeepAlive(boolean isProxy) { | |
| return isProxy ? userKeepAliveProxy : userKeepAliveServer; | |
| } | |
| /* called by a clientVector thread when all its connections have timed out | |
| * and that vector of connections should be removed. | |
| */ | |
| private void removeVector(KeepAliveKey k) { | |
| assert cacheLock.isHeldByCurrentThread(); | |
| super.remove(k); | |
| } | |
| /** | |
| * Check to see if this URL has a cached HttpClient | |
| */ | |
| public HttpClient get(URL url, Object obj) { | |
| cacheLock.lock(); | |
| try { | |
| KeepAliveKey key = new KeepAliveKey(url, obj); | |
| ClientVector v = super.get(key); | |
| if (v == null) { // nothing in cache yet | |
| return null; | |
| } | |
| return v.get(); | |
| } finally { | |
| cacheLock.unlock(); | |
| } | |
| } | |
| /* Sleeps for an allotted timeout, then checks for timed out connections. | |
| * Errs on the side of caution (leave connections idle for a relatively | |
| * short time). | |
| */ | |
| @Override | |
| public void run() { | |
| do { | |
| try { | |
| Thread.sleep(LIFETIME); | |
| } catch (InterruptedException e) {} | |
| List<HttpClient> closeList = null; | |
| // Remove all outdated HttpClients. | |
| cacheLock.lock(); | |
| try { | |
| if (isEmpty()) { | |
| // cache not used in the last LIFETIME - exit | |
| keepAliveTimer = null; | |
| break; | |
| } | |
| long currentTime = System.currentTimeMillis(); | |
| List<KeepAliveKey> keysToRemove = new ArrayList<>(); | |
| for (KeepAliveKey key : keySet()) { | |
| ClientVector v = get(key); | |
| KeepAliveEntry e = v.peekLast(); | |
| while (e != null) { | |
| if ((currentTime - e.idleStartTime) > v.nap) { | |
| v.pollLast(); | |
| if (closeList == null) { | |
| closeList = new ArrayList<>(); | |
| } | |
| closeList.add(e.hc); | |
| } else { | |
| break; | |
| } | |
| e = v.peekLast(); | |
| } | |
| if (v.isEmpty()) { | |
| keysToRemove.add(key); | |
| } | |
| } | |
| for (KeepAliveKey key : keysToRemove) { | |
| removeVector(key); | |
| } | |
| } finally { | |
| cacheLock.unlock(); | |
| // close connections outside cacheLock | |
| if (closeList != null) { | |
| for (HttpClient hc : closeList) { | |
| hc.closeServer(); | |
| } | |
| } | |
| } | |
| } while (keepAliveTimer == Thread.currentThread()); | |
| } | |
| /* | |
| * Do not serialize this class! | |
| */ | |
| @java.io.Serial | |
| private void writeObject(ObjectOutputStream stream) throws IOException { | |
| throw new NotSerializableException(); | |
| } | |
| @java.io.Serial | |
| private void readObject(ObjectInputStream stream) | |
| throws IOException, ClassNotFoundException | |
| { | |
| throw new NotSerializableException(); | |
| } | |
| /* LIFO order for reusing HttpClients. Most recent entries at the front. | |
| * If > maxConns are in use, discard oldest. | |
| */ | |
| class ClientVector extends ArrayDeque<KeepAliveEntry> { | |
| @java.io.Serial | |
| private static final long serialVersionUID = -8680532108106489459L; | |
| // sleep time in milliseconds, before cache clear | |
| int nap; | |
| ClientVector(int nap) { | |
| this.nap = nap; | |
| } | |
| /* return a still valid, idle HttpClient */ | |
| HttpClient get() { | |
| assert cacheLock.isHeldByCurrentThread(); | |
| // check the most recent connection, use if still valid | |
| KeepAliveEntry e = peekFirst(); | |
| if (e == null) { | |
| return null; | |
| } | |
| long currentTime = System.currentTimeMillis(); | |
| if ((currentTime - e.idleStartTime) > nap) { | |
| return null; // all connections stale - will be cleaned up later | |
| } else { | |
| pollFirst(); | |
| if (KeepAliveCache.logger.isLoggable(PlatformLogger.Level.FINEST)) { | |
| String msg = "cached HttpClient was idle for " | |
| + Long.toString(currentTime - e.idleStartTime); | |
| KeepAliveCache.logger.finest(msg); | |
| } | |
| return e.hc; | |
| } | |
| } | |
| HttpClient put(HttpClient h) { | |
| assert cacheLock.isHeldByCurrentThread(); | |
| HttpClient staleClient = null; | |
| assert KeepAliveCache.getMaxConnections() > 0; | |
| if (size() >= KeepAliveCache.getMaxConnections()) { | |
| // remove oldest connection | |
| staleClient = removeLast().hc; | |
| } | |
| addFirst(new KeepAliveEntry(h, System.currentTimeMillis())); | |
| // close after releasing the locks | |
| return staleClient; | |
| } | |
| /* | |
| * Do not serialize this class! | |
| */ | |
| @java.io.Serial | |
| private void writeObject(ObjectOutputStream stream) throws IOException { | |
| throw new NotSerializableException(); | |
| } | |
| @java.io.Serial | |
| private void readObject(ObjectInputStream stream) | |
| throws IOException, ClassNotFoundException { | |
| throw new NotSerializableException(); | |
| } | |
| } | |
| } | |
| class KeepAliveKey { | |
| private final String protocol; | |
| private final String host; | |
| private final int port; | |
| private final Object obj; // additional key, such as socketfactory | |
| /** | |
| * Constructor | |
| * | |
| * @param url the URL containing the protocol, host and port information | |
| */ | |
| public KeepAliveKey(URL url, Object obj) { | |
| this.protocol = url.getProtocol(); | |
| this.host = url.getHost(); | |
| this.port = url.getPort(); | |
| this.obj = obj; | |
| } | |
| /** | |
| * Determine whether or not two objects of this type are equal | |
| */ | |
| @Override | |
| public boolean equals(Object obj) { | |
| if (!(obj instanceof KeepAliveKey kae)) | |
| return false; | |
| return host.equals(kae.host) | |
| && (port == kae.port) | |
| && protocol.equals(kae.protocol) | |
| && this.obj == kae.obj; | |
| } | |
| /** | |
| * The hashCode() for this object is the string hashCode() of | |
| * concatenation of the protocol, host name and port. | |
| */ | |
| @Override | |
| public int hashCode() { | |
| String str = protocol+host+port; | |
| return this.obj == null ? str.hashCode() : | |
| str.hashCode() + this.obj.hashCode(); | |
| } | |
| } | |
| class KeepAliveEntry { | |
| final HttpClient hc; | |
| final long idleStartTime; | |
| KeepAliveEntry(HttpClient hc, long idleStartTime) { | |
| this.hc = hc; | |
| this.idleStartTime = idleStartTime; | |
| } | |
| } |