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 com.squareup.okhttp.internal.http; 17 18 import com.squareup.okhttp.Address; 19 import com.squareup.okhttp.ConnectionPool; 20 import com.squareup.okhttp.Route; 21 import com.squareup.okhttp.internal.Internal; 22 import com.squareup.okhttp.internal.RouteDatabase; 23 import com.squareup.okhttp.internal.Util; 24 import com.squareup.okhttp.internal.io.RealConnection; 25 import java.io.IOException; 26 import java.io.InterruptedIOException; 27 import java.lang.ref.Reference; 28 import java.lang.ref.WeakReference; 29 import java.net.ProtocolException; 30 import java.net.SocketTimeoutException; 31 import java.security.cert.CertificateException; 32 import javax.net.ssl.SSLHandshakeException; 33 import javax.net.ssl.SSLPeerUnverifiedException; 34 import okio.Sink; 35 36 import static java.util.concurrent.TimeUnit.MILLISECONDS; 37 38 /** 39 * This class coordinates the relationship between three entities: 40 * 41 * <ul> 42 * <li><strong>Connections:</strong> physical socket connections to remote servers. These are 43 * potentially slow to establish so it is necessary to be able to cancel a connection 44 * currently being connected. 45 * <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on 46 * connections. Each connection has its own allocation limit, which defines how many 47 * concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream 48 * at a time, SPDY and HTTP/2 typically carry multiple. 49 * <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and 50 * its follow up requests. We prefer to keep all streams of a single call on the same 51 * connection for better behavior and locality. 52 * </ul> 53 * 54 * <p>Instances of this class act on behalf of the call, using one or more streams over one or 55 * more connections. This class has APIs to release each of the above resources: 56 * 57 * <ul> 58 * <li>{@link #noNewStreams()} prevents the connection from being used for new streams in the 59 * future. Use this after a {@code Connection: close} header, or when the connection may be 60 * inconsistent. 61 * <li>{@link #streamFinished streamFinished()} releases the active stream from this allocation. 62 * Note that only one stream may be active at a given time, so it is necessary to call {@link 63 * #streamFinished streamFinished()} before creating a subsequent stream with {@link 64 * #newStream newStream()}. 65 * <li>{@link #release()} removes the call's hold on the connection. Note that this won't 66 * immediately free the connection if there is a stream still lingering. That happens when a 67 * call is complete but its response body has yet to be fully consumed. 68 * </ul> 69 * 70 * <p>This class supports {@linkplain #cancel asynchronous canceling}. This is intended to have 71 * the smallest blast radius possible. If an HTTP/2 stream is active, canceling will cancel that 72 * stream but not the other streams sharing its connection. But if the TLS handshake is still in 73 * progress then canceling may break the entire connection. 74 */ 75 public final class StreamAllocation { 76 public final Address address; 77 private final ConnectionPool connectionPool; 78 79 // State guarded by connectionPool. 80 private RouteSelector routeSelector; 81 private RealConnection connection; 82 private boolean released; 83 private boolean canceled; 84 private HttpStream stream; 85 86 public StreamAllocation(ConnectionPool connectionPool, Address address) { 87 this.connectionPool = connectionPool; 88 this.address = address; 89 } 90 91 public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout, 92 boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) 93 throws RouteException, IOException { 94 try { 95 RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, 96 writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks); 97 98 HttpStream resultStream; 99 if (resultConnection.framedConnection != null) { 100 resultStream = new Http2xStream(this, resultConnection.framedConnection); 101 } else { 102 resultConnection.getSocket().setSoTimeout(readTimeout); 103 resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS); 104 resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS); 105 resultStream = new Http1xStream(this, resultConnection.source, resultConnection.sink); 106 } 107 108 synchronized (connectionPool) { 109 resultConnection.streamCount++; 110 stream = resultStream; 111 return resultStream; 112 } 113 } catch (IOException e) { 114 throw new RouteException(e); 115 } 116 } 117 118 /** 119 * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated 120 * until a healthy connection is found. 121 */ 122 private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, 123 int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) 124 throws IOException, RouteException { 125 while (true) { 126 RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, 127 connectionRetryEnabled); 128 129 // If this is a brand new connection, we can skip the extensive health checks. 130 synchronized (connectionPool) { 131 if (candidate.streamCount == 0) { 132 return candidate; 133 } 134 } 135 136 // Otherwise do a potentially-slow check to confirm that the pooled connection is still good. 137 if (candidate.isHealthy(doExtensiveHealthChecks)) { 138 return candidate; 139 } 140 141 connectionFailed(); 142 } 143 } 144 145 /** 146 * Returns a connection to host a new stream. This prefers the existing connection if it exists, 147 * then the pool, finally building a new connection. 148 */ 149 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, 150 boolean connectionRetryEnabled) throws IOException, RouteException { 151 synchronized (connectionPool) { 152 if (released) throw new IllegalStateException("released"); 153 if (stream != null) throw new IllegalStateException("stream != null"); 154 if (canceled) throw new IOException("Canceled"); 155 156 RealConnection allocatedConnection = this.connection; 157 if (allocatedConnection != null && !allocatedConnection.noNewStreams) { 158 return allocatedConnection; 159 } 160 161 // Attempt to get a connection from the pool. 162 RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this); 163 if (pooledConnection != null) { 164 this.connection = pooledConnection; 165 return pooledConnection; 166 } 167 168 // Attempt to create a connection. 169 if (routeSelector == null) { 170 routeSelector = new RouteSelector(address, routeDatabase()); 171 } 172 } 173 174 Route route = routeSelector.next(); 175 RealConnection newConnection = new RealConnection(route); 176 acquire(newConnection); 177 178 synchronized (connectionPool) { 179 Internal.instance.put(connectionPool, newConnection); 180 this.connection = newConnection; 181 if (canceled) throw new IOException("Canceled"); 182 } 183 184 newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(), 185 connectionRetryEnabled); 186 routeDatabase().connected(newConnection.getRoute()); 187 188 return newConnection; 189 } 190 191 public void streamFinished(HttpStream stream) { 192 synchronized (connectionPool) { 193 if (stream == null || stream != this.stream) { 194 throw new IllegalStateException("expected " + this.stream + " but was " + stream); 195 } 196 } 197 deallocate(false, false, true); 198 } 199 200 public HttpStream stream() { 201 synchronized (connectionPool) { 202 return stream; 203 } 204 } 205 206 private RouteDatabase routeDatabase() { 207 return Internal.instance.routeDatabase(connectionPool); 208 } 209 210 public synchronized RealConnection connection() { 211 return connection; 212 } 213 214 public void release() { 215 deallocate(false, true, false); 216 } 217 218 /** Forbid new streams from being created on the connection that hosts this allocation. */ 219 public void noNewStreams() { 220 deallocate(true, false, false); 221 } 222 223 /** 224 * Releases resources held by this allocation. If sufficient resources are allocated, the 225 * connection will be detached or closed. 226 */ 227 private void deallocate(boolean noNewStreams, boolean released, boolean streamFinished) { 228 RealConnection connectionToClose = null; 229 synchronized (connectionPool) { 230 if (streamFinished) { 231 this.stream = null; 232 } 233 if (released) { 234 this.released = true; 235 } 236 if (connection != null) { 237 if (noNewStreams) { 238 connection.noNewStreams = true; 239 } 240 if (this.stream == null && (this.released || connection.noNewStreams)) { 241 release(connection); 242 if (connection.streamCount > 0) { 243 routeSelector = null; 244 } 245 if (connection.allocations.isEmpty()) { 246 connection.idleAtNanos = System.nanoTime(); 247 if (Internal.instance.connectionBecameIdle(connectionPool, connection)) { 248 connectionToClose = connection; 249 } 250 } 251 connection = null; 252 } 253 } 254 } 255 if (connectionToClose != null) { 256 Util.closeQuietly(connectionToClose.getSocket()); 257 } 258 } 259 260 public void cancel() { 261 HttpStream streamToCancel; 262 RealConnection connectionToCancel; 263 synchronized (connectionPool) { 264 canceled = true; 265 streamToCancel = stream; 266 connectionToCancel = connection; 267 } 268 if (streamToCancel != null) { 269 streamToCancel.cancel(); 270 } else if (connectionToCancel != null) { 271 connectionToCancel.cancel(); 272 } 273 } 274 275 private void connectionFailed(IOException e) { 276 synchronized (connectionPool) { 277 if (routeSelector != null) { 278 if (connection.streamCount == 0) { 279 // Record the failure on a fresh route. 280 Route failedRoute = connection.getRoute(); 281 routeSelector.connectFailed(failedRoute, e); 282 } else { 283 // We saw a failure on a recycled connection, reset this allocation with a fresh route. 284 routeSelector = null; 285 } 286 } 287 } 288 connectionFailed(); 289 } 290 291 /** Finish the current stream and prevent new streams from being created. */ 292 public void connectionFailed() { 293 deallocate(true, false, true); 294 } 295 296 /** 297 * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to 298 * {@link #release} on the same connection. 299 */ 300 public void acquire(RealConnection connection) { 301 connection.allocations.add(new WeakReference<>(this)); 302 } 303 304 /** Remove this allocation from the connection's list of allocations. */ 305 private void release(RealConnection connection) { 306 for (int i = 0, size = connection.allocations.size(); i < size; i++) { 307 Reference<StreamAllocation> reference = connection.allocations.get(i); 308 if (reference.get() == this) { 309 connection.allocations.remove(i); 310 return; 311 } 312 } 313 throw new IllegalStateException(); 314 } 315 316 public boolean recover(RouteException e) { 317 // Android-changed: Canceled StreamAllocations can never recover http://b/33763156 318 if (canceled) { 319 return false; 320 } 321 if (connection != null) { 322 connectionFailed(e.getLastConnectException()); 323 } 324 325 if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt. 326 || !isRecoverable(e)) { 327 return false; 328 } 329 330 return true; 331 } 332 333 public boolean recover(IOException e, Sink requestBodyOut) { 334 if (connection != null) { 335 int streamCount = connection.streamCount; 336 connectionFailed(e); 337 338 if (streamCount == 1) { 339 // This isn't a recycled connection. 340 // TODO(jwilson): find a better way for this. 341 return false; 342 } 343 } 344 345 boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink; 346 if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt. 347 || !isRecoverable(e) 348 || !canRetryRequestBody) { 349 return false; 350 } 351 352 return true; 353 } 354 355 private boolean isRecoverable(IOException e) { 356 // If there was a protocol problem, don't recover. 357 if (e instanceof ProtocolException) { 358 return false; 359 } 360 361 // If there was an interruption or timeout, don't recover. 362 if (e instanceof InterruptedIOException) { 363 return false; 364 } 365 366 return true; 367 } 368 369 private boolean isRecoverable(RouteException e) { 370 // Problems with a route may mean the connection can be retried with a new route, or may 371 // indicate a client-side or server-side issue that should not be retried. To tell, we must look 372 // at the cause. 373 374 IOException ioe = e.getLastConnectException(); 375 376 // If there was a protocol problem, don't recover. 377 if (ioe instanceof ProtocolException) { 378 return false; 379 } 380 381 // If there was an interruption don't recover, but if there was a timeout 382 // we should try the next route (if there is one). 383 if (ioe instanceof InterruptedIOException) { 384 return ioe instanceof SocketTimeoutException; 385 } 386 387 // Look for known client-side or negotiation errors that are unlikely to be fixed by trying 388 // again with a different route. 389 if (ioe instanceof SSLHandshakeException) { 390 // If the problem was a CertificateException from the X509TrustManager, 391 // do not retry. 392 if (ioe.getCause() instanceof CertificateException) { 393 return false; 394 } 395 } 396 if (ioe instanceof SSLPeerUnverifiedException) { 397 // e.g. a certificate pinning error. 398 return false; 399 } 400 401 // An example of one we might want to retry with a different route is a problem connecting to a 402 // proxy and would manifest as a standard IOException. Unless it is one we know we should not 403 // retry, we return true and try a new route. 404 return true; 405 } 406 407 @Override public String toString() { 408 return address.toString(); 409 } 410 } 411