1 /*
2  * Copyright 2012-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15 package com.amazonaws.http;
16
17 import com.amazonaws.annotation.SdkInternalApi;
18 import com.amazonaws.annotation.SdkTestInternalApi;
19
20 import org.apache.commons.logging.Log;
21 import org.apache.commons.logging.LogFactory;
22 import org.apache.http.conn.HttpClientConnectionManager;
23
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.TimeUnit;
29
30 /**
31  * Daemon thread to periodically check connection pools for idle connections.
32  * <p/>
33  * Connections sitting around idle in the HTTP connection pool for too long will
34  * eventually be terminated by the AWS end of the connection, and will go into
35  * CLOSE_WAIT. If this happens, sockets will sit around in CLOSE_WAIT, still
36  * using resources on the client side to manage that socket. Many sockets stuck
37  * in CLOSE_WAIT can prevent the OS from creating new connections.
38  * <p/>
39  * This class closes idle connections before they can move into the CLOSE_WAIT
40  * state.
41  * <p/>
42  * This thread is important because by default, we disable Apache HttpClient's
43  * stale connection checking, so without this thread running in the background,
44  * cleaning up old/inactive HTTP connections, we'd see more IO exceptions when
45  * stale connections (i.e. closed on the AWS side) are left in the connection
46  * pool, and requests grab one of them to begin executing a request.
47  */

48 @SdkInternalApi
49 public final class IdleConnectionReaper extends Thread {
50
51     /**
52      * Shared log for any errors during connection reaping.
53      */

54     private static final Log LOG = LogFactory.getLog(IdleConnectionReaper.class);
55     /**
56      * The period between invocations of the idle connection reaper.
57      */

58     private static final int PERIOD_MILLISECONDS = 1000 * 60;
59
60     /**
61      * Legacy constant used when {@link #registerConnectionManager(HttpClientConnectionManager)} is called. New code paths should
62      * use {@link #registerConnectionManager(HttpClientConnectionManager, long)} and provide the max idle timeout for that
63      * particular connection manager.
64      */

65     @Deprecated
66     private static final int DEFAULT_MAX_IDLE_MILLIS = 1000 * 60;
67
68     private static final Map<HttpClientConnectionManager, Long> connectionManagers = new ConcurrentHashMap<HttpClientConnectionManager, Long>();
69     /**
70      * Singleton instance of the connection reaper.
71      */

72     private static volatile IdleConnectionReaper instance;
73     /**
74      * Set to true when shutting down the reaper;  Once set to truethis
75      * flag is never set back to false.
76      */

77     private volatile boolean shuttingDown;
78
79     /**
80      * Private constructor - singleton pattern.
81      */

82     private IdleConnectionReaper() {
83         super("java-sdk-http-connection-reaper");
84         setDaemon(true);
85     }
86
87     /**
88      * Registers the given connection manager with this reaper.
89      *
90      * @return true if the connection manager has been successfully registered; false otherwise.
91      * @deprecated By {@link #registerConnectionManager(HttpClientConnectionManager, long)}.
92      */

93     @Deprecated
94     public static boolean registerConnectionManager(HttpClientConnectionManager connectionManager) {
95         return registerConnectionManager(connectionManager, DEFAULT_MAX_IDLE_MILLIS);
96     }
97
98     /**
99      * Registers the given connection manager with this reaper;
100      *
101      * @param connectionManager Connection manager to register
102      * @param maxIdleInMs       Max idle connection timeout in milliseconds for this connection manager.
103      * @return true if the connection manager has been successfully registered; false otherwise.
104      */

105     public static boolean registerConnectionManager(HttpClientConnectionManager connectionManager, long maxIdleInMs) {
106         if (instance == null) {
107             synchronized (IdleConnectionReaper.class) {
108                 if (instance == null) {
109                     instance = new IdleConnectionReaper();
110                     instance.start();
111                 }
112             }
113         }
114         return connectionManagers.put(connectionManager, maxIdleInMs) == null;
115     }
116
117     /**
118      * Removes the given connection manager from this reaper,
119      * and shutting down the reaper if there is zero connection manager left.
120      *
121      * @return true if the connection manager has been successfully removed;
122      * false otherwise.
123      */

124     public static boolean removeConnectionManager(HttpClientConnectionManager connectionManager) {
125         boolean wasRemoved = connectionManagers.remove(connectionManager) != null;
126         if (connectionManagers.isEmpty()) {
127             shutdown();
128         }
129         return wasRemoved;
130     }
131
132     @SdkTestInternalApi
133     public static List<HttpClientConnectionManager> getRegisteredConnectionManagers() {
134         return new ArrayList<HttpClientConnectionManager>(connectionManagers.keySet());
135     }
136
137     /**
138      * Shuts down the thread, allowing the class and instance to be collected.
139      * <p/>
140      * Since this is a daemon thread, its running will not prevent JVM shutdown.
141      * It will, however, prevent this class from being unloaded or garbage
142      * collected, in the context of a long-running application, until it is
143      * interrupted. This method will stop the thread's execution and clear its
144      * state. Any use of a service client will cause the thread to be restarted.
145      *
146      * @return true if an actual shutdown has been made; false otherwise.
147      */

148     public static synchronized boolean shutdown() {
149         if (instance != null) {
150             instance.markShuttingDown();
151             instance.interrupt();
152             connectionManagers.clear();
153             instance = null;
154             return true;
155         }
156         return false;
157     }
158
159     /**
160      * For testing purposes.
161      * Returns the number of connection managers currently monitored by this
162      * reaper.
163      */

164     static int size() {
165         return connectionManagers.size();
166     }
167
168     private void markShuttingDown() {
169         shuttingDown = true;
170     }
171
172     @SuppressWarnings("unchecked")
173     @Override
174     public void run() {
175         while (!shuttingDown) {
176             try {
177                 for (Map.Entry<HttpClientConnectionManager, Long> entry : connectionManagers.entrySet()) {
178                     // When we release connections, the connection manager leaves them
179                     // open so they can be reused.  We want to close out any idle
180                     // connections so that they don't sit around in CLOSE_WAIT.
181                     try {
182                         entry.getKey().closeIdleConnections(entry.getValue(), TimeUnit.MILLISECONDS);
183                     } catch (Exception t) {
184                         LOG.warn("Unable to close idle connections", t);
185                     }
186                 }
187
188                 Thread.sleep(PERIOD_MILLISECONDS);
189             } catch (Throwable t) {
190                 LOG.debug("Reaper thread: ", t);
191             }
192         }
193
194         LOG.debug("Shutting down reaper thread.");
195     }
196 }