X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Flibs%2Frdbms%2Fdatabase%2FDatabase.php;h=a2caa96a96f2d7f7ba3854c7d0f8dce56501582d;hb=201c8d34975165405d7ba014f05656e586d882f0;hp=6e30d3fca4f08d12c323377fd4c74f736e13a2fd;hpb=0c91901454502b3ae2b7228b398aacd34f1a3d12;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 6e30d3fca4..a2caa96a96 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -71,12 +71,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var int New Database instance will already be connected when returned */ const NEW_CONNECTED = 1; - /** @var string SQL query */ - protected $lastQuery = ''; + /** @var string The last SQL query attempted */ + private $lastQuery = ''; /** @var float|bool UNIX timestamp of last write query */ - protected $lastWriteTime = false; + private $lastWriteTime = false; /** @var string|bool */ - protected $phpError = false; + private $lastPhpError = false; + /** @var string Server that this instance is currently connected to */ protected $server; /** @var string User that this instance is currently connected under the name of */ @@ -874,7 +875,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * Set a custom error handler for logging errors during database connection */ protected function installErrorHandler() { - $this->phpError = false; + $this->lastPhpError = false; $this->htmlErrors = ini_set( 'html_errors', '0' ); set_error_handler( [ $this, 'connectionErrorLogger' ] ); } @@ -897,8 +898,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @return string|bool Last PHP error for this DB (typically connection errors) */ protected function getLastPHPError() { - if ( $this->phpError ) { - $error = preg_replace( '!\[\]!', '', $this->phpError ); + if ( $this->lastPhpError ) { + $error = preg_replace( '!\[\]!', '', $this->lastPhpError ); $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error ); return $error; @@ -915,7 +916,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param string $errstr */ public function connectionErrorLogger( $errno, $errstr ) { - $this->phpError = $errstr; + $this->lastPhpError = $errstr; } /** @@ -1019,7 +1020,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * * @throws DBUnexpectedError */ - protected function assertHasConnectionHandle() { + final protected function assertHasConnectionHandle() { if ( !$this->isOpen() ) { throw new DBUnexpectedError( $this, "DB connection was already closed." ); } @@ -1028,7 +1029,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** * Make sure that this server is not marked as a replica nor read-only as a sanity check * - * @throws DBUnexpectedError + * @throws DBReadOnlyRoleError + * @throws DBReadOnlyError */ protected function assertIsWritableMaster() { if ( $this->getLBInfo( 'replica' ) === true ) { @@ -1063,6 +1065,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** * Run a query and return a DBMS-dependent wrapper or boolean * + * This is meant to handle the basic command of actually sending a query to the + * server via the driver. No implicit transaction, reconnection, nor retry logic + * should happen here. The higher level query() method is designed to handle those + * sorts of concerns. This method should not trigger such higher level methods. + * + * The lastError() and lastErrno() methods should meaningfully reflect what error, + * if any, occured during the last call to this method. Methods like executeQuery(), + * query(), select(), insert(), update(), delete(), and upsert() implement their calls + * to doQuery() such that an immediately subsequent call to lastError()/lastErrno() + * meaningfully reflects any error that occured during that public query method call. + * * For SELECT queries, this returns either: * - a) A driver-specific value/resource, only on success. This can be iterated * over by calling fetchObject()/fetchRow() until there are no more rows. @@ -1107,11 +1120,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware // for all queries within a request. Use cases: // - Treating these as writes would trigger ChronologyProtector (see method doc). // - We use this method to reject writes to replicas, but we need to allow - // use of transactions on replicas for read snapshots. This fine given + // use of transactions on replicas for read snapshots. This is fine given // that transactions by themselves don't make changes, only actual writes // within the transaction matter, which we still detect. return !preg_match( - '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|\(SELECT)\b/i', + '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i', $sql ); } @@ -1140,7 +1153,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected function isTransactableQuery( $sql ) { return !in_array( $this->getQueryVerb( $sql ), - [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER' ], + [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE' ], true ); } @@ -1189,109 +1202,132 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function query( $sql, $fname = __METHOD__, $flags = 0 ) { - $this->assertTransactionStatus( $sql, $fname ); - $this->assertHasConnectionHandle(); - $flags = (int)$flags; // b/c; this field used to be a bool - $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS ); + // Sanity check that the SQL query is appropriate in the current context and is + // allowed for an outside caller (e.g. does not break transaction/session tracking). + $this->assertQueryIsCurrentlyAllowed( $sql, $fname ); + + // Send the query to the server and fetch any corresponding errors + list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags ); + if ( $ret === false ) { + $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS ); + // Throw an error unless both the ignore flag was set and a rollback is not needed + $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable ); + } + + return $this->resultObject( $ret ); + } + + /** + * Execute a query, retrying it if there is a recoverable connection loss + * + * This is similar to query() except: + * - It does not prevent all non-ROLLBACK queries if there is a corrupted transaction + * - It does not disallow raw queries that are supposed to use dedicated IDatabase methods + * - It does not throw exceptions for common error cases + * + * This is meant for internal use with Database subclasses. + * + * @param string $sql Original SQL query + * @param string $fname Name of the calling function + * @param int $flags Bitfield of class QUERY_* constants + * @return array An n-tuple of: + * - mixed|bool: An object, resource, or true on success; false on failure + * - string: The result of calling lastError() + * - int: The result of calling lastErrno() + * - bool: Whether a rollback is needed to allow future non-rollback queries + * @throws DBUnexpectedError + */ + final protected function executeQuery( $sql, $fname, $flags ) { + $this->assertHasConnectionHandle(); $priorTransaction = $this->trxLevel; - $priorWritesPending = $this->writesOrCallbacksPending(); - $this->lastQuery = $sql; if ( $this->isWriteQuery( $sql ) ) { # In theory, non-persistent writes are allowed in read-only mode, but due to things # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway... $this->assertIsWritableMaster(); - # Do not treat temporary table writes as "meaningful writes" that need committing. - # Profile them as reads. Integration tests can override this behavior via $flags. + # Do not treat temporary table writes as "meaningful writes" since they are only + # visible to one session and are not permanent. Profile them as reads. Integration + # tests can override this behavior via $flags. $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT ); $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent ); - $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL ); + $isPermWrite = ( $tableType !== self::TEMP_NORMAL ); # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries - if ( $isEffectiveWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) { + if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) { throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" ); } } else { - $isEffectiveWrite = false; + $isPermWrite = false; } - # Add trace comment to the begin of the sql string, right after the operator. - # Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598) + // Add trace comment to the begin of the sql string, right after the operator. + // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598) $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 ); - # Send the query to the server and fetch any corresponding errors - $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ); - $lastError = $this->lastError(); - $lastErrno = $this->lastErrno(); - - $recoverableSR = false; // recoverable statement rollback? - $recoverableCL = false; // recoverable connection loss? - - if ( $ret === false && $this->wasConnectionLoss() ) { - # Check if no meaningful session state was lost - $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending ); - # Update session state tracking and try to restore the connection - $reconnected = $this->replaceLostConnection( __METHOD__ ); - # Silently resend the query to the server if it is safe and possible - if ( $recoverableCL && $reconnected ) { - $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ); - $lastError = $this->lastError(); - $lastErrno = $this->lastErrno(); - - if ( $ret === false && $this->wasConnectionLoss() ) { - # Query probably causes disconnects; reconnect and do not re-run it - $this->replaceLostConnection( __METHOD__ ); - } else { - $recoverableCL = false; // connection does not need recovering - $recoverableSR = $this->wasKnownStatementRollbackError(); - } - } - } else { - $recoverableSR = $this->wasKnownStatementRollbackError(); + // Send the query to the server and fetch any corresponding errors + list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) = + $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ); + // Check if the query failed due to a recoverable connection loss + if ( $ret === false && $recoverableCL && $reconnected ) { + // Silently resend the query to the server since it is safe and possible + list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) = + $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ); } + $corruptedTrx = false; + if ( $ret === false ) { if ( $priorTransaction ) { if ( $recoverableSR ) { # We're ignoring an error that caused just the current query to be aborted. # But log the cause so we can log a deprecation notice if a caller actually # does ignore it. - $this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ]; + $this->trxStatusIgnoredCause = [ $err, $errno, $fname ]; } elseif ( !$recoverableCL ) { # Either the query was aborted or all queries after BEGIN where aborted. # In the first case, the only options going forward are (a) ROLLBACK, or # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only # option is ROLLBACK, since the snapshots would have been released. + $corruptedTrx = true; // cannot recover $this->trxStatus = self::STATUS_TRX_ERROR; $this->trxStatusCause = - $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname ); - $ignoreErrors = false; // cannot recover + $this->getQueryExceptionAndLog( $err, $errno, $sql, $fname ); $this->trxStatusIgnoredCause = null; } } - - $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors ); } - return $this->resultObject( $ret ); + return [ $ret, $err, $errno, $corruptedTrx ]; } /** - * Wrapper for query() that also handles profiling, logging, and affected row count updates + * Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count + * tracking, and reconnects (without retry) on query failure due to connection loss * * @param string $sql Original SQL query * @param string $commentedSql SQL query with debugging/trace comment - * @param bool $isEffectiveWrite Whether the query is a (non-temporary table) write + * @param bool $isPermWrite Whether the query is a (non-temporary table) write * @param string $fname Name of the calling function - * @return bool|ResultWrapper True for a successful write query, ResultWrapper - * object for a successful read query, or false on failure + * @param int $flags Bitfield of class QUERY_* constants + * @return array An n-tuple of: + * - mixed|bool: An object, resource, or true on success; false on failure + * - string: The result of calling lastError() + * - int: The result of calling lastErrno() + * - bool: Whether a statement rollback error occured + * - bool: Whether a disconnect *both* happened *and* was recoverable + * - bool: Whether a reconnection attempt was *both* made *and* succeeded + * @throws DBUnexpectedError */ - private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) { - $this->beginIfImplied( $sql, $fname ); + private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) { + $priorWritesPending = $this->writesOrCallbacksPending(); - # Keep track of whether the transaction has write queries pending - if ( $isEffectiveWrite ) { + if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) { + $this->beginIfImplied( $sql, $fname ); + } + + // Keep track of whether the transaction has write queries pending + if ( $isPermWrite ) { $this->lastWriteTime = microtime( true ); if ( $this->trxLevel && !$this->trxDoneWrites ) { $this->trxDoneWrites = true; @@ -1300,36 +1336,41 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } - if ( $this->getFlag( self::DBO_DEBUG ) ) { - $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" ); - } - - $isMaster = !is_null( $this->getLBInfo( 'master' ) ); - # generalizeSQL() will probably cut down the query to reasonable - # logging size most of the time. The substr is really just a sanity check. - if ( $isMaster ) { - $queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 ); - } else { - $queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 ); - } - - # Include query transaction state - $queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : ""; + $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: '; + $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix ); $startTime = microtime( true ); - $ps = $this->profiler ? ( $this->profiler )( $queryProf ) : null; + $ps = $this->profiler + ? ( $this->profiler )( $generalizedSql->stringify() ) + : null; $this->affectedRowCount = null; + $this->lastQuery = $sql; $ret = $this->doQuery( $commentedSql ); + $lastError = $this->lastError(); + $lastErrno = $this->lastErrno(); + $this->affectedRowCount = $this->affectedRows(); unset( $ps ); // profile out (if set) $queryRuntime = max( microtime( true ) - $startTime, 0.0 ); + $recoverableSR = false; // recoverable statement rollback? + $recoverableCL = false; // recoverable connection loss? + $reconnected = false; // reconnection both attempted and succeeded? + if ( $ret !== false ) { $this->lastPing = $startTime; - if ( $isEffectiveWrite && $this->trxLevel ) { + if ( $isPermWrite && $this->trxLevel ) { $this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() ); $this->trxWriteCallers[] = $fname; } + } elseif ( $this->wasConnectionError( $lastErrno ) ) { + # Check if no meaningful session state was lost + $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending ); + # Update session state tracking and try to restore the connection + $reconnected = $this->replaceLostConnection( __METHOD__ ); + } else { + # Check if only the last query was rolled back + $recoverableSR = $this->wasKnownStatementRollbackError(); } if ( $sql === self::PING_QUERY ) { @@ -1337,18 +1378,26 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } $this->trxProfiler->recordQueryCompletion( - $queryProf, + $generalizedSql, $startTime, - $isEffectiveWrite, - $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret ) + $isPermWrite, + $isPermWrite ? $this->affectedRows() : $this->numRows( $ret ) ); - $this->queryLogger->debug( $sql, [ - 'method' => $fname, - 'master' => $isMaster, - 'runtime' => $queryRuntime, - ] ); - return $ret; + // Avoid the overhead of logging calls unless debug mode is enabled + if ( $this->getFlag( self::DBO_DEBUG ) ) { + $this->queryLogger->debug( + "{method} [{runtime}s]: $sql", + [ + 'method' => $fname, + 'db_host' => $this->getServer(), + 'domain' => $this->getDomainID(), + 'runtime' => round( $queryRuntime, 3 ) + ] + ); + } + + return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ]; } /** @@ -1409,7 +1458,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param string $fname * @throws DBTransactionStateError */ - private function assertTransactionStatus( $sql, $fname ) { + private function assertQueryIsCurrentlyAllowed( $sql, $fname ) { $verb = $this->getQueryVerb( $sql ); if ( $verb === 'USE' ) { throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." ); @@ -1546,11 +1595,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @param int $errno * @param string $sql * @param string $fname - * @param bool $ignoreErrors + * @param bool $ignore * @throws DBQueryError */ - public function reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors = false ) { - if ( $ignoreErrors ) { + public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) { + if ( $ignore ) { $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" ); } else { $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname ); @@ -1580,9 +1629,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ] ) ); $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" ); - $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno ); - if ( $wasQueryTimeout ) { + if ( $this->wasQueryTimeout( $error, $errno ) ) { $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname ); + } elseif ( $this->wasConnectionError( $errno ) ) { + $e = new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname ); } else { $e = new DBQueryError( $this, $error, $errno, $sql, $fname ); } @@ -1607,17 +1657,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $options['LIMIT'] = 1; $res = $this->select( $table, $var, $cond, $fname, $options, $join_conds ); - if ( $res === false || !$this->numRows( $res ) ) { - return false; + if ( $res === false ) { + throw new DBUnexpectedError( $this, "Got false from select()" ); } $row = $this->fetchRow( $res ); - - if ( $row !== false ) { - return reset( $row ); - } else { + if ( $row === false ) { return false; } + + return reset( $row ); } public function selectFieldValues( @@ -1635,7 +1684,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $res = $this->select( $table, [ 'value' => $var ], $cond, $fname, $options, $join_conds ); if ( $res === false ) { - return false; + throw new DBUnexpectedError( $this, "Got false from select()" ); } $values = []; @@ -1872,19 +1921,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ) { $options = (array)$options; $options['LIMIT'] = 1; - $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds ); + $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds ); if ( $res === false ) { - return false; + throw new DBUnexpectedError( $this, "Got false from select()" ); } if ( !$this->numRows( $res ) ) { return false; } - $obj = $this->fetchObject( $res ); - - return $obj; + return $this->fetchObject( $res ); } public function estimateRowCount( @@ -2036,36 +2083,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds ); } - /** - * Removes most variables from an SQL query and replaces them with X or N for numbers. - * It's only slightly flawed. Don't use for anything important. - * - * @param string $sql A SQL Query - * - * @return string - */ - protected static function generalizeSQL( $sql ) { - # This does the same as the regexp below would do, but in such a way - # as to avoid crashing php on some large strings. - # $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql ); - - $sql = str_replace( "\\\\", '', $sql ); - $sql = str_replace( "\\'", '', $sql ); - $sql = str_replace( "\\\"", '', $sql ); - $sql = preg_replace( "/'.*'/s", "'X'", $sql ); - $sql = preg_replace( '/".*"/s', "'X'", $sql ); - - # All newlines, tabs, etc replaced by single space - $sql = preg_replace( '/\s+/', ' ', $sql ); - - # All numbers => N, - # except the ones surrounded by characters, e.g. l10n - $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql ); - $sql = preg_replace( '/(?fieldInfo( $table, $field ); @@ -4146,8 +4163,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * a wrapper. Nowadays, raw database objects are never exposed to external * callers, so this is unnecessary in external code. * - * @param bool|ResultWrapper|resource $result - * @return bool|ResultWrapper + * @param bool|IResultWrapper|resource $result + * @return bool|IResultWrapper */ protected function resultObject( $result ) { if ( !$result ) { @@ -4637,7 +4654,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * Delete a table * @param string $tableName * @param string $fName - * @return bool|ResultWrapper + * @return bool|IResultWrapper * @since 1.18 */ public function dropTable( $tableName, $fName = __METHOD__ ) {