diff --git a/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
index b45c325..2499d04 100644
--- a/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
+++ b/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
@@ -1,377 +1,378 @@
 <?php
 
 abstract class AphrontBaseMySQLDatabaseConnection
   extends AphrontDatabaseConnection {
 
   private $configuration;
   private $connection;
   private $connectionPool = array();
   private $lastResult;
 
   private $nextError;
 
   abstract protected function connect();
   abstract protected function rawQuery($raw_query);
   abstract protected function rawQueries(array $raw_queries);
   abstract protected function fetchAssoc($result);
   abstract protected function getErrorCode($connection);
   abstract protected function getErrorDescription($connection);
   abstract protected function closeConnection();
   abstract protected function freeResult($result);
 
   public function __construct(array $configuration) {
     $this->configuration = $configuration;
   }
 
   public function __clone() {
     $this->establishConnection();
   }
 
   public function close() {
     if ($this->lastResult) {
       $this->lastResult = null;
     }
     if ($this->connection) {
       $this->closeConnection();
       $this->connection = null;
     }
   }
 
   public function escapeColumnName($name) {
     return '`'.str_replace('`', '``', $name).'`';
   }
 
 
   public function escapeMultilineComment($comment) {
     // These can either terminate a comment, confuse the hell out of the parser,
     // make MySQL execute the comment as a query, or, in the case of semicolon,
     // are quasi-dangerous because the semicolon could turn a broken query into
     // a working query plus an ignored query.
 
     static $map = array(
       '--'  => '(DOUBLEDASH)',
       '*/'  => '(STARSLASH)',
       '//'  => '(SLASHSLASH)',
       '#'   => '(HASH)',
       '!'   => '(BANG)',
       ';'   => '(SEMICOLON)',
     );
 
     $comment = str_replace(
       array_keys($map),
       array_values($map),
       $comment);
 
     // For good measure, kill anything else that isn't a nice printable
     // character.
     $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment);
 
     return '/* '.$comment.' */';
   }
 
   public function escapeStringForLikeClause($value) {
     $value = addcslashes($value, '\%_');
     $value = $this->escapeUTF8String($value);
     return $value;
   }
 
   protected function getConfiguration($key, $default = null) {
     return idx($this->configuration, $key, $default);
   }
 
   private function establishConnection() {
     $host = $this->getConfiguration('host');
     $database = $this->getConfiguration('database');
 
     $profiler = PhutilServiceProfiler::getInstance();
     $call_id = $profiler->beginServiceCall(
       array(
         'type'      => 'connect',
         'host'      => $host,
         'database'  => $database,
       ));
 
     $retries = max(1, $this->getConfiguration('retries', 3));
     while ($retries--) {
       try {
         $conn = $this->connect();
         $profiler->endServiceCall($call_id, array());
         break;
       } catch (AphrontQueryException $ex) {
         if ($retries && $ex->getCode() == 2003) {
           $class = get_class($ex);
           $message = $ex->getMessage();
           phlog(pht('Retrying (%d) after %s: %s', $retries, $class, $message));
         } else {
           $profiler->endServiceCall($call_id, array());
           throw $ex;
         }
       }
     }
 
     $this->connection = $conn;
   }
 
   protected function requireConnection() {
     if (!$this->connection) {
       if ($this->connectionPool) {
         $this->connection = array_pop($this->connectionPool);
       } else {
         $this->establishConnection();
       }
     }
     return $this->connection;
   }
 
   protected function beginAsyncConnection() {
     $connection = $this->requireConnection();
     $this->connection = null;
     return $connection;
   }
 
   protected function endAsyncConnection($connection) {
     if ($this->connection) {
       $this->connectionPool[] = $this->connection;
     }
     $this->connection = $connection;
   }
 
   public function selectAllResults() {
     $result = array();
     $res = $this->lastResult;
     if ($res == null) {
       throw new Exception(pht('No query result to fetch from!'));
     }
     while (($row = $this->fetchAssoc($res))) {
       $result[] = $row;
     }
     return $result;
   }
 
   public function executeRawQuery($raw_query) {
     $this->lastResult = null;
     $retries = max(1, $this->getConfiguration('retries', 3));
     while ($retries--) {
       try {
         $this->requireConnection();
         $is_write = $this->checkWrite($raw_query);
 
         $profiler = PhutilServiceProfiler::getInstance();
         $call_id = $profiler->beginServiceCall(
           array(
             'type'    => 'query',
             'config'  => $this->configuration,
             'query'   => $raw_query,
             'write'   => $is_write,
           ));
 
         $result = $this->rawQuery($raw_query);
 
         $profiler->endServiceCall($call_id, array());
 
         if ($this->nextError) {
           $result = null;
         }
 
         if ($result) {
           $this->lastResult = $result;
           break;
         }
 
         $this->throwQueryException($this->connection);
       } catch (AphrontConnectionLostQueryException $ex) {
         if ($this->isInsideTransaction()) {
           // Zero out the transaction state to prevent a second exception
           // ("program exited with open transaction") from being thrown, since
           // we're about to throw a more relevant/useful one instead.
           $state = $this->getTransactionState();
           while ($state->getDepth()) {
             $state->decreaseDepth();
           }
 
           // We can't close the connection before this because
           // isInsideTransaction() and getTransactionState() depend on the
           // connection.
           $this->close();
 
           throw $ex;
         }
 
         $this->close();
 
         if (!$retries) {
           throw $ex;
         }
       }
     }
   }
 
   public function executeRawQueries(array $raw_queries) {
     if (!$raw_queries) {
       return array();
     }
 
     $is_write = false;
     foreach ($raw_queries as $key => $raw_query) {
       $is_write = $is_write || $this->checkWrite($raw_query);
       $raw_queries[$key] = rtrim($raw_query, "\r\n\t ;");
     }
 
     $profiler = PhutilServiceProfiler::getInstance();
     $call_id = $profiler->beginServiceCall(
       array(
         'type'    => 'multi-query',
         'config'  => $this->configuration,
         'queries' => $raw_queries,
         'write'   => $is_write,
       ));
 
     $results = $this->rawQueries($raw_queries);
 
     $profiler->endServiceCall($call_id, array());
 
     return $results;
   }
 
   protected function processResult($result) {
     if (!$result) {
       try {
         $this->throwQueryException($this->requireConnection());
       } catch (Exception $ex) {
         return $ex;
       }
     } else if (is_bool($result)) {
       return $this->getAffectedRows();
     }
     $rows = array();
     while (($row = $this->fetchAssoc($result))) {
       $rows[] = $row;
     }
     $this->freeResult($result);
     return $rows;
   }
 
   protected function checkWrite($raw_query) {
     // NOTE: The opening "(" allows queries in the form of:
     //
     //   (SELECT ...) UNION (SELECT ...)
     $is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query);
     if ($is_write) {
       if ($this->getReadOnly()) {
         throw new Exception(
           pht(
             'Attempting to issue a write query on a read-only '.
             'connection (to database "%s")!',
             $this->getConfiguration('database')));
       }
       AphrontWriteGuard::willWrite();
       return true;
     }
 
     return false;
   }
 
   protected function throwQueryException($connection) {
     if ($this->nextError) {
       $errno = $this->nextError;
       $error = pht('Simulated error.');
       $this->nextError = null;
     } else {
       $errno = $this->getErrorCode($connection);
       $error = $this->getErrorDescription($connection);
     }
     $this->throwQueryCodeException($errno, $error);
   }
 
   private function throwCommonException($errno, $error) {
     $message = pht('#%d: %s', $errno, $error);
 
     switch ($errno) {
       case 2013: // Connection Dropped
         throw new AphrontConnectionLostQueryException($message);
       case 2006: // Gone Away
         $more = pht(
           "This error may occur if your MySQL '%s' or '%s' ".
           "configuration values are set too low.",
           'wait_timeout',
           'max_allowed_packet');
         throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");
       case 1213: // Deadlock
         throw new AphrontDeadlockQueryException($message);
       case 1205: // Lock wait timeout exceeded
         throw new AphrontLockTimeoutQueryException($message);
       case 1062: // Duplicate Key
         // NOTE: In some versions of MySQL we get a key name back here, but
         // older versions just give us a key index ("key 2") so it's not
         // portable to parse the key out of the error and attach it to the
         // exception.
         throw new AphrontDuplicateKeyQueryException($message);
       case 1044: // Access denied to database
       case 1142: // Access denied to table
       case 1143: // Access denied to column
+      case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).
         throw new AphrontAccessDeniedQueryException($message);
       case 1045: // Access denied (auth)
         throw new AphrontInvalidCredentialsQueryException($message);
       case 1146: // No such table
       case 1049: // No such database
       case 1054: // Unknown column "..." in field list
         throw new AphrontSchemaQueryException($message);
     }
 
     // TODO: 1064 is syntax error, and quite terrible in production.
 
     return null;
   }
 
   protected function throwConnectionException($errno, $error, $user, $host) {
     $this->throwCommonException($errno, $error);
 
     $message = pht(
       'Attempt to connect to %s@%s failed with error #%d: %s.',
       $user,
       $host,
       $errno,
       $error);
 
     throw new AphrontConnectionQueryException($message, $errno);
   }
 
 
   protected function throwQueryCodeException($errno, $error) {
     $this->throwCommonException($errno, $error);
 
     $message = pht(
       '#%d: %s',
       $errno,
       $error);
 
     throw new AphrontQueryException($message, $errno);
   }
 
   /**
    * Force the next query to fail with a simulated error. This should be used
    * ONLY for unit tests.
    */
   public function simulateErrorOnNextQuery($error) {
     $this->nextError = $error;
     return $this;
   }
 
   /**
    * Check inserts for characters outside of the BMP. Even with the strictest
    * settings, MySQL will silently truncate data when it encounters these, which
    * can lead to data loss and security problems.
    */
   protected function validateUTF8String($string) {
     if (phutil_is_utf8($string)) {
       return;
     }
 
     throw new AphrontCharacterSetQueryException(
       pht(
         'Attempting to construct a query using a non-utf8 string when '.
         'utf8 is expected. Use the `%%B` conversion to escape binary '.
         'strings data.'));
   }
 
 }
diff --git a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
index 710300e..7371863 100644
--- a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
+++ b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
@@ -1,215 +1,233 @@
 <?php
 
 final class AphrontMySQLDatabaseConnection
   extends AphrontBaseMySQLDatabaseConnection {
 
   public function escapeUTF8String($string) {
     $this->validateUTF8String($string);
     return $this->escapeBinaryString($string);
   }
 
   public function escapeBinaryString($string) {
     return mysql_real_escape_string($string, $this->requireConnection());
   }
 
   public function getInsertID() {
     return mysql_insert_id($this->requireConnection());
   }
 
   public function getAffectedRows() {
     return mysql_affected_rows($this->requireConnection());
   }
 
   protected function closeConnection() {
     mysql_close($this->requireConnection());
   }
 
   protected function connect() {
     if (!function_exists('mysql_connect')) {
       // We have to '@' the actual call since it can spew all sorts of silly
       // noise, but it will also silence fatals caused by not having MySQL
       // installed, which has bitten me on three separate occasions. Make sure
       // such failures are explicit and loud.
       throw new Exception(
         pht(
           'About to call %s, but the PHP MySQL extension is not available!',
           'mysql_connect()'));
     }
 
     $user = $this->getConfiguration('user');
     $host = $this->getConfiguration('host');
     $port = $this->getConfiguration('port');
 
     if ($port) {
       $host .= ':'.$port;
     }
 
     $database = $this->getConfiguration('database');
 
     $pass = $this->getConfiguration('pass');
     if ($pass instanceof PhutilOpaqueEnvelope) {
       $pass = $pass->openEnvelope();
     }
 
-    $conn = @mysql_connect(
-      $host,
-      $user,
-      $pass,
-      $new_link = true,
-      $flags = 0);
+    $timeout = $this->getConfiguration('timeout');
+    $timeout_ini = 'mysql.connect_timeout';
+    if ($timeout) {
+      $old_timeout = ini_get($timeout_ini);
+      ini_set($timeout_ini, $timeout);
+    }
+
+    try {
+      $conn = @mysql_connect(
+        $host,
+        $user,
+        $pass,
+        $new_link = true,
+        $flags = 0);
+    } catch (Exception $ex) {
+      if ($timeout) {
+        ini_set($timeout_ini, $old_timeout);
+      }
+      throw $ex;
+    }
+
+    if ($timeout) {
+      ini_set($timeout_ini, $old_timeout);
+    }
 
     if (!$conn) {
       $errno = mysql_errno();
       $error = mysql_error();
       $this->throwConnectionException($errno, $error, $user, $host);
     }
 
     if ($database !== null) {
       $ret = @mysql_select_db($database, $conn);
       if (!$ret) {
         $this->throwQueryException($conn);
       }
     }
 
     $ok = @mysql_set_charset('utf8mb4', $conn);
     if (!$ok) {
       mysql_set_charset('utf8', $conn);
     }
 
     return $conn;
   }
 
   protected function rawQuery($raw_query) {
     return @mysql_query($raw_query, $this->requireConnection());
   }
 
   /**
    * @phutil-external-symbol function mysql_multi_query
    * @phutil-external-symbol function mysql_fetch_result
    * @phutil-external-symbol function mysql_more_results
    * @phutil-external-symbol function mysql_next_result
    */
   protected function rawQueries(array $raw_queries) {
     $conn = $this->requireConnection();
     $results = array();
 
     if (!function_exists('mysql_multi_query')) {
       foreach ($raw_queries as $key => $raw_query) {
         $results[$key] = $this->processResult($this->rawQuery($raw_query));
       }
       return $results;
     }
 
     if (!mysql_multi_query(implode("\n;\n\n", $raw_queries), $conn)) {
       $ex = $this->processResult(false);
       return array_fill_keys(array_keys($raw_queries), $ex);
     }
 
     $processed_all = false;
     foreach ($raw_queries as $key => $raw_query) {
       $results[$key] = $this->processResult(@mysql_fetch_result($conn));
       if (!mysql_more_results($conn)) {
         $processed_all = true;
         break;
       }
       mysql_next_result($conn);
     }
 
     if (!$processed_all) {
       throw new Exception(
         pht('There are some results left in the result set.'));
     }
 
     return $results;
   }
 
   protected function freeResult($result) {
     mysql_free_result($result);
   }
 
   public function supportsParallelQueries() {
     // fb_parallel_query() doesn't support results with different columns.
     return false;
   }
 
   /**
    * @phutil-external-symbol function fb_parallel_query
    */
   public function executeParallelQueries(
     array $queries,
     array $conns = array()) {
     assert_instances_of($conns, __CLASS__);
 
     $map = array();
     $is_write = false;
     foreach ($queries as $id => $query) {
       $is_write = $is_write || $this->checkWrite($query);
       $conn = idx($conns, $id, $this);
 
       $host = $conn->getConfiguration('host');
       $port = 0;
       $match = null;
       if (preg_match('/(.+):(.+)/', $host, $match)) {
         list(, $host, $port) = $match;
       }
 
       $pass = $conn->getConfiguration('pass');
       if ($pass instanceof PhutilOpaqueEnvelope) {
         $pass = $pass->openEnvelope();
       }
 
       $map[$id] = array(
         'sql' => $query,
         'ip' => $host,
         'port' => $port,
         'username' => $conn->getConfiguration('user'),
         'password' => $pass,
         'db' => $conn->getConfiguration('database'),
       );
     }
 
     $profiler = PhutilServiceProfiler::getInstance();
     $call_id = $profiler->beginServiceCall(
       array(
         'type'    => 'multi-query',
         'queries' => $queries,
         'write'   => $is_write,
       ));
 
     $map = fb_parallel_query($map);
 
     $profiler->endServiceCall($call_id, array());
 
     $results = array();
     $pos = 0;
     $err_pos = 0;
     foreach ($queries as $id => $query) {
       $errno = idx(idx($map, 'errno', array()), $err_pos);
       $err_pos++;
       if ($errno) {
         try {
           $this->throwQueryCodeException($errno, $map['error'][$id]);
         } catch (Exception $ex) {
           $results[$id] = $ex;
         }
         continue;
       }
       $results[$id] = $map['result'][$pos];
       $pos++;
     }
     return $results;
   }
 
   protected function fetchAssoc($result) {
     return mysql_fetch_assoc($result);
   }
 
   protected function getErrorCode($connection) {
     return mysql_errno($connection);
   }
 
   protected function getErrorDescription($connection) {
     return mysql_error($connection);
   }
 
 }