X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fdb%2FDatabase.php;h=e07836b225b4d36db16de98f3ff2a0ff69c8af48;hb=d1cb2084b71fa31012697575d8528a02e10a92bc;hp=5023acd868af8732231e1887838e6508870ffb03;hpb=f7429252f85c5835b291def55fc04b8196c1bb39;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/db/Database.php b/includes/db/Database.php index 5023acd868..e07836b225 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -32,24 +32,35 @@ abstract class DatabaseBase implements IDatabase { /** Number of times to re-try an operation in case of deadlock */ const DEADLOCK_TRIES = 4; - /** Minimum time to wait before retry, in microseconds */ const DEADLOCK_DELAY_MIN = 500000; - /** Maximum time to wait before retry */ const DEADLOCK_DELAY_MAX = 1500000; + /** How long before it is worth doing a dummy query to test the connection */ + const PING_TTL = 1.0; + + /** @var string SQL query */ protected $mLastQuery = ''; + /** @var bool */ protected $mDoneWrites = false; + /** @var string|bool */ protected $mPHPError = false; - - protected $mServer, $mUser, $mPassword, $mDBname; + /** @var string */ + protected $mServer; + /** @var string */ + protected $mUser; + /** @var string */ + protected $mPassword; + /** @var string */ + protected $mDBname; /** @var BagOStuff APC cache */ protected $srvCache; /** @var resource Database connection */ protected $mConn = null; + /** @var bool */ protected $mOpened = false; /** @var array[] List of (callable, method name) */ @@ -61,20 +72,27 @@ abstract class DatabaseBase implements IDatabase { /** @var bool Whether to suppress triggering of post-commit callbacks */ protected $suppressPostCommitCallbacks = false; + /** @var string */ protected $mTablePrefix; + /** @var string */ protected $mSchema; + /** @var integer */ protected $mFlags; + /** @var bool */ protected $mForeign; + /** @var array */ protected $mLBInfo = []; + /** @var bool|null */ protected $mDefaultBigSelects = null; + /** @var array|bool */ protected $mSchemaVars = false; /** @var array */ protected $mSessionVars = []; - + /** @var array|null */ protected $preparedArgs; - + /** @var string|bool|null Stashed value of html_errors INI setting */ protected $htmlErrors; - + /** @var string */ protected $delimiter = ';'; /** @@ -177,6 +195,12 @@ abstract class DatabaseBase implements IDatabase { */ protected $allViews = null; + /** @var float UNIX timestamp */ + protected $lastPing = 0.0; + + /** @var int[] Prior mFlags values */ + private $priorFlags = []; + /** @var TransactionProfiler */ protected $trxProfiler; @@ -409,14 +433,33 @@ abstract class DatabaseBase implements IDatabase { return $this->mOpened; } - public function setFlag( $flag ) { + public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { + if ( $remember === self::REMEMBER_PRIOR ) { + array_push( $this->priorFlags, $this->mFlags ); + } $this->mFlags |= $flag; } - public function clearFlag( $flag ) { + public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) { + if ( $remember === self::REMEMBER_PRIOR ) { + array_push( $this->priorFlags, $this->mFlags ); + } $this->mFlags &= ~$flag; } + public function restoreFlags( $state = self::RESTORE_PRIOR ) { + if ( !$this->priorFlags ) { + return; + } + + if ( $state === self::RESTORE_INITIAL ) { + $this->mFlags = reset( $this->priorFlags ); + $this->priorFlags = []; + } else { + $this->mFlags = array_pop( $this->priorFlags ); + } + } + public function getFlag( $flag ) { return !!( $this->mFlags & $flag ); } @@ -703,7 +746,7 @@ abstract class DatabaseBase implements IDatabase { " performing implicit commit before closing connection!" ); } - $this->commit( __METHOD__, 'flush' ); + $this->commit( __METHOD__, self::FLUSHING_INTERNAL ); } $closed = $this->closeConnection(); @@ -786,8 +829,8 @@ abstract class DatabaseBase implements IDatabase { $priorWritesPending = $this->writesOrCallbacksPending(); $this->mLastQuery = $sql; - $isWriteQuery = $this->isWriteQuery( $sql ); - if ( $isWriteQuery ) { + $isWrite = $this->isWriteQuery( $sql ); + if ( $isWrite ) { $reason = $this->getReadOnlyReason(); if ( $reason !== false ) { throw new DBReadOnlyError( $this, "Database is read-only: $reason" ); @@ -815,54 +858,26 @@ abstract class DatabaseBase implements IDatabase { if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX ) && $this->isTransactableQuery( $sql ) ) { - $this->begin( __METHOD__ . " ($fname)" ); + $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL ); $this->mTrxAutomatic = true; } # Keep track of whether the transaction has write queries pending - if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWriteQuery ) { + if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) { $this->mTrxDoneWrites = true; $this->getTransactionProfiler()->transactionWritingIn( $this->mServer, $this->mDBname, $this->mTrxShortId ); } - $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( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'DatabaseBase::query-master'; - } else { - $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'DatabaseBase::query'; - } - # Include query transaction state - $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : ""; - - $profiler = Profiler::instance(); - if ( !$profiler instanceof ProfilerStub ) { - $totalProfSection = $profiler->scopedProfileIn( $totalProf ); - $queryProfSection = $profiler->scopedProfileIn( $queryProf ); - } - if ( $this->debug() ) { wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) ); } - $queryId = MWDebug::query( $sql, $fname, $isMaster ); - # Avoid fatals if close() was called $this->assertOpen(); - # Do the query and handle errors - $startTime = microtime( true ); - $ret = $this->doQuery( $commentedSql ); - $queryRuntime = microtime( true ) - $startTime; - # Log the query time and feed it into the DB trx profiler - $this->getTransactionProfiler()->recordQueryCompletion( - $queryProf, $startTime, $isWriteQuery, $this->affectedRows() ); - - MWDebug::queryTime( $queryId ); + # Send the query to the server + $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ); # Try reconnecting if the connection was lost if ( false === $ret && $this->wasErrorReissuable() ) { @@ -883,12 +898,7 @@ abstract class DatabaseBase implements IDatabase { $this->reportQueryError( $lastError, $lastErrno, $sql, $fname ); } else { # Should be safe to silently retry the query - $startTime = microtime( true ); - $ret = $this->doQuery( $commentedSql ); - $queryRuntime = microtime( true ) - $startTime; - # Log the query time and feed it into the DB trx profiler - $this->getTransactionProfiler()->recordQueryCompletion( - $queryProf, $startTime, $isWriteQuery, $this->affectedRows() ); + $ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ); } } else { wfDebug( "Failed\n" ); @@ -913,16 +923,47 @@ abstract class DatabaseBase implements IDatabase { $res = $this->resultObject( $ret ); - // Destroy profile sections in the opposite order to their creation - ScopedCallback::consume( $queryProfSection ); - ScopedCallback::consume( $totalProfSection ); + return $res; + } + + private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) { + $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( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + } else { + $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + } + + # Include query transaction state + $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : ""; - if ( $isWriteQuery && $this->mTrxLevel ) { - $this->mTrxWriteDuration += $queryRuntime; - $this->mTrxWriteCallers[] = $fname; + $profiler = Profiler::instance(); + if ( !( $profiler instanceof ProfilerStub ) ) { + $queryProfSection = $profiler->scopedProfileIn( $queryProf ); } - return $res; + $startTime = microtime( true ); + $ret = $this->doQuery( $commentedSql ); + $queryRuntime = microtime( true ) - $startTime; + + unset( $queryProfSection ); // profile out (if set) + + if ( $ret !== false ) { + $this->lastPing = $startTime; + if ( $isWrite && $this->mTrxLevel ) { + $this->mTrxWriteDuration += $queryRuntime; + $this->mTrxWriteCallers[] = $fname; + } + } + + $this->getTransactionProfiler()->recordQueryCompletion( + $queryProf, $startTime, $isWrite, $this->affectedRows() + ); + MWDebug::query( $sql, $fname, $isMaster, $queryRuntime ); + + return $ret; } private function canRecoverFromDisconnect( $sql, $priorWritesPending ) { @@ -2203,7 +2244,7 @@ abstract class DatabaseBase implements IDatabase { $useTrx = !$this->mTrxLevel; if ( $useTrx ) { - $this->begin( $fname ); + $this->begin( $fname, self::TRANSACTION_INTERNAL ); } try { # Update any existing conflicting row(s) @@ -2221,7 +2262,7 @@ abstract class DatabaseBase implements IDatabase { throw $e; } if ( $useTrx ) { - $this->commit( $fname ); + $this->commit( $fname, self::TRANSACTION_INTERNAL ); } return $ok; @@ -2520,7 +2561,7 @@ abstract class DatabaseBase implements IDatabase { $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ]; } else { // If no transaction is active, then make one for this callback - $this->begin( __METHOD__ ); + $this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); try { call_user_func( $callback ); $this->commit( __METHOD__ ); @@ -2559,7 +2600,7 @@ abstract class DatabaseBase implements IDatabase { $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled? /** @var Exception $e */ - $e = $ePrior = null; // last exception + $e = null; // first exception do { // callbacks may add callbacks :) $callbacks = array_merge( $this->mTrxIdleCallbacks, @@ -2577,11 +2618,9 @@ abstract class DatabaseBase implements IDatabase { } else { $this->clearFlag( DBO_TRX ); // restore auto-commit } - } catch ( Exception $e ) { - if ( $ePrior ) { - MWExceptionHandler::logException( $ePrior ); - } - $ePrior = $e; + } catch ( Exception $ex ) { + MWExceptionHandler::logException( $ex ); + $e = $e ?: $ex; // Some callbacks may use startAtomic/endAtomic, so make sure // their transactions are ended so other callbacks don't fail if ( $this->trxLevel() ) { @@ -2592,7 +2631,7 @@ abstract class DatabaseBase implements IDatabase { } while ( count( $this->mTrxIdleCallbacks ) ); if ( $e instanceof Exception ) { - throw $e; // re-throw any last exception + throw $e; // re-throw any first exception } } @@ -2602,9 +2641,10 @@ abstract class DatabaseBase implements IDatabase { * This method should not be used outside of Database/LoadBalancer * * @since 1.22 + * @throws Exception */ public function runOnTransactionPreCommitCallbacks() { - $e = $ePrior = null; // last exception + $e = null; // first exception do { // callbacks may add callbacks :) $callbacks = $this->mTrxPreCommitCallbacks; $this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard) @@ -2612,23 +2652,21 @@ abstract class DatabaseBase implements IDatabase { try { list( $phpCallback ) = $callback; call_user_func( $phpCallback ); - } catch ( Exception $e ) { - if ( $ePrior ) { - MWExceptionHandler::logException( $ePrior ); - } - $ePrior = $e; + } catch ( Exception $ex ) { + MWExceptionHandler::logException( $ex ); + $e = $e ?: $ex; } } } while ( count( $this->mTrxPreCommitCallbacks ) ); if ( $e instanceof Exception ) { - throw $e; // re-throw any last exception + throw $e; // re-throw any first exception } } final public function startAtomic( $fname = __METHOD__ ) { if ( !$this->mTrxLevel ) { - $this->begin( $fname ); + $this->begin( $fname, self::TRANSACTION_INTERNAL ); $this->mTrxAutomatic = true; // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result // in all changes being in one transaction to keep requests transactional. @@ -2651,58 +2689,43 @@ abstract class DatabaseBase implements IDatabase { } if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) { - $this->commit( $fname, 'flush' ); + $this->commit( $fname, self::FLUSHING_INTERNAL ); } } final public function doAtomicSection( $fname, callable $callback ) { $this->startAtomic( $fname ); try { - call_user_func_array( $callback, [ $this, $fname ] ); + $res = call_user_func_array( $callback, [ $this, $fname ] ); } catch ( Exception $e ) { $this->rollback( $fname ); throw $e; } $this->endAtomic( $fname ); + + return $res; } - final public function begin( $fname = __METHOD__ ) { - if ( $this->mTrxLevel ) { // implicit commit + final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) { + // Protect against mismatched atomic section, transaction nesting, and snapshot loss + if ( $this->mTrxLevel ) { if ( $this->mTrxAtomicLevels ) { - // If the current transaction was an automatic atomic one, then we definitely have - // a problem. Same if there is any unclosed atomic level. $levels = implode( ', ', $this->mTrxAtomicLevels ); - throw new DBUnexpectedError( - $this, - "Got explicit BEGIN from $fname while atomic section(s) $levels are open." - ); + $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open."; + throw new DBUnexpectedError( $this, $msg ); } elseif ( !$this->mTrxAutomatic ) { - // We want to warn about inadvertently nested begin/commit pairs, but not about - // auto-committing implicit transactions that were started by query() via DBO_TRX - throw new DBUnexpectedError( - $this, - "$fname: Transaction already in progress (from {$this->mTrxFname}), " . - " performing implicit commit!" - ); - } elseif ( $this->mTrxDoneWrites ) { - // The transaction was automatic and has done write operations - throw new DBUnexpectedError( - $this, - "$fname: Automatic transaction with writes in progress" . - " (from {$this->mTrxFname}), performing implicit commit!\n" - ); - } - - $this->runOnTransactionPreCommitCallbacks(); - $writeTime = $this->pendingWriteQueryDuration(); - $this->doCommit( $fname ); - if ( $this->mTrxDoneWrites ) { - $this->mDoneWrites = microtime( true ); - $this->getTransactionProfiler()->transactionWritingOut( - $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime ); + $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname})."; + throw new DBUnexpectedError( $this, $msg ); + } else { + // @TODO: make this an exception at some point + $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname})."; + wfLogDBError( $msg ); + return; // join the main transaction set } - - $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT ); + } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) { + // @TODO: make this an exception at some point + wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." ); + return; // let any writes be in the main transaction } // Avoid fatals if close() was called @@ -2742,28 +2765,27 @@ abstract class DatabaseBase implements IDatabase { $levels = implode( ', ', $this->mTrxAtomicLevels ); throw new DBUnexpectedError( $this, - "Got COMMIT while atomic sections $levels are still open" + "$fname: Got COMMIT while atomic sections $levels are still open." ); } - if ( $flush === 'flush' ) { + if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) { if ( !$this->mTrxLevel ) { return; // nothing to do } elseif ( !$this->mTrxAutomatic ) { throw new DBUnexpectedError( $this, - "$fname: Flushing an explicit transaction, getting out of sync!" + "$fname: Flushing an explicit transaction, getting out of sync." ); } } else { if ( !$this->mTrxLevel ) { - wfWarn( "$fname: No transaction to commit, something got out of sync!" ); + wfWarn( "$fname: No transaction to commit, something got out of sync." ); return; // nothing to do } elseif ( $this->mTrxAutomatic ) { - throw new DBUnexpectedError( - $this, - "$fname: Explicit commit of implicit transaction." - ); + // @TODO: make this an exception at some point + wfLogDBError( "$fname: Explicit commit of implicit transaction." ); + return; // wait for the main transaction set commit round } } @@ -2796,14 +2818,19 @@ abstract class DatabaseBase implements IDatabase { } final public function rollback( $fname = __METHOD__, $flush = '' ) { - if ( $flush !== 'flush' ) { + if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) { if ( !$this->mTrxLevel ) { - wfWarn( "$fname: No transaction to rollback, something got out of sync!" ); return; // nothing to do } } else { if ( !$this->mTrxLevel ) { + wfWarn( "$fname: No transaction to rollback, something got out of sync." ); return; // nothing to do + } elseif ( $this->getFlag( DBO_TRX ) ) { + throw new DBUnexpectedError( + $this, + "$fname: Expected mass rollback of all peer databases (DBO_TRX set)." + ); } } @@ -2837,10 +2864,7 @@ abstract class DatabaseBase implements IDatabase { } } - /** - * @return bool - */ - protected function explicitTrxActive() { + public function explicitTrxActive() { return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic ); } @@ -2945,21 +2969,35 @@ abstract class DatabaseBase implements IDatabase { } public function ping() { - try { - // This will reconnect if possible, or error out if not - $this->query( "SELECT 1 AS ping", __METHOD__ ); + if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) { return true; - } catch ( DBError $e ) { - return false; } + + $ignoreErrors = true; + $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR ); + // This will reconnect if possible or return false if not + $ok = (bool)$this->query( "SELECT 1 AS ping", __METHOD__, $ignoreErrors ); + $this->restoreFlags( self::RESTORE_PRIOR ); + + return $ok; } /** * @return bool */ protected function reconnect() { - # Stub. Not essential to override. - return true; + $this->closeConnection(); + $this->mOpened = false; + $this->mConn = false; + try { + $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); + $this->lastPing = microtime( true ); + $ok = true; + } catch ( DBConnectionError $e ) { + $ok = false; + } + + return $ok; } public function getSessionLagStatus() { @@ -3297,16 +3335,32 @@ abstract class DatabaseBase implements IDatabase { } public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) { + if ( $this->writesOrCallbacksPending() ) { + // This only flushes transactions to clear snapshots, not to write data + throw new DBUnexpectedError( + $this, + "$fname: Cannot COMMIT to clear snapshot because writes are pending." + ); + } + if ( !$this->lock( $lockKey, $fname, $timeout ) ) { return null; } $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) { - $this->commit( __METHOD__, 'flush' ); - $this->unlock( $lockKey, $fname ); + if ( $this->trxLevel() ) { + // There is a good chance an exception was thrown, causing any early return + // from the caller. Let any error handler get a chance to issue rollback(). + // If there isn't one, let the error bubble up and trigger server-side rollback. + $this->onTransactionResolution( function () use ( $lockKey, $fname ) { + $this->unlock( $lockKey, $fname ); + } ); + } else { + $this->unlock( $lockKey, $fname ); + } } ); - $this->commit( __METHOD__, 'flush' ); + $this->commit( __METHOD__, self::FLUSHING_INTERNAL ); return $unlocker; }