1 /*
2  * Copyright (C) 2015 Square, Inc.
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  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package okhttp3.internal.connection;
17
18 import java.io.IOException;
19 import java.net.Socket;
20 import java.util.List;
21 import okhttp3.Address;
22 import okhttp3.Call;
23 import okhttp3.EventListener;
24 import okhttp3.Interceptor;
25 import okhttp3.OkHttpClient;
26 import okhttp3.Route;
27 import okhttp3.internal.Util;
28 import okhttp3.internal.http.ExchangeCodec;
29
30 import static okhttp3.internal.Util.closeQuietly;
31
32 /**
33  * Attempts to find the connections for a sequence of exchanges. This uses the following strategies:
34  *
35  * <ol>
36  *   <li>If the current call already has a connection that can satisfy the request it is used.
37  *       Using the same connection for an initial exchange and its follow-ups may improve locality.
38  *
39  *   <li>If there is a connection in the pool that can satisfy the request it is used. Note that
40  *       it is possible for shared exchanges to make requests to different host names! See {@link
41  *       RealConnection#isEligible} for details.
42  *
43  *   <li>If there's no existing connection, make a list of routes (which may require blocking DNS
44  *       lookups) and attempt a new connection them. When failures occur, retries iterate the list
45  *       of available routes.
46  * </ol>
47  *
48  * <p>If the pool gains an eligible connection while DNS, TCP, or TLS work is in flight, this finder
49  * will prefer pooled connections. Only pooled HTTP/2 connections are used for such de-duplication.
50  *
51  * <p>It is possible to cancel the finding process.
52  */

53 final class ExchangeFinder {
54   private final Transmitter transmitter;
55   private final Address address;
56   private final RealConnectionPool connectionPool;
57   private final Call call;
58   private final EventListener eventListener;
59
60   private RouteSelector.Selection routeSelection;
61
62   // State guarded by connectionPool.
63   private final RouteSelector routeSelector;
64   private RealConnection connectingConnection;
65   private boolean hasStreamFailure;
66   private Route nextRouteToTry;
67
68   ExchangeFinder(Transmitter transmitter, RealConnectionPool connectionPool,
69       Address address, Call call, EventListener eventListener) {
70     this.transmitter = transmitter;
71     this.connectionPool = connectionPool;
72     this.address = address;
73     this.call = call;
74     this.eventListener = eventListener;
75     this.routeSelector = new RouteSelector(
76         address, connectionPool.routeDatabase, call, eventListener);
77   }
78
79   public ExchangeCodec find(
80       OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
81     int connectTimeout = chain.connectTimeoutMillis();
82     int readTimeout = chain.readTimeoutMillis();
83     int writeTimeout = chain.writeTimeoutMillis();
84     int pingIntervalMillis = client.pingIntervalMillis();
85     boolean connectionRetryEnabled = client.retryOnConnectionFailure();
86
87     try {
88       RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
89           writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
90       return resultConnection.newCodec(client, chain);
91     } catch (RouteException e) {
92       trackFailure();
93       throw e;
94     } catch (IOException e) {
95       trackFailure();
96       throw new RouteException(e);
97     }
98   }
99
100   /**
101    * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
102    * until a healthy connection is found.
103    */

104   private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
105       int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
106       boolean doExtensiveHealthChecks) throws IOException {
107     while (true) {
108       RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
109           pingIntervalMillis, connectionRetryEnabled);
110
111       // If this is a brand new connection, we can skip the extensive health checks.
112       synchronized (connectionPool) {
113         if (candidate.successCount == 0 && !candidate.isMultiplexed()) {
114           return candidate;
115         }
116       }
117
118       // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
119       // isn't, take it out of the pool and start again.
120       if (!candidate.isHealthy(doExtensiveHealthChecks)) {
121         candidate.noNewExchanges();
122         continue;
123       }
124
125       return candidate;
126     }
127   }
128
129   /**
130    * Returns a connection to host a new stream. This prefers the existing connection if it exists,
131    * then the pool, finally building a new connection.
132    */

133   private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
134       int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
135     boolean foundPooledConnection = false;
136     RealConnection result = null;
137     Route selectedRoute = null;
138     RealConnection releasedConnection;
139     Socket toClose;
140     synchronized (connectionPool) {
141       if (transmitter.isCanceled()) throw new IOException("Canceled");
142       hasStreamFailure = false// This is a fresh attempt.
143
144       // Attempt to use an already-allocated connection. We need to be careful here because our
145       // already-allocated connection may have been restricted from creating new exchanges.
146       releasedConnection = transmitter.connection;
147       toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
148           ? transmitter.releaseConnectionNoEvents()
149           : null;
150
151       if (transmitter.connection != null) {
152         // We had an already-allocated connection and it's good.
153         result = transmitter.connection;
154         releasedConnection = null;
155       }
156
157       if (result == null) {
158         // Attempt to get a connection from the pool.
159         if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, nullfalse)) {
160           foundPooledConnection = true;
161           result = transmitter.connection;
162         } else if (nextRouteToTry != null) {
163           selectedRoute = nextRouteToTry;
164           nextRouteToTry = null;
165         } else if (retryCurrentRoute()) {
166           selectedRoute = transmitter.connection.route();
167         }
168       }
169     }
170     closeQuietly(toClose);
171
172     if (releasedConnection != null) {
173       eventListener.connectionReleased(call, releasedConnection);
174     }
175     if (foundPooledConnection) {
176       eventListener.connectionAcquired(call, result);
177     }
178     if (result != null) {
179       // If we found an already-allocated or pooled connection, we're done.
180       return result;
181     }
182
183     // If we need a route selection, make one. This is a blocking operation.
184     boolean newRouteSelection = false;
185     if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
186       newRouteSelection = true;
187       routeSelection = routeSelector.next();
188     }
189
190     List<Route> routes = null;
191     synchronized (connectionPool) {
192       if (transmitter.isCanceled()) throw new IOException("Canceled");
193
194       if (newRouteSelection) {
195         // Now that we have a set of IP addresses, make another attempt at getting a connection from
196         // the pool. This could match due to connection coalescing.
197         routes = routeSelection.getAll();
198         if (connectionPool.transmitterAcquirePooledConnection(
199             address, transmitter, routes, false)) {
200           foundPooledConnection = true;
201           result = transmitter.connection;
202         }
203       }
204
205       if (!foundPooledConnection) {
206         if (selectedRoute == null) {
207           selectedRoute = routeSelection.next();
208         }
209
210         // Create a connection and assign it to this allocation immediately. This makes it possible
211         // for an asynchronous cancel() to interrupt the handshake we're about to do.
212         result = new RealConnection(connectionPool, selectedRoute);
213         connectingConnection = result;
214       }
215     }
216
217     // If we found a pooled connection on the 2nd time around, we're done.
218     if (foundPooledConnection) {
219       eventListener.connectionAcquired(call, result);
220       return result;
221     }
222
223     // Do TCP + TLS handshakes. This is a blocking operation.
224     result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
225         connectionRetryEnabled, call, eventListener);
226     connectionPool.routeDatabase.connected(result.route());
227
228     Socket socket = null;
229     synchronized (connectionPool) {
230       connectingConnection = null;
231       // Last attempt at connection coalescing, which only occurs if we attempted multiple
232       // concurrent connections to the same host.
233       if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
234         // We lost the race! Close the connection we created and return the pooled connection.
235         result.noNewExchanges = true;
236         socket = result.socket();
237         result = transmitter.connection;
238
239         // It's possible for us to obtain a coalesced connection that is immediately unhealthy. In
240         // that case we will retry the route we just successfully connected with.
241         nextRouteToTry = selectedRoute;
242       } else {
243         connectionPool.put(result);
244         transmitter.acquireConnectionNoEvents(result);
245       }
246     }
247     closeQuietly(socket);
248
249     eventListener.connectionAcquired(call, result);
250     return result;
251   }
252
253   RealConnection connectingConnection() {
254     assert (Thread.holdsLock(connectionPool));
255     return connectingConnection;
256   }
257
258   void trackFailure() {
259     assert (!Thread.holdsLock(connectionPool));
260     synchronized (connectionPool) {
261       hasStreamFailure = true// Permit retries.
262     }
263   }
264
265   /** Returns true if there is a failure that retrying might fix. */
266   boolean hasStreamFailure() {
267     synchronized (connectionPool) {
268       return hasStreamFailure;
269     }
270   }
271
272   /** Returns true if a current route is still good or if there are routes we haven't tried yet. */
273   boolean hasRouteToTry() {
274     synchronized (connectionPool) {
275       if (nextRouteToTry != null) {
276         return true;
277       }
278       if (retryCurrentRoute()) {
279         // Lock in the route because retryCurrentRoute() is racy and we don't want to call it twice.
280         nextRouteToTry = transmitter.connection.route();
281         return true;
282       }
283       return (routeSelection != null && routeSelection.hasNext())
284           || routeSelector.hasNext();
285     }
286   }
287
288   /**
289    * Return true if the route used for the current connection should be retried, even if the
290    * connection itself is unhealthy. The biggest gotcha here is that we shouldn't reuse routes from
291    * coalesced connections.
292    */

293   private boolean retryCurrentRoute() {
294     return transmitter.connection != null
295         && transmitter.connection.routeFailureCount == 0
296         && Util.sameConnection(transmitter.connection.route().address().url(), address.url());
297   }
298 }
299