Home | History | Annotate | Download | only in http
      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