1/*
2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.incubator.http;
27
28import java.lang.ref.WeakReference;
29import java.net.InetSocketAddress;
30import java.util.HashMap;
31import java.util.LinkedList;
32import java.util.ListIterator;
33import java.util.Objects;
34import java.util.concurrent.atomic.AtomicLong;
35import java.util.concurrent.atomic.AtomicReference;
36import jdk.incubator.http.internal.common.Utils;
37
38/**
39 * Http 1.1 connection pool.
40 */
41final class ConnectionPool {
42
43    // These counters are used to distribute ids for debugging
44    // The ACTIVE_CLEANER_COUNTER will tell how many CacheCleaner
45    // are active at a given time. It will increase when a new
46    // CacheCleaner is started and decrease when it exits.
47    static final AtomicLong ACTIVE_CLEANER_COUNTER = new AtomicLong();
48    // The POOL_IDS_COUNTER increases each time a new ConnectionPool
49    // is created. It may wrap and become negative but will never be
50    // decremented.
51    static final AtomicLong POOL_IDS_COUNTER = new AtomicLong();
52    // The cleanerCounter is used to name cleaner threads within a
53    // a connection pool, and increments monotically.
54    // It may wrap and become negative but will never be
55    // decremented.
56    final AtomicLong cleanerCounter = new AtomicLong();
57
58    static final long KEEP_ALIVE = Utils.getIntegerNetProperty(
59            "jdk.httpclient.keepalive.timeout", 1200); // seconds
60
61    // Pools of idle connections
62
63    final HashMap<CacheKey,LinkedList<HttpConnection>> plainPool;
64    final HashMap<CacheKey,LinkedList<HttpConnection>> sslPool;
65    // A monotically increasing id for this connection pool.
66    // It may be negative (that's OK)
67    // Mostly used for debugging purposes when looking at thread dumps.
68    // Global scope.
69    final long poolID = POOL_IDS_COUNTER.incrementAndGet();
70    final AtomicReference<CacheCleaner> cleanerRef;
71
72    /**
73     * Entries in connection pool are keyed by destination address and/or
74     * proxy address:
75     * case 1: plain TCP not via proxy (destination only)
76     * case 2: plain TCP via proxy (proxy only)
77     * case 3: SSL not via proxy (destination only)
78     * case 4: SSL over tunnel (destination and proxy)
79     */
80    static class CacheKey {
81        final InetSocketAddress proxy;
82        final InetSocketAddress destination;
83
84        CacheKey(InetSocketAddress destination, InetSocketAddress proxy) {
85            this.proxy = proxy;
86            this.destination = destination;
87        }
88
89        @Override
90        public boolean equals(Object obj) {
91            if (obj == null) {
92                return false;
93            }
94            if (getClass() != obj.getClass()) {
95                return false;
96            }
97            final CacheKey other = (CacheKey) obj;
98            if (!Objects.equals(this.proxy, other.proxy)) {
99                return false;
100            }
101            if (!Objects.equals(this.destination, other.destination)) {
102                return false;
103            }
104            return true;
105        }
106
107        @Override
108        public int hashCode() {
109            return Objects.hash(proxy, destination);
110        }
111    }
112
113    static class ExpiryEntry {
114        final HttpConnection connection;
115        final long expiry; // absolute time in seconds of expiry time
116        ExpiryEntry(HttpConnection connection, long expiry) {
117            this.connection = connection;
118            this.expiry = expiry;
119        }
120    }
121
122    final LinkedList<ExpiryEntry> expiryList;
123
124    /**
125     * There should be one of these per HttpClient.
126     */
127    ConnectionPool() {
128        plainPool = new HashMap<>();
129        sslPool = new HashMap<>();
130        expiryList = new LinkedList<>();
131        cleanerRef = new AtomicReference<>();
132    }
133
134    void start() {
135    }
136
137    static CacheKey cacheKey(InetSocketAddress destination,
138                             InetSocketAddress proxy)
139    {
140        return new CacheKey(destination, proxy);
141    }
142
143    synchronized HttpConnection getConnection(boolean secure,
144                                              InetSocketAddress addr,
145                                              InetSocketAddress proxy) {
146        CacheKey key = new CacheKey(addr, proxy);
147        HttpConnection c = secure ? findConnection(key, sslPool)
148                                  : findConnection(key, plainPool);
149        //System.out.println ("getConnection returning: " + c);
150        return c;
151    }
152
153    /**
154     * Returns the connection to the pool.
155     */
156    synchronized void returnToPool(HttpConnection conn) {
157        if (conn instanceof PlainHttpConnection) {
158            putConnection(conn, plainPool);
159        } else {
160            putConnection(conn, sslPool);
161        }
162        addToExpiryList(conn);
163        //System.out.println("Return to pool: " + conn);
164    }
165
166    private HttpConnection
167    findConnection(CacheKey key,
168                   HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
169        LinkedList<HttpConnection> l = pool.get(key);
170        if (l == null || l.isEmpty()) {
171            return null;
172        } else {
173            HttpConnection c = l.removeFirst();
174            removeFromExpiryList(c);
175            return c;
176        }
177    }
178
179    /* called from cache cleaner only  */
180    private void
181    removeFromPool(HttpConnection c,
182                   HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
183        //System.out.println("cacheCleaner removing: " + c);
184        LinkedList<HttpConnection> l = pool.get(c.cacheKey());
185        assert l != null;
186        boolean wasPresent = l.remove(c);
187        assert wasPresent;
188    }
189
190    private void
191    putConnection(HttpConnection c,
192                  HashMap<CacheKey,LinkedList<HttpConnection>> pool) {
193        CacheKey key = c.cacheKey();
194        LinkedList<HttpConnection> l = pool.get(key);
195        if (l == null) {
196            l = new LinkedList<>();
197            pool.put(key, l);
198        }
199        l.add(c);
200    }
201
202    static String makeCleanerName(long poolId, long cleanerId) {
203        return "HTTP-Cache-cleaner-" + poolId + "-" + cleanerId;
204    }
205
206    // only runs while entries exist in cache
207    final static class CacheCleaner extends Thread {
208
209        volatile boolean stopping;
210        // A monotically increasing id. May wrap and become negative (that's OK)
211        // Mostly used for debugging purposes when looking at thread dumps.
212        // Scoped per connection pool.
213        final long cleanerID;
214        // A reference to the owning ConnectionPool.
215        // This reference's referent may become null if the HttpClientImpl
216        // that owns this pool is GC'ed.
217        final WeakReference<ConnectionPool> ownerRef;
218
219        CacheCleaner(ConnectionPool owner) {
220            this(owner, owner.cleanerCounter.incrementAndGet());
221        }
222
223        CacheCleaner(ConnectionPool owner, long cleanerID) {
224            super(null, null, makeCleanerName(owner.poolID, cleanerID), 0, false);
225            this.cleanerID = cleanerID;
226            this.ownerRef = new WeakReference<>(owner);
227            setDaemon(true);
228        }
229
230        synchronized boolean stopping() {
231            return stopping || ownerRef.get() == null;
232        }
233
234        synchronized void stopCleaner() {
235            stopping = true;
236        }
237
238        @Override
239        public void run() {
240            ACTIVE_CLEANER_COUNTER.incrementAndGet();
241            try {
242                while (!stopping()) {
243                    try {
244                        Thread.sleep(3000);
245                    } catch (InterruptedException e) {}
246                    ConnectionPool owner = ownerRef.get();
247                    if (owner == null) return;
248                    owner.cleanCache(this);
249                    owner = null;
250                }
251            } finally {
252                ACTIVE_CLEANER_COUNTER.decrementAndGet();
253            }
254        }
255    }
256
257    synchronized void removeFromExpiryList(HttpConnection c) {
258        if (c == null) {
259            return;
260        }
261        ListIterator<ExpiryEntry> li = expiryList.listIterator();
262        while (li.hasNext()) {
263            ExpiryEntry e = li.next();
264            if (e.connection.equals(c)) {
265                li.remove();
266                return;
267            }
268        }
269        CacheCleaner cleaner = this.cleanerRef.get();
270        if (expiryList.isEmpty() && cleaner != null) {
271            this.cleanerRef.compareAndSet(cleaner, null);
272            cleaner.stopCleaner();
273            cleaner.interrupt();
274        }
275    }
276
277    private void cleanCache(CacheCleaner cleaner) {
278        long now = System.currentTimeMillis() / 1000;
279        LinkedList<HttpConnection> closelist = new LinkedList<>();
280
281        synchronized (this) {
282            ListIterator<ExpiryEntry> li = expiryList.listIterator();
283            while (li.hasNext()) {
284                ExpiryEntry entry = li.next();
285                if (entry.expiry <= now) {
286                    li.remove();
287                    HttpConnection c = entry.connection;
288                    closelist.add(c);
289                    if (c instanceof PlainHttpConnection) {
290                        removeFromPool(c, plainPool);
291                    } else {
292                        removeFromPool(c, sslPool);
293                    }
294                }
295            }
296            if (expiryList.isEmpty() && cleaner != null) {
297                this.cleanerRef.compareAndSet(cleaner, null);
298                cleaner.stopCleaner();
299            }
300        }
301        for (HttpConnection c : closelist) {
302            //System.out.println ("KAC: closing " + c);
303            c.close();
304        }
305    }
306
307    private synchronized void addToExpiryList(HttpConnection conn) {
308        long now = System.currentTimeMillis() / 1000;
309        long then = now + KEEP_ALIVE;
310        if (expiryList.isEmpty()) {
311            CacheCleaner cleaner = new CacheCleaner(this);
312            if (this.cleanerRef.compareAndSet(null, cleaner)) {
313                cleaner.start();
314            }
315            expiryList.add(new ExpiryEntry(conn, then));
316            return;
317        }
318
319        ListIterator<ExpiryEntry> li = expiryList.listIterator();
320        while (li.hasNext()) {
321            ExpiryEntry entry = li.next();
322
323            if (then > entry.expiry) {
324                li.previous();
325                // insert here
326                li.add(new ExpiryEntry(conn, then));
327                return;
328            }
329        }
330        // first element of list
331        expiryList.add(new ExpiryEntry(conn, then));
332    }
333}
334