Home | History | Annotate | Download | only in okhttp
      1 /*
      2  * Copyright (C) 2013 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;
     17 
     18 import com.squareup.okhttp.internal.Internal;
     19 import com.squareup.okhttp.internal.RecordingHostnameVerifier;
     20 import com.squareup.okhttp.internal.SslContextBuilder;
     21 import com.squareup.okhttp.internal.Util;
     22 import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
     23 import com.squareup.okhttp.internal.http.RecordingProxySelector;
     24 import com.squareup.okhttp.mockwebserver.MockWebServer;
     25 import java.io.IOException;
     26 import java.net.InetAddress;
     27 import java.net.InetSocketAddress;
     28 import java.net.Proxy;
     29 import java.util.Arrays;
     30 import java.util.List;
     31 import java.util.concurrent.Executor;
     32 import javax.net.SocketFactory;
     33 import javax.net.ssl.SSLContext;
     34 import org.junit.After;
     35 import org.junit.Before;
     36 import org.junit.Test;
     37 
     38 import static org.junit.Assert.assertEquals;
     39 import static org.junit.Assert.assertFalse;
     40 import static org.junit.Assert.assertNotNull;
     41 import static org.junit.Assert.assertNull;
     42 import static org.junit.Assert.assertSame;
     43 import static org.junit.Assert.assertTrue;
     44 import static org.junit.Assert.fail;
     45 
     46 public final class ConnectionPoolTest {
     47   static {
     48     Internal.initializeInstanceForTests();
     49   }
     50 
     51   private static final List<ConnectionSpec> CONNECTION_SPECS = Util.immutableList(
     52       ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
     53 
     54   private static final int KEEP_ALIVE_DURATION_MS = 5000;
     55   private static final SSLContext sslContext = SslContextBuilder.localhost();
     56 
     57   private MockWebServer spdyServer;
     58   private InetSocketAddress spdySocketAddress;
     59   private Address spdyAddress;
     60 
     61   private MockWebServer httpServer;
     62   private Address httpAddress;
     63   private InetSocketAddress httpSocketAddress;
     64 
     65   private ConnectionPool pool;
     66   private FakeExecutor cleanupExecutor;
     67   private Connection httpA;
     68   private Connection httpB;
     69   private Connection httpC;
     70   private Connection httpD;
     71   private Connection httpE;
     72   private Connection spdyA;
     73 
     74   private Object owner;
     75 
     76   @Before public void setUp() throws Exception {
     77     setUp(2);
     78   }
     79 
     80   private void setUp(int poolSize) throws Exception {
     81     SocketFactory socketFactory = SocketFactory.getDefault();
     82     RecordingProxySelector proxySelector = new RecordingProxySelector();
     83 
     84     spdyServer = new MockWebServer();
     85     httpServer = new MockWebServer();
     86     spdyServer.useHttps(sslContext.getSocketFactory(), false);
     87 
     88     httpServer.start();
     89     httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
     90         null, null, AuthenticatorAdapter.INSTANCE, null,
     91         Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), CONNECTION_SPECS, proxySelector);
     92     httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
     93         httpServer.getPort());
     94 
     95     spdyServer.start();
     96     spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
     97         sslContext.getSocketFactory(), new RecordingHostnameVerifier(), CertificatePinner.DEFAULT,
     98         AuthenticatorAdapter.INSTANCE, null, Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1),
     99         CONNECTION_SPECS, proxySelector);
    100     spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
    101         spdyServer.getPort());
    102 
    103     Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress);
    104     Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress);
    105     pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS);
    106     // Disable the automatic execution of the cleanup.
    107     cleanupExecutor = new FakeExecutor();
    108     pool.replaceCleanupExecutorForTests(cleanupExecutor);
    109     httpA = new Connection(pool, httpRoute);
    110     httpA.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    111     httpB = new Connection(pool, httpRoute);
    112     httpB.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    113     httpC = new Connection(pool, httpRoute);
    114     httpC.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    115     httpD = new Connection(pool, httpRoute);
    116     httpD.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    117     httpE = new Connection(pool, httpRoute);
    118     httpE.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    119     spdyA = new Connection(pool, spdyRoute);
    120     spdyA.connect(20000, 20000, 2000, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    121 
    122     owner = new Object();
    123     httpA.setOwner(owner);
    124     httpB.setOwner(owner);
    125     httpC.setOwner(owner);
    126     httpD.setOwner(owner);
    127     httpE.setOwner(owner);
    128   }
    129 
    130   @After public void tearDown() throws Exception {
    131     httpServer.shutdown();
    132     spdyServer.shutdown();
    133 
    134     Util.closeQuietly(httpA.getSocket());
    135     Util.closeQuietly(httpB.getSocket());
    136     Util.closeQuietly(httpC.getSocket());
    137     Util.closeQuietly(httpD.getSocket());
    138     Util.closeQuietly(httpE.getSocket());
    139     Util.closeQuietly(spdyA.getSocket());
    140   }
    141 
    142   private void resetWithPoolSize(int poolSize) throws Exception {
    143     tearDown();
    144     setUp(poolSize);
    145   }
    146 
    147   @Test public void poolSingleHttpConnection() throws Exception {
    148     resetWithPoolSize(1);
    149     Connection connection = pool.get(httpAddress);
    150     assertNull(connection);
    151 
    152     connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress));
    153     connection.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
    154     connection.setOwner(owner);
    155     assertEquals(0, pool.getConnectionCount());
    156 
    157     pool.recycle(connection);
    158     assertNull(connection.getOwner());
    159     assertEquals(1, pool.getConnectionCount());
    160     assertEquals(1, pool.getHttpConnectionCount());
    161     assertEquals(0, pool.getMultiplexedConnectionCount());
    162 
    163     Connection recycledConnection = pool.get(httpAddress);
    164     assertNull(connection.getOwner());
    165     assertEquals(connection, recycledConnection);
    166     assertTrue(recycledConnection.isAlive());
    167 
    168     recycledConnection = pool.get(httpAddress);
    169     assertNull(recycledConnection);
    170   }
    171 
    172   @Test public void getDoesNotScheduleCleanup() {
    173     Connection connection = pool.get(httpAddress);
    174     assertNull(connection);
    175     cleanupExecutor.assertExecutionScheduled(false);
    176   }
    177 
    178   @Test public void recycleSchedulesCleanup() {
    179     cleanupExecutor.assertExecutionScheduled(false);
    180     pool.recycle(httpA);
    181     cleanupExecutor.assertExecutionScheduled(true);
    182   }
    183 
    184   @Test public void shareSchedulesCleanup() {
    185     cleanupExecutor.assertExecutionScheduled(false);
    186     pool.share(spdyA);
    187     cleanupExecutor.assertExecutionScheduled(true);
    188   }
    189 
    190   @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
    191     pool.recycle(httpA);
    192     pool.recycle(httpB);
    193     pool.recycle(httpC);
    194     assertPooled(pool, httpC, httpB, httpA);
    195 
    196     pool.performCleanup();
    197     assertPooled(pool, httpC, httpB);
    198   }
    199 
    200   @Test public void getSpdyConnection() throws Exception {
    201     pool.share(spdyA);
    202     assertSame(spdyA, pool.get(spdyAddress));
    203     assertPooled(pool, spdyA);
    204   }
    205 
    206   @Test public void getHttpConnection() throws Exception {
    207     pool.recycle(httpA);
    208     assertSame(httpA, pool.get(httpAddress));
    209     assertPooled(pool);
    210   }
    211 
    212   @Test public void expiredConnectionNotReturned() throws Exception {
    213     pool.recycle(httpA);
    214 
    215     // Allow enough time to pass so that the connection is now expired.
    216     Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
    217 
    218     // The connection is held, but will not be returned.
    219     assertNull(pool.get(httpAddress));
    220     assertPooled(pool, httpA);
    221 
    222     // The connection must be cleaned up.
    223     pool.performCleanup();
    224     assertPooled(pool);
    225   }
    226 
    227   @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
    228     pool.recycle(httpA);
    229     pool.recycle(httpB);
    230     pool.recycle(httpC);
    231     pool.recycle(httpD);
    232     assertPooled(pool, httpD, httpC, httpB, httpA);
    233 
    234     pool.performCleanup();
    235     assertPooled(pool, httpD, httpC);
    236   }
    237 
    238   @Test public void expiredConnectionsAreEvicted() throws Exception {
    239     pool.recycle(httpA);
    240     pool.recycle(httpB);
    241 
    242     // Allow enough time to pass so that the connections are now expired.
    243     Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
    244     assertPooled(pool, httpB, httpA);
    245 
    246     // The connections must be cleaned up.
    247     pool.performCleanup();
    248     assertPooled(pool);
    249   }
    250 
    251   @Test public void nonAliveConnectionNotReturned() throws Exception {
    252     pool.recycle(httpA);
    253 
    254     // Close the connection. It is an ex-connection. It has ceased to be.
    255     httpA.getSocket().close();
    256     assertPooled(pool, httpA);
    257     assertNull(pool.get(httpAddress));
    258 
    259     // The connection must be cleaned up.
    260     pool.performCleanup();
    261     assertPooled(pool);
    262   }
    263 
    264   @Test public void differentAddressConnectionNotReturned() throws Exception {
    265     pool.recycle(httpA);
    266     assertNull(pool.get(spdyAddress));
    267     assertPooled(pool, httpA);
    268   }
    269 
    270   @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
    271     pool.share(spdyA);
    272     pool.recycle(httpA);
    273     assertPooled(pool, httpA, spdyA);
    274     assertSame(spdyA, pool.get(spdyAddress));
    275     assertPooled(pool, spdyA, httpA);
    276   }
    277 
    278   @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
    279     pool.recycle(httpA);
    280     pool.recycle(httpB);
    281     assertSame(httpA, pool.get(httpAddress));
    282   }
    283 
    284   @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
    285     httpA.getSocket().shutdownInput();
    286     pool.recycle(httpA); // Should close httpA.
    287     assertTrue(httpA.getSocket().isClosed());
    288 
    289     // The pool should remain empty, and there is no need to schedule a cleanup.
    290     assertPooled(pool);
    291     cleanupExecutor.assertExecutionScheduled(false);
    292   }
    293 
    294   @Test public void shareHttpConnectionFails() throws Exception {
    295     try {
    296       pool.share(httpA);
    297       fail();
    298     } catch (IllegalArgumentException expected) {
    299     }
    300     // The pool should remain empty, and there is no need to schedule a cleanup.
    301     assertPooled(pool);
    302     cleanupExecutor.assertExecutionScheduled(false);
    303   }
    304 
    305   @Test public void recycleSpdyConnectionDoesNothing() throws Exception {
    306     pool.recycle(spdyA);
    307     // The pool should remain empty, and there is no need to schedule the cleanup.
    308     assertPooled(pool);
    309     cleanupExecutor.assertExecutionScheduled(false);
    310   }
    311 
    312   @Test public void validateIdleSpdyConnectionTimeout() throws Exception {
    313     pool.share(spdyA);
    314     assertPooled(pool, spdyA); // Connection should be in the pool.
    315 
    316     Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
    317     pool.performCleanup();
    318     assertPooled(pool, spdyA); // Connection should still be in the pool.
    319 
    320     Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
    321     pool.performCleanup();
    322     assertPooled(pool); // Connection should have been removed.
    323   }
    324 
    325   @Test public void validateIdleHttpConnectionTimeout() throws Exception {
    326     pool.recycle(httpA);
    327     assertPooled(pool, httpA); // Connection should be in the pool.
    328     cleanupExecutor.assertExecutionScheduled(true);
    329 
    330     Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
    331     pool.performCleanup();
    332     assertPooled(pool, httpA); // Connection should still be in the pool.
    333 
    334     Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
    335     pool.performCleanup();
    336     assertPooled(pool); // Connection should have been removed.
    337   }
    338 
    339   @Test public void maxConnections() throws IOException, InterruptedException {
    340     // Pool should be empty.
    341     assertEquals(0, pool.getConnectionCount());
    342 
    343     // http A should be added to the pool.
    344     pool.recycle(httpA);
    345     assertEquals(1, pool.getConnectionCount());
    346     assertEquals(1, pool.getHttpConnectionCount());
    347     assertEquals(0, pool.getMultiplexedConnectionCount());
    348 
    349     // http B should be added to the pool.
    350     pool.recycle(httpB);
    351     assertEquals(2, pool.getConnectionCount());
    352     assertEquals(2, pool.getHttpConnectionCount());
    353     assertEquals(0, pool.getMultiplexedConnectionCount());
    354 
    355     // http C should be added
    356     pool.recycle(httpC);
    357     assertEquals(3, pool.getConnectionCount());
    358     assertEquals(3, pool.getHttpConnectionCount());
    359     assertEquals(0, pool.getSpdyConnectionCount());
    360 
    361     pool.performCleanup();
    362 
    363     // http A should be removed by cleanup.
    364     assertEquals(2, pool.getConnectionCount());
    365     assertEquals(2, pool.getHttpConnectionCount());
    366     assertEquals(0, pool.getMultiplexedConnectionCount());
    367 
    368     // spdy A should be added
    369     pool.share(spdyA);
    370     assertEquals(3, pool.getConnectionCount());
    371     assertEquals(2, pool.getHttpConnectionCount());
    372     assertEquals(1, pool.getSpdyConnectionCount());
    373 
    374     pool.performCleanup();
    375 
    376     // http B should be removed by cleanup.
    377     assertEquals(2, pool.getConnectionCount());
    378     assertEquals(1, pool.getHttpConnectionCount());
    379     assertEquals(1, pool.getMultiplexedConnectionCount());
    380 
    381     // http C should be returned.
    382     Connection recycledHttpConnection = pool.get(httpAddress);
    383     recycledHttpConnection.setOwner(owner);
    384     assertNotNull(recycledHttpConnection);
    385     assertTrue(recycledHttpConnection.isAlive());
    386     assertEquals(1, pool.getConnectionCount());
    387     assertEquals(0, pool.getHttpConnectionCount());
    388     assertEquals(1, pool.getMultiplexedConnectionCount());
    389 
    390     // spdy A will be returned but also kept in the pool.
    391     Connection sharedSpdyConnection = pool.get(spdyAddress);
    392     assertNotNull(sharedSpdyConnection);
    393     assertEquals(spdyA, sharedSpdyConnection);
    394     assertEquals(1, pool.getConnectionCount());
    395     assertEquals(0, pool.getHttpConnectionCount());
    396     assertEquals(1, pool.getMultiplexedConnectionCount());
    397 
    398     // http C should be added to the pool
    399     pool.recycle(httpC);
    400     assertEquals(2, pool.getConnectionCount());
    401     assertEquals(1, pool.getHttpConnectionCount());
    402     assertEquals(1, pool.getMultiplexedConnectionCount());
    403 
    404     // An http connection should be removed from the pool.
    405     recycledHttpConnection = pool.get(httpAddress);
    406     assertNotNull(recycledHttpConnection);
    407     assertTrue(recycledHttpConnection.isAlive());
    408     assertEquals(1, pool.getConnectionCount());
    409     assertEquals(0, pool.getHttpConnectionCount());
    410     assertEquals(1, pool.getMultiplexedConnectionCount());
    411 
    412     // spdy A will be returned but also kept in the pool.
    413     sharedSpdyConnection = pool.get(spdyAddress);
    414     assertEquals(spdyA, sharedSpdyConnection);
    415     assertNotNull(sharedSpdyConnection);
    416     assertEquals(1, pool.getConnectionCount());
    417     assertEquals(0, pool.getHttpConnectionCount());
    418     assertEquals(1, pool.getMultiplexedConnectionCount());
    419 
    420     // http D should be added to the pool.
    421     pool.recycle(httpD);
    422     assertEquals(2, pool.getConnectionCount());
    423     assertEquals(1, pool.getHttpConnectionCount());
    424     assertEquals(1, pool.getMultiplexedConnectionCount());
    425 
    426     // http E should be added to the pool.
    427     pool.recycle(httpE);
    428     assertEquals(3, pool.getConnectionCount());
    429     assertEquals(2, pool.getHttpConnectionCount());
    430     assertEquals(1, pool.getSpdyConnectionCount());
    431 
    432     pool.performCleanup();
    433 
    434     // spdy A should be removed from the pool by cleanup.
    435     assertEquals(2, pool.getConnectionCount());
    436     assertEquals(2, pool.getHttpConnectionCount());
    437     assertEquals(0, pool.getMultiplexedConnectionCount());
    438   }
    439 
    440   @Test public void connectionCleanup() throws Exception {
    441     ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
    442 
    443     // Add 3 connections to the pool.
    444     pool.recycle(httpA);
    445     pool.recycle(httpB);
    446     pool.share(spdyA);
    447 
    448     // Give the cleanup callable time to run and settle down.
    449     Thread.sleep(100);
    450 
    451     // Kill http A.
    452     Util.closeQuietly(httpA.getSocket());
    453 
    454     assertEquals(3, pool.getConnectionCount());
    455     assertEquals(2, pool.getHttpConnectionCount());
    456     assertEquals(1, pool.getSpdyConnectionCount());
    457 
    458     // Http A should be removed.
    459     pool.performCleanup();
    460     assertPooled(pool, spdyA, httpB);
    461     assertEquals(2, pool.getConnectionCount());
    462     assertEquals(1, pool.getHttpConnectionCount());
    463     assertEquals(1, pool.getMultiplexedConnectionCount());
    464 
    465     // Now let enough time pass for the connections to expire.
    466     Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
    467 
    468     // All remaining connections should be removed.
    469     pool.performCleanup();
    470     assertEquals(0, pool.getConnectionCount());
    471   }
    472 
    473   @Test public void maxIdleConnectionsLimitEnforced() throws Exception {
    474     ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
    475 
    476     // Hit the max idle connections limit of 2.
    477     pool.recycle(httpA);
    478     pool.recycle(httpB);
    479     Thread.sleep(100); // Give the cleanup callable time to run.
    480     assertPooled(pool, httpB, httpA);
    481 
    482     // Adding httpC bumps httpA.
    483     pool.recycle(httpC);
    484     Thread.sleep(100); // Give the cleanup callable time to run.
    485     assertPooled(pool, httpC, httpB);
    486 
    487     // Adding httpD bumps httpB.
    488     pool.recycle(httpD);
    489     Thread.sleep(100); // Give the cleanup callable time to run.
    490     assertPooled(pool, httpD, httpC);
    491 
    492     // Adding httpE bumps httpC.
    493     pool.recycle(httpE);
    494     Thread.sleep(100); // Give the cleanup callable time to run.
    495     assertPooled(pool, httpE, httpD);
    496   }
    497 
    498   @Test public void evictAllConnections() throws Exception {
    499     resetWithPoolSize(10);
    500     pool.recycle(httpA);
    501     Util.closeQuietly(httpA.getSocket()); // Include a closed connection in the pool.
    502     pool.recycle(httpB);
    503     pool.share(spdyA);
    504     int connectionCount = pool.getConnectionCount();
    505     assertTrue(connectionCount == 2 || connectionCount == 3);
    506 
    507     pool.evictAll();
    508     assertEquals(0, pool.getConnectionCount());
    509   }
    510 
    511   @Test public void closeIfOwnedBy() throws Exception {
    512     httpA.closeIfOwnedBy(owner);
    513     assertFalse(httpA.isAlive());
    514     assertFalse(httpA.clearOwner());
    515   }
    516 
    517   @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception {
    518     httpA.closeIfOwnedBy(new Object());
    519     assertTrue(httpA.isAlive());
    520     assertTrue(httpA.clearOwner());
    521   }
    522 
    523   @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception {
    524     try {
    525       spdyA.closeIfOwnedBy(owner);
    526       fail();
    527     } catch (IllegalStateException expected) {
    528     }
    529   }
    530 
    531   @Test public void cleanupRunnableStopsEventually() throws Exception {
    532     pool.recycle(httpA);
    533     pool.share(spdyA);
    534     assertPooled(pool, spdyA, httpA);
    535 
    536     // The cleanup should terminate once the pool is empty again.
    537     cleanupExecutor.fakeExecute();
    538     assertPooled(pool);
    539 
    540     cleanupExecutor.assertExecutionScheduled(false);
    541 
    542     // Adding a new connection should cause the cleanup to start up again.
    543     pool.recycle(httpB);
    544 
    545     cleanupExecutor.assertExecutionScheduled(true);
    546 
    547     // The cleanup should terminate once the pool is empty again.
    548     cleanupExecutor.fakeExecute();
    549     assertPooled(pool);
    550   }
    551 
    552   private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
    553     assertEquals(Arrays.asList(connections), pool.getConnections());
    554   }
    555 
    556   /**
    557    * An executor that does not actually execute anything by default. See
    558    * {@link #fakeExecute()}.
    559    */
    560   private static class FakeExecutor implements Executor {
    561 
    562     private Runnable runnable;
    563 
    564     @Override
    565     public void execute(Runnable runnable) {
    566       // This is a bonus assertion for the invariant: At no time should two runnables be scheduled.
    567       assertNull(this.runnable);
    568       this.runnable = runnable;
    569     }
    570 
    571     public void assertExecutionScheduled(boolean expected) {
    572       assertEquals(expected, runnable != null);
    573     }
    574 
    575     /**
    576      * Executes the runnable.
    577      */
    578     public void fakeExecute() {
    579       Runnable toRun = this.runnable;
    580       this.runnable = null;
    581       toRun.run();
    582     }
    583   }
    584 }
    585