X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22auteur_infos%22%2C%20%22id_auteur=%24id%22%29%20.%20%22?a=blobdiff_plain;f=includes%2Flibs%2Frdbms%2Fdatabase%2FDatabase.php;h=1779880d2719cc65731cfc2860846ac69a4afc4c;hb=78c3b9c21a15030b7ac1cf15c2ae1806dd3d10db;hp=056f18959fb9c3984dbefb101855b8b797d046d5;hpb=3597e03663453c0596c69cb7f36070bade7ec6af;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 056f18959f..1779880d27 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -101,6 +101,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware protected $queryLogger; /** @var callback Error logging callback */ protected $errorLogger; + /** @var callback Deprecation logging callback */ + protected $deprecationLogger; /** @var resource|null Database connection */ protected $conn = null; @@ -141,6 +143,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */ protected $affectedRowCount; + /** + * @var int Transaction status + */ + protected $trxStatus = self::STATUS_TRX_NONE; + /** + * @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR + */ + protected $trxStatusCause; + /** + * @var array|null If wasKnownStatementRollbackError() prevented trxStatus from being set, + * the relevant details are stored here. + */ + protected $trxStatusIgnoredCause; /** * Either 1 if a transaction is active or 0 otherwise. * The other Trx fields may not be meaningfull if this is 0. @@ -197,7 +212,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** * Array of levels of atomicity within transactions * - * @var array + * @var array List of (name, unique ID, savepoint ID) */ private $trxAtomicLevels = []; /** @@ -259,6 +274,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var int */ protected $nonNativeInsertSelectBatchSize = 10000; + /** @var int Transaction is in a error state requiring a full or savepoint rollback */ + const STATUS_TRX_ERROR = 1; + /** @var int Transaction is active and in a normal state */ + const STATUS_TRX_OK = 2; + /** @var int No transaction is active */ + const STATUS_TRX_NONE = 3; + /** * @note: exceptions for missing libraries/drivers should be thrown in initConnection() * @param array $params Parameters passed from Database::factory() @@ -297,6 +319,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->connLogger = $params['connLogger']; $this->queryLogger = $params['queryLogger']; $this->errorLogger = $params['errorLogger']; + $this->deprecationLogger = $params['deprecationLogger']; if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) { $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize']; @@ -381,6 +404,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * includes the agent as a SQL comment. * - trxProfiler: Optional TransactionProfiler instance. * - errorLogger: Optional callback that takes an Exception and logs it. + * - deprecationLogger: Optional callback that takes a string and logs it. * - cliMode: Whether to consider the execution context that of a CLI script. * - agent: Optional name used to identify the end-user in query profiling/logging. * - srvCache: Optional BagOStuff instance to an APC-style cache. @@ -422,6 +446,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING ); }; } + if ( !isset( $p['deprecationLogger'] ) ) { + $p['deprecationLogger'] = function ( $msg ) { + trigger_error( $msg, E_USER_DEPRECATED ); + }; + } /** @var Database $conn */ $conn = new $class( $p ); @@ -548,6 +577,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $this->trxLevel ? $this->trxTimestamp : null; } + /** + * @return int One of the STATUS_TRX_* class constants + * @since 1.31 + */ + public function trxStatus() { + return $this->trxStatus; + } + public function tablePrefix( $prefix = null ) { $old = $this->tablePrefix; if ( $prefix !== null ) { @@ -704,6 +741,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return $fnames; } + /** + * @return string + */ + private function flatAtomicSectionList() { + return array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) { + return $accum === null ? $v[0] : "$accum, " . $v[0]; + } ); + } + public function isOpen() { return $this->opened; } @@ -846,42 +892,78 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ); } - public function close() { + final public function close() { + $exception = null; // error to throw after disconnecting + if ( $this->conn ) { // Resolve any dangling transaction first - if ( $this->trxLevel() ) { - // Meaningful transactions should ideally have been resolved by now - if ( $this->writesOrCallbacksPending() ) { + if ( $this->trxLevel ) { + if ( $this->trxAtomicLevels ) { + // Cannot let incomplete atomic sections be committed + $levels = $this->flatAtomicSectionList(); + $exception = new DBUnexpectedError( + $this, + __METHOD__ . ": atomic sections $levels are still open." + ); + } elseif ( $this->trxAutomatic ) { + // Only the connection manager can commit non-empty DBO_TRX transactions + if ( $this->writesOrCallbacksPending() ) { + $exception = new DBUnexpectedError( + $this, + __METHOD__ . + ": mass commit/rollback of peer transaction required (DBO_TRX set)." + ); + } + } elseif ( $this->trxLevel ) { + // Commit explicit transactions as if this was commit() $this->queryLogger->warning( __METHOD__ . ": writes or callbacks still pending.", [ 'trace' => ( new RuntimeException() )->getTraceAsString() ] ); } - // Check if it is possible to properly commit and trigger callbacks + if ( $this->trxEndCallbacksSuppressed ) { - throw new DBUnexpectedError( + $exception = $exception ?: new DBUnexpectedError( $this, __METHOD__ . ': callbacks are suppressed; cannot properly commit.' ); } - // Commit the changes and run any callbacks as needed - $this->commit( __METHOD__, self::FLUSHING_INTERNAL ); + + // Commit or rollback the changes and run any callbacks as needed + if ( $this->trxStatus === self::STATUS_TRX_OK && !$exception ) { + $this->commit( + __METHOD__, + $this->trxAutomatic ? self::FLUSHING_INTERNAL : self::FLUSHING_ONE + ); + } else { + $this->rollback( __METHOD__, self::FLUSHING_INTERNAL ); + } } + // Close the actual connection in the binding handle $closed = $this->closeConnection(); $this->conn = false; - // Sanity check that no callbacks are dangling - if ( - $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks - ) { - throw new RuntimeException( "Transaction callbacks still pending." ); - } } else { $closed = true; // already closed; nothing to do } $this->opened = false; + // Throw any unexpected errors after having disconnected + if ( $exception instanceof Exception ) { + throw $exception; + } + + // Sanity check that no callbacks are dangling + if ( + $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks + ) { + throw new RuntimeException( + "Transaction callbacks are still pending:\n" . + implode( ', ', $this->pendingWriteAndCallbackCallers() ) + ); + } + return $closed; } @@ -1005,6 +1087,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) { + $this->assertTransactionStatus( $sql, $fname ); + + # Avoid fatals if close() was called + $this->assertOpen(); + $priorWritesPending = $this->writesOrCallbacksPending(); $this->lastQuery = $sql; @@ -1055,9 +1142,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->queryLogger->debug( "{$this->dbName} {$commentedSql}" ); } - # Avoid fatals if close() was called - $this->assertOpen(); - # Send the query to the server and fetch any corresponding errors $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname ); $lastError = $this->lastError(); @@ -1083,20 +1167,29 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } if ( $ret === false ) { - # Deadlocks cause the entire transaction to abort, not just the statement. - # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html - # https://www.postgresql.org/docs/9.1/static/explicit-locking.html - if ( $this->wasDeadlock() ) { - if ( $this->explicitTrxActive() || $priorWritesPending ) { - $tempIgnore = false; // not recoverable + if ( $this->trxLevel ) { + if ( !$this->wasKnownStatementRollbackError() ) { + # Either the query was aborted or all queries after BEGIN where aborted. + if ( $this->explicitTrxActive() || $priorWritesPending ) { + # 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. + $this->trxStatus = self::STATUS_TRX_ERROR; + $this->trxStatusCause = + $this->makeQueryException( $lastError, $lastErrno, $sql, $fname ); + $tempIgnore = false; // cannot recover + } else { + # Nothing prior was there to lose from the transaction, + # so just roll it back. + $this->rollback( __METHOD__ . " ($fname)", self::FLUSHING_INTERNAL ); + } + $this->trxStatusIgnoredCause = null; + } else { + # 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 ]; } - # Usually the transaction is rolled back to BEGIN, leaving an empty transaction. - # Destroy any such transaction so the rollback callbacks run in AUTO-COMMIT mode - # as normal. Also, if DBO_TRX is set and an explicit transaction rolled back here, - # further queries should be back in AUTO-COMMIT mode, not stuck in a transaction. - $this->doRollback( __METHOD__ ); - # Update state tracking to reflect transaction loss - $this->handleTransactionLoss(); } $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore ); @@ -1200,6 +1293,33 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } } + /** + * @param string $sql + * @param string $fname + * @throws DBTransactionStateError + */ + private function assertTransactionStatus( $sql, $fname ) { + if ( $this->getQueryVerb( $sql ) === 'ROLLBACK' ) { // transaction/savepoint + return; + } + + if ( $this->trxStatus < self::STATUS_TRX_OK ) { + throw new DBTransactionStateError( + $this, + "Cannot execute query from $fname while transaction status is ERROR.", + [], + $this->trxStatusCause + ); + } elseif ( $this->trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) { + list( $iLastError, $iLastErrno, $iFname ) = $this->trxStatusIgnoredCause; + call_user_func( $this->deprecationLogger, + "Caller from $fname ignored an error originally raised from $iFname: " . + "[$iLastErrno] $iLastError" + ); + $this->trxStatusIgnoredCause = null; + } + } + /** * Determine whether or not it is safe to retry queries after a database * connection is lost @@ -1224,7 +1344,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } elseif ( $sql === 'ROLLBACK' ) { return true; // transaction lost...which is also what was requested :) } elseif ( $this->explicitTrxActive() ) { - return false; // don't drop atomocity + return false; // don't drop atomocity and explicit snapshots } elseif ( $priorWritesPending ) { return false; // prior writes lost from implicit transaction } @@ -1238,7 +1358,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware private function handleSessionLoss() { // Clean up tracking of session-level things... // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html - // https://www.postgresql.org/docs/9.1/static/sql-createtable.html (ignoring ON COMMIT) + // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT) $this->sessionTempTables = []; // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS @@ -1299,27 +1419,42 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $tempIgnore ) { $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" ); } else { - $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); - $this->queryLogger->error( - "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}", - $this->getLogContext( [ - 'method' => __METHOD__, - 'errno' => $errno, - 'error' => $error, - 'sql1line' => $sql1line, - 'fname' => $fname, - ] ) - ); - $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" ); - $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno ); - if ( $wasQueryTimeout ) { - throw new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname ); - } else { - throw new DBQueryError( $this, $error, $errno, $sql, $fname ); - } + $exception = $this->makeQueryException( $error, $errno, $sql, $fname ); + + throw $exception; } } + /** + * @param string $error + * @param string|int $errno + * @param string $sql + * @param string $fname + * @return DBError + */ + private function makeQueryException( $error, $errno, $sql, $fname ) { + $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); + $this->queryLogger->error( + "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}", + $this->getLogContext( [ + 'method' => __METHOD__, + 'errno' => $errno, + 'error' => $error, + 'sql1line' => $sql1line, + 'fname' => $fname, + ] ) + ); + $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" ); + $wasQueryTimeout = $this->wasQueryTimeout( $error, $errno ); + if ( $wasQueryTimeout ) { + $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname ); + } else { + $e = new DBQueryError( $this, $error, $errno, $sql, $fname ); + } + + return $e; + } + public function freeResult( $res ) { } @@ -3026,6 +3161,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware return false; } + /** + * @return bool Whether it is safe to assume the given error only caused statement rollback + * @note This is for backwards compatibility for callers catching DBError exceptions in + * order to ignore problems like duplicate key errors or foriegn key violations + * @since 1.31 + */ + protected function wasKnownStatementRollbackError() { + return false; // don't know; it could have caused a transaction rollback + } + public function deadlockLoop() { $args = func_get_args(); $function = array_shift( $args ); @@ -3321,56 +3466,104 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->doSavepoint( $savepointId, $fname ); } - $this->trxAtomicLevels[] = [ $fname, $savepointId ]; + $sectionId = new AtomicSectionIdentifier; + $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ]; + + return $sectionId; } final public function endAtomic( $fname = __METHOD__ ) { - if ( !$this->trxLevel ) { - throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." ); + if ( !$this->trxLevel || !$this->trxAtomicLevels ) { + throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." ); } - list( $savedFname, $savepointId ) = $this->trxAtomicLevels - ? array_pop( $this->trxAtomicLevels ) : [ null, null ]; + // Check if the current section matches $fname + $pos = count( $this->trxAtomicLevels ) - 1; + list( $savedFname, , $savepointId ) = $this->trxAtomicLevels[$pos]; + if ( $savedFname !== $fname ) { - throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." ); + throw new DBUnexpectedError( + $this, + "Invalid atomic section ended (got $fname but expected $savedFname)." + ); } + // Remove the last section and re-index the array + $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos ); + if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) { $this->commit( $fname, self::FLUSHING_INTERNAL ); - } elseif ( $savepointId && $savepointId !== 'n/a' ) { + } elseif ( $savepointId !== null && $savepointId !== 'n/a' ) { $this->doReleaseSavepoint( $savepointId, $fname ); } } - final public function cancelAtomic( $fname = __METHOD__ ) { - if ( !$this->trxLevel ) { - throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." ); + final public function cancelAtomic( + $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null + ) { + if ( !$this->trxLevel || !$this->trxAtomicLevels ) { + throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." ); } - list( $savedFname, $savepointId ) = $this->trxAtomicLevels - ? array_pop( $this->trxAtomicLevels ) : [ null, null ]; - if ( $savedFname !== $fname ) { - throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." ); + if ( $sectionId !== null ) { + // Find the (last) section with the given $sectionId + $pos = -1; + foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) { + if ( $asId === $sectionId ) { + $pos = $i; + } + } + if ( $pos < 0 ) { + throw new DBUnexpectedError( "Atomic section not found (for $fname)" ); + } + // Remove all descendant sections and re-index the array + $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 ); } - if ( !$savepointId ) { - throw new DBUnexpectedError( $this, "Uncancelable atomic section canceled (got $fname)." ); + + // Check if the current section matches $fname + $pos = count( $this->trxAtomicLevels ) - 1; + list( $savedFname, , $savepointId ) = $this->trxAtomicLevels[$pos]; + + if ( $savedFname !== $fname ) { + throw new DBUnexpectedError( + $this, + "Invalid atomic section ended (got $fname but expected $savedFname)." + ); } - if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) { - $this->rollback( $fname, self::FLUSHING_INTERNAL ); - } elseif ( $savepointId !== 'n/a' ) { - $this->doRollbackToSavepoint( $savepointId, $fname ); + // Remove the last section and re-index the array + $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos ); + + if ( $savepointId !== null ) { + // Rollback the transaction to the state just before this atomic section + if ( $savepointId === 'n/a' ) { + $this->rollback( $fname, self::FLUSHING_INTERNAL ); + } else { + $this->doRollbackToSavepoint( $savepointId, $fname ); + $this->trxStatus = self::STATUS_TRX_OK; // no exception; recovered + $this->trxStatusIgnoredCause = null; + } + } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) { + // Put the transaction into an error state if it's not already in one + $this->trxStatus = self::STATUS_TRX_ERROR; + $this->trxStatusCause = new DBUnexpectedError( + $this, + "Uncancelable atomic section canceled (got $fname)." + ); } $this->affectedRowCount = 0; // for the sake of consistency } - final public function doAtomicSection( $fname, callable $callback ) { - $this->startAtomic( $fname, self::ATOMIC_CANCELABLE ); + final public function doAtomicSection( + $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE + ) { + $sectionId = $this->startAtomic( $fname, $cancelable ); try { $res = call_user_func_array( $callback, [ $this, $fname ] ); } catch ( Exception $e ) { - $this->cancelAtomic( $fname ); + $this->cancelAtomic( $fname, $sectionId ); + throw $e; } $this->endAtomic( $fname ); @@ -3379,12 +3572,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) { + static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ]; + if ( !in_array( $mode, $modes, true ) ) { + throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." ); + } + // Protect against mismatched atomic section, transaction nesting, and snapshot loss if ( $this->trxLevel ) { if ( $this->trxAtomicLevels ) { - $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) { - return $accum === null ? $v[0] : "$accum, " . $v[0]; - } ); + $levels = $this->flatAtomicSectionList(); $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open."; throw new DBUnexpectedError( $this, $msg ); } elseif ( !$this->trxAutomatic ) { @@ -3403,6 +3599,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->assertOpen(); $this->doBegin( $fname ); + $this->trxStatus = self::STATUS_TRX_OK; + $this->trxStatusIgnoredCause = null; $this->trxAtomicCounter = 0; $this->trxTimestamp = microtime( true ); $this->trxFname = $fname; @@ -3418,6 +3616,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxWriteCallers = []; // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ. // Get an estimate of the replication lag before any such queries. + $this->trxReplicaLag = null; // clear cached value first $this->trxReplicaLag = $this->getApproximateLagStatus()['lag']; // T147697: make explicitTrxActive() return true until begin() finishes. This way, no // caller will think its OK to muck around with the transaction just because startAtomic() @@ -3436,12 +3635,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxLevel = 1; } - final public function commit( $fname = __METHOD__, $flush = '' ) { + final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) { + static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ]; + if ( !in_array( $flush, $modes, true ) ) { + throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." ); + } + if ( $this->trxLevel && $this->trxAtomicLevels ) { - // There are still atomic sections open. This cannot be ignored - $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) { - return $accum === null ? $v[0] : "$accum, " . $v[0]; - } ); + // There are still atomic sections open; this cannot be ignored + $levels = $this->flatAtomicSectionList(); throw new DBUnexpectedError( $this, "$fname: Got COMMIT while atomic sections $levels are still open." @@ -3476,6 +3678,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->runOnTransactionPreCommitCallbacks(); $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY ); $this->doCommit( $fname ); + $this->trxStatus = self::STATUS_TRX_NONE; if ( $this->trxDoneWrites ) { $this->lastWriteTime = microtime( true ); $this->trxProfiler->transactionWritingOut( @@ -3521,6 +3724,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->assertOpen(); $this->doRollback( $fname ); + $this->trxStatus = self::STATUS_TRX_NONE; $this->trxAtomicLevels = []; if ( $this->trxDoneWrites ) { $this->trxProfiler->transactionWritingOut( @@ -3707,7 +3911,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function getSessionLagStatus() { - return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus(); + return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus(); } /** @@ -3718,11 +3922,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * is this lag plus transaction duration. If they don't, it is still * safe to be pessimistic. This returns null if there is no transaction. * + * This returns null if the lag status for this transaction was not yet recorded. + * * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN) * @since 1.27 */ - final protected function getTransactionLagStatus() { - return $this->trxLevel + final protected function getRecordedTransactionLagStatus() { + return ( $this->trxLevel && $this->trxReplicaLag !== null ) ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ] : null; }