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 @@ 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 @@ 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); } }