X-Git-Url: https://git.cyclocoop.org/%7B%24admin_url%7Dcompta/operations/modifier.php?a=blobdiff_plain;f=includes%2Flibs%2Frdbms%2Fdatabase%2FDatabase.php;h=056f18959fb9c3984dbefb101855b8b797d046d5;hb=6c169ee1fd84dcf82596edc8c696eff40f2b9aed;hp=5f7215277bbf76bf7e8b8798876f58c793d21895;hpb=e7038cda804d5edbd4222981ad2c1e4aa6c981b7;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 5f7215277b..056f18959f 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -35,6 +35,7 @@ use BagOStuff; use HashBagOStuff; use LogicException; use InvalidArgumentException; +use UnexpectedValueException; use Exception; use RuntimeException; @@ -282,6 +283,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->flags |= self::DBO_TRX; } } + // Disregard deprecated DBO_IGNORE flag (T189999) + $this->flags &= ~self::DBO_IGNORE; $this->sessionVars = $params['variables']; @@ -632,6 +635,20 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware ); } + /** + * @return string|null + */ + final protected function getTransactionRoundId() { + // If transaction round participation is enabled, see if one is active + if ( $this->getFlag( self::DBO_TRX ) ) { + $id = $this->getLBInfo( 'trxRoundId' ); + + return is_string( $id ) ? $id : null; + } + + return null; + } + public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) { if ( !$this->trxLevel ) { return false; @@ -693,7 +710,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { if ( ( $flag & self::DBO_IGNORE ) ) { - throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." ); + throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." ); } if ( $remember === self::REMEMBER_PRIOR ) { @@ -704,7 +721,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) { if ( ( $flag & self::DBO_IGNORE ) ) { - throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." ); + throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." ); } if ( $remember === self::REMEMBER_PRIOR ) { @@ -1041,38 +1058,31 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware # Avoid fatals if close() was called $this->assertOpen(); - # Send the query to the server + # Send the query to the server and fetch any corresponding errors $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname ); + $lastError = $this->lastError(); + $lastErrno = $this->lastErrno(); # Try reconnecting if the connection was lost - if ( false === $ret && $this->wasConnectionLoss() ) { + if ( $ret === false && $this->wasConnectionLoss() ) { + # Check if any meaningful session state was lost $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending ); - # Stash the last error values before anything might clear them - $lastError = $this->lastError(); - $lastErrno = $this->lastErrno(); - # Update state tracking to reflect transaction loss due to disconnection - $this->handleSessionLoss(); - if ( $this->reconnect() ) { - $msg = __METHOD__ . ': lost connection to {dbserver}; reconnected'; - $params = [ 'dbserver' => $this->getServer() ]; - $this->connLogger->warning( $msg, $params ); - $this->queryLogger->warning( $msg, $params + - [ 'trace' => ( new RuntimeException() )->getTraceAsString() ] ); - - if ( $recoverable ) { - # Should be safe to silently retry the query - $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname ); - } else { - # Callers may catch the exception and continue to use the DB - $this->reportQueryError( $lastError, $lastErrno, $sql, $fname ); + # 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 ( $reconnected && $recoverable ) { + $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $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 { - $msg = __METHOD__ . ': lost connection to {dbserver} permanently'; - $this->connLogger->error( $msg, [ 'dbserver' => $this->getServer() ] ); } } - if ( false === $ret ) { + 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 @@ -1080,17 +1090,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $this->explicitTrxActive() || $priorWritesPending ) { $tempIgnore = false; // not recoverable } + # 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->handleSessionLoss(); + $this->handleTransactionLoss(); } - $this->reportQueryError( - $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore ); + $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore ); } - $res = $this->resultObject( $ret ); - - return $res; + return $this->resultObject( $ret ); } /** @@ -1205,6 +1217,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware # didn't matter anyway (aside from DBO_TRX snapshot loss). if ( $this->namedLocksHeld ) { return false; // possible critical section violation + } elseif ( $this->sessionTempTables ) { + return false; // tables might be queried latter } elseif ( $sql === 'COMMIT' ) { return !$priorWritesPending; // nothing written anyway? (T127428) } elseif ( $sql === 'ROLLBACK' ) { @@ -1219,36 +1233,41 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * Clean things up after transaction loss due to disconnection - * - * @return null|Exception + * Clean things up after session (and thus transaction) loss */ 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) + $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 + $this->namedLocksHeld = []; + // Session loss implies transaction loss + $this->handleTransactionLoss(); + } + + /** + * Clean things up after transaction loss + */ + private function handleTransactionLoss() { $this->trxLevel = 0; $this->trxAtomicCounter = 0; $this->trxIdleCallbacks = []; // T67263; transaction already lost $this->trxPreCommitCallbacks = []; // T67263; transaction already lost - $this->sessionTempTables = []; - $this->namedLocksHeld = []; - - // Note: if callback suppression is set then some *Callbacks arrays are not cleared here - $e = null; try { - // Handle callbacks in trxEndCallbacks + // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution(). + // If callback suppression is set then the array will remain unhandled. $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK ); } catch ( Exception $ex ) { // Already logged; move on... - $e = $e ?: $ex; } try { - // Handle callbacks in trxRecurringCallbacks + // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener() $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK ); } catch ( Exception $ex ) { // Already logged; move on... - $e = $e ?: $ex; } - - return $e; } /** @@ -1277,7 +1296,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @throws DBQueryError */ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - if ( $this->getFlag( self::DBO_IGNORE ) || $tempIgnore ) { + if ( $tempIgnore ) { $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" ); } else { $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); @@ -3070,6 +3089,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) { + if ( !$this->trxLevel && $this->getTransactionRoundId() ) { + // Start an implicit transaction similar to how query() does + $this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); + $this->trxAutomatic = true; + } + $this->trxIdleCallbacks[] = [ $callback, $fname ]; if ( !$this->trxLevel ) { $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE ); @@ -3077,10 +3102,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) { - if ( $this->trxLevel || $this->getFlag( self::DBO_TRX ) ) { - // As long as DBO_TRX is set, writes will accumulate until the load balancer issues - // an implicit commit of all peer databases. This is true even if a transaction has - // not yet been triggered by writes; make sure $callback runs *after* any such writes. + if ( !$this->trxLevel && $this->getTransactionRoundId() ) { + // Start an implicit transaction similar to how query() does + $this->begin( __METHOD__, self::TRANSACTION_INTERNAL ); + $this->trxAutomatic = true; + } + + if ( $this->trxLevel ) { $this->trxPreCommitCallbacks[] = [ $callback, $fname ]; } else { // No transaction is active nor will start implicitly, so make one for this callback @@ -3389,10 +3417,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->trxWriteAdjQueryCount = 0; $this->trxWriteCallers = []; // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ. - // Get an estimate of the replica DB lag before then, treating estimate staleness - // as lag itself just to be safe - $status = $this->getApproximateLagStatus(); - $this->trxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] ); + // Get an estimate of the replication lag before any such queries. + $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() // has not yet completed (e.g. setting trxAtomicLevels). @@ -3645,11 +3671,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } /** - * Close existing database connection and open a new connection + * Close any existing (dead) database connection and open a new connection * + * @param string $fname * @return bool True if new connection is opened successfully, false if error */ - protected function reconnect() { + protected function replaceLostConnection( $fname ) { $this->closeConnection(); $this->opened = false; $this->conn = false; @@ -3657,10 +3684,25 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $this->open( $this->server, $this->user, $this->password, $this->dbName ); $this->lastPing = microtime( true ); $ok = true; + + $this->connLogger->warning( + $fname . ': lost connection to {dbserver}; reconnected', + [ + 'dbserver' => $this->getServer(), + 'trace' => ( new RuntimeException() )->getTraceAsString() + ] + ); } catch ( DBConnectionError $e ) { $ok = false; + + $this->connLogger->error( + $fname . ': lost connection to {dbserver} permanently', + [ 'dbserver' => $this->getServer() ] + ); } + $this->handleSessionLoss(); + return $ok; } @@ -3679,7 +3721,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN) * @since 1.27 */ - protected function getTransactionLagStatus() { + final protected function getTransactionLagStatus() { return $this->trxLevel ? [ 'lag' => $this->trxReplicaLag, 'since' => $this->trxTimestamp() ] : null;