Revert "rdbms: make automatic connection recovery apply to more cases"

This reverts commit 4cac31de4e.

Reason for revert: Blocking the train, reverting the chain.

Change-Id: I7f275b3a25379c6f3256e90947c8eed4b232c0f4
This commit is contained in:
Ladsgroup 2022-03-17 18:27:30 +00:00 committed by Amir Sarabadani
parent 226da4c3a0
commit 31c1ca8658
17 changed files with 218 additions and 901 deletions

View file

@ -1815,7 +1815,6 @@ $wgAutoloadLocalClasses = [
'Wikimedia\\Rdbms\\DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyError.php',
'Wikimedia\\Rdbms\\DBReadOnlyRoleError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyRoleError.php',
'Wikimedia\\Rdbms\\DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBReplicationWaitError.php',
'Wikimedia\\Rdbms\\DBSessionStateError' => __DIR__ . '/includes/libs/rdbms/exception/DBSessionStateError.php',
'Wikimedia\\Rdbms\\DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionError.php',
'Wikimedia\\Rdbms\\DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionSizeError.php',
'Wikimedia\\Rdbms\\DBTransactionStateError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionStateError.php',

View file

@ -133,9 +133,7 @@ class MWExceptionHandler {
// to rollback some databases due to connection issues or exceptions.
// However, any sensible DB driver will rollback implicitly anyway.
try {
$lbFactory = $services->getDBLoadBalancerFactory();
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
$lbFactory->flushPrimarySessions( __METHOD__ );
$services->getDBLoadBalancerFactory()->rollbackPrimaryChanges( __METHOD__ );
} catch ( DBError $e2 ) {
// If the DB is unreachable, rollback() will throw an error
// and the error report() method might need messages from the DB,

View file

@ -617,10 +617,6 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
public function flushSession( $fname = __METHOD__, $owner = null ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
return $this->__call( __FUNCTION__, func_get_args() );
}

View file

@ -119,10 +119,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/** @var int[] Prior flags member variable values */
private $priorFlags = [];
/** @var array<string,array> Map of (name => (UNIX time,trx ID)) for current lock() mutexes */
/** @var array<string,float> Map of (name => UNIX timestamp) for locks obtained via lock() */
protected $sessionNamedLocks = [];
/** @var array<string,array> Map of (name => (type,pristine,trx ID)) for current temp tables */
/** @var array Map of (table name => 1) for current TEMPORARY tables */
protected $sessionTempTables = [];
/** @var array Map of (table name => 1) for current TEMPORARY tables */
protected $sessionDirtyTempTables = [];
/** @var array|null Replication lag estimate at the time of BEGIN for the last transaction */
private $trxReplicaLagStatus = null;
@ -163,20 +165,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/** @var int New Database instance will already be connected when returned */
public const NEW_CONNECTED = 1;
/** No errors occurred during the query */
protected const ERR_NONE = 0;
/** Retry query due to a connection loss detected while sending the query (session intact) */
protected const ERR_RETRY_QUERY = 1;
/** Abort query (no retries) due to a statement rollback (session/transaction intact) */
protected const ERR_ABORT_QUERY = 2;
/** Abort any current transaction, by rolling it back, due to an error during the query */
protected const ERR_ABORT_TRX = 4;
/** Abort and reset session due to server-side session-level state loss (locks, temp tables) */
protected const ERR_ABORT_SESSION = 8;
/** Assume that queries taking this long to yield connection loss errors are at fault */
protected const DROPPED_CONN_BLAME_THRESHOLD_SEC = 3.0;
/** @var string Idiom used when a cancelable atomic section started the transaction */
private const NOT_APPLICABLE = 'n/a';
@ -558,32 +546,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
* Get the status of the current transaction
*
* @return int One of the TransactionManager::STATUS_TRX_* class constants
* @return int One of the STATUS_TRX_* class constants
* @since 1.31
* @internal This method should not be used outside of Database/LoadBalancer
*/
public function trxStatus() {
return $this->transactionManager->trxStatus();
}
/**
* Get important session state that cannot be recovered upon connection loss
*
* @return array<string,mixed> Map of (property => value) for the current session state
*/
private function sessionStateInfo() {
return [
'trxId' => $this->transactionManager->getTrxId(),
'trxExplicit' => $this->transactionManager->explicitTrxActive(),
'trxWriteCallers' => $this->transactionManager->pendingWriteCallers(),
'trxPreCommitCbCallers' => $this->transactionManager->pendingPreCommitCallbackCallers(),
'sessNamedLocks' => $this->sessionNamedLocks,
'sessTempTables' => $this->sessionTempTables
];
}
public function tablePrefix( $prefix = null ) {
$old = $this->currentDomain->getTablePrefix();
@ -864,7 +833,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
[
'db_server' => $this->getServerName(),
'db_name' => $this->getDBname(),
'db_user' => $this->connectionParams[self::CONN_USER] ?? null,
'db_user' => $this->connectionParams[self::CONN_USER],
],
$extras
);
@ -1001,7 +970,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*
* - Reject write queries to replica DBs, in query().
*
* @param string $sql SQL query
* @param string $sql
* @param int $flags Query flags to query()
* @return bool
*/
@ -1037,16 +1006,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
* @param string $sql SQL query
* @param string $sql
* @return string|null
*/
protected function getQueryVerb( $sql ) {
// Distinguish ROLLBACK from ROLLBACK TO SAVEPOINT
return preg_match(
'/^\s*(rollback\s+to\s+savepoint|[a-z]+)/i',
$sql,
$m
) ? strtoupper( $m[1] ) : null;
return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
}
/**
@ -1066,18 +1030,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
protected function isTransactableQuery( $sql ) {
return !in_array(
$this->getQueryVerb( $sql ),
[
'BEGIN',
'ROLLBACK',
'ROLLBACK TO SAVEPOINT',
'COMMIT',
'SET',
'SHOW',
'CREATE',
'ALTER',
'USE',
'SHOW'
],
[ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
true
);
}
@ -1130,10 +1083,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
if ( $queryVerb === 'CREATE' ) {
// Record the type of temporary table being created
$tableType = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
} elseif ( isset( $this->sessionTempTables[$table] ) ) {
$tableType = $this->sessionTempTables[$table]['type'];
} else {
$tableType = null;
$tableType = $this->sessionTempTables[$table] ?? null;
}
if ( $tableType !== null ) {
@ -1156,24 +1107,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
switch ( $verb ) {
case 'CREATE':
$this->sessionTempTables[$table] = [
'type' => $tmpTableType,
'pristine' => true,
'trxId' => $this->transactionManager->getTrxId()
];
$this->sessionTempTables[$table] = $tmpTableType;
break;
case 'DROP':
unset( $this->sessionTempTables[$table] );
unset( $this->sessionDirtyTempTables[$table] );
break;
case 'TRUNCATE':
if ( isset( $this->sessionTempTables[$table] ) ) {
$this->sessionTempTables[$table]['pristine'] = true;
}
unset( $this->sessionDirtyTempTables[$table] );
break;
default:
if ( isset( $this->sessionTempTables[$table] ) ) {
$this->sessionTempTables[$table]['pristine'] = false;
}
$this->sessionDirtyTempTables[$table] = 1;
break;
}
}
@ -1189,28 +1133,24 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
protected function isPristineTemporaryTable( $table ) {
$rawTable = $this->tableName( $table, 'raw' );
return isset( $this->sessionTempTables[$rawTable] )
? $this->sessionTempTables[$rawTable]['pristine']
: false;
return (
isset( $this->sessionTempTables[$rawTable] ) &&
!isset( $this->sessionDirtyTempTables[$rawTable] )
);
}
public function query( $sql, $fname = __METHOD__, $flags = self::QUERY_NORMAL ) {
$flags = (int)$flags; // b/c; this field used to be a bool
// Double check that the SQL query is appropriate in the current context and is
// allowed for an outside caller (e.g. does not break session/transaction tracking).
// 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, $errFlags ) = $this->executeQuery( $sql, $fname, $flags );
list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
if ( $ret === false ) {
// An error occurred; log and report it as needed. Errors that corrupt the state of
// the transaction/session cannot be silenced from the client.
$ignore = (
$this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS ) &&
!$this->fieldHasBit( $errFlags, self::ERR_ABORT_SESSION ) &&
!$this->fieldHasBit( $errFlags, self::ERR_ABORT_TRX )
);
$this->reportQueryError( $err, $errno, $sql, $fname, $ignore );
$ignoreErrors = $this->fieldHasBit( $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 $ret;
@ -1226,19 +1166,21 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*
* This is meant for internal use with Database subclasses.
*
* @param string $sql Original SQL statement query
* @param string $sql Original SQL query
* @param string $fname Name of the calling function
* @param int $flags Bit field 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()
* - int: Bit field of ERR_* class constants
* - 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();
if ( $this->isWriteQuery( $sql, $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
@ -1249,16 +1191,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
foreach ( $tempTableChanges as list( $tmpType ) ) {
$isPermWrite = $isPermWrite || ( $tmpType !== self::TEMP_NORMAL );
}
// Permit temporary table writes on replica DB connections
// but require a writable primary DB connection for any persistent writes.
if ( $isPermWrite ) {
$this->assertIsWritablePrimary();
// DBConnRef uses QUERY_REPLICA_ROLE to enforce replica roles for raw SQL queries
// DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
if ( $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
throw new DBReadOnlyRoleError(
$this,
"Cannot write; target role is DB_REPLICA"
);
throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
}
}
} else {
@ -1268,51 +1209,61 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$tempTableChanges = [];
}
// Add agent and calling method comments to the SQL
$commentedSql = $this->getCommentedSql( $sql, $fname );
// How many silent retry attempts are left for recoverable connection loss errors
$retriesLeft = $this->fieldHasBit( $flags, self::QUERY_NO_RETRY ) ? 0 : 1;
// 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).
// NOTE: Don't add varying ids such as request id or session id to the comment.
// It would break aggregation of similar queries in analysis tools (see T193050#7512149)
$encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
$commentedSql = preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
$corruptedTrx = false;
$cs = $this->commenceCriticalSection( __METHOD__ );
do {
// Start a DBO_TRX wrapper transaction as needed (throw an error on failure)
if ( $this->beginIfImplied( $sql, $fname, $flags ) ) {
// Since begin() was called, any connection loss already handled
--$retriesLeft;
}
// Send the query to the server, fetching any results and corresponding errors.
// Silently retry the query if the error was just a recoverable connection loss.
list( $ret, $err, $errno, $errflags ) = $this->executeQueryAttempt(
$sql,
$commentedSql,
$isPermWrite,
$fname,
$flags
);
} while ( $this->fieldHasBit( $errflags, self::ERR_RETRY_QUERY ) && $retriesLeft-- > 0 );
// Send the query to the server and fetch any corresponding errors.
// This also doubles as a "ping" to see if the connection was dropped.
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
$allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
// 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 );
}
// Register creation and dropping of temporary tables
$this->registerTempWrites( $ret, $tempTableChanges );
if ( $ret === false && $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->transactionManager->setTrxStatusIgnoredCause( [ $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
$trxError = $this->getQueryException( $err, $errno, $sql, $fname );
$this->transactionManager->setTransactionError( $trxError );
}
}
$this->completeCriticalSection( __METHOD__, $cs );
return [ $ret, $err, $errno, $errflags ];
return [ $ret, $err, $errno, $corruptedTrx ];
}
/**
* Wrapper for doQuery() that handles a single SQL statement query attempt
* Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count
* tracking, and reconnects (without retry) on query failure due to connection loss
*
* This method handles profiling, debug logging, reconnection and the tracking of:
* - write callers
* - last write time
* - affected row count of the last write
* - whether writes occured in a transaction
*
* This method does *not* handle DBO_TRX transaction logic *nor* query retries.
*
* @param string $sql Original SQL statement query
* @param string $commentedSql SQL statement query with debugging/trace comment
* @param string $sql Original SQL query
* @param string $commentedSql SQL query with debugging/trace comment
* @param bool $isPermWrite Whether the query is a (non-temporary table) write
* @param string $fname Name of the calling function
* @param int $flags Bit field of class QUERY_* constants
@ -1320,12 +1271,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* - mixed|bool: An object, resource, or true on success; false on failure
* - string: The result of calling lastError()
* - int: The result of calling lastErrno()
* - int: Bit field of ERR_* class constants
* - bool: Whether a statement rollback error occurred
* - bool: Whether a disconnect *both* happened *and* was recoverable
* - bool: Whether a reconnection attempt was *both* made *and* succeeded
* @throws DBUnexpectedError
*/
private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
// Transaction attributes before issuing this query
$priorSessInfo = $this->sessionStateInfo();
$priorWritesPending = $this->writesOrCallbacksPending();
if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
$this->beginIfImplied( $sql, $fname );
}
// Keep track of whether the transaction has write queries pending
if ( $isPermWrite ) {
@ -1336,25 +1292,29 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
);
}
$prefix = ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) ? 'query-m: ' : 'query: ';
$generalizedSql = new GeneralizedSql( $sql, $prefix );
$prefix = $this->topologyRole === IDatabase::ROLE_STREAMING_MASTER ? 'query-m: ' : 'query: ';
$generalizedSql = new GeneralizedSql( $commentedSql, $prefix );
$startTime = microtime( true );
$ps = $this->profiler ? ( $this->profiler )( $generalizedSql->stringify() ) : 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 );
$errflags = self::ERR_NONE;
$recoverableSR = false; // recoverable statement rollback?
$recoverableCL = false; // recoverable connection loss?
$reconnected = false; // reconnection both attempted and succeeded?
if ( $ret !== false ) {
$this->lastPing = $startTime;
// Keep track of whether the transaction has write queries pending
if ( $isPermWrite && $this->trxLevel() ) {
$this->transactionManager->updateTrxWriteQueryReport(
$this->getQueryVerb( $sql ),
@ -1364,51 +1324,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
);
}
} elseif ( $this->isConnectionError( $lastErrno ) ) {
// Connection lost before or during the query...
// Determine how to proceed given the lost session state
$connLossFlag = $this->assessConnectionLoss( $sql, $queryRuntime, $priorSessInfo );
// Update session state tracking and try to reestablish a connection
# 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( $lastErrno, __METHOD__ );
// Check if important server-side session-level state was lost
if ( $connLossFlag >= self::ERR_ABORT_SESSION ) {
$sessionError = $this->getQueryException( $lastError, $lastErrno, $sql, $fname );
$this->transactionManager->setSessionError( $sessionError );
}
// Check if important server-side transaction-level state was lost
if ( $connLossFlag >= self::ERR_ABORT_TRX ) {
$trxError = $this->getQueryException( $lastError, $lastErrno, $sql, $fname );
$this->transactionManager->setTransactionError( $trxError );
}
// Check if the query should be retried (having made the reconnection attempt)
if ( $connLossFlag === self::ERR_RETRY_QUERY ) {
$errflags |= ( $reconnected ? self::ERR_RETRY_QUERY : self::ERR_ABORT_QUERY );
} else {
$errflags |= $connLossFlag;
}
} elseif ( $this->isKnownStatementRollbackError( $lastErrno ) ) {
// Query error triggered a server-side statement-only rollback...
$errflags |= self::ERR_ABORT_QUERY;
if ( $this->trxLevel() ) {
// Allow legacy callers to ignore such errors via QUERY_IGNORE_DBO_TRX and
// try/catch. However, a deprecation notice will be logged on the next query.
$cause = [ $lastError, $lastErrno, $fname ];
$this->transactionManager->setTrxStatusIgnoredCause( $cause );
}
} else {
// Some other error occurred during the query...
if ( $this->trxLevel() ) {
// Server-side handling of errors during transactions varies widely depending on
// the RDBMS type and configuration. There are several possible results: (a) the
// whole transaction is rolled back, (b) only the queries after BEGIN are rolled
// back, (c) the transaction is marked as "aborted" and a ROLLBACK is required
// before other queries are permitted. For compatibility reasons, pessimistically
// require a ROLLBACK query (not using SAVEPOINT) before allowing other queries.
$trxError = $this->getQueryException( $lastError, $lastErrno, $sql, $fname );
$this->transactionManager->setTransactionError( $trxError );
$errflags |= self::ERR_ABORT_TRX;
} else {
$errflags |= self::ERR_ABORT_QUERY;
}
# Check if only the last query was rolled back
$recoverableSR = $this->wasKnownStatementRollbackError();
}
if ( $sql === self::PING_QUERY ) {
@ -1447,22 +1369,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
static::class . '::doQuery() should return an IResultWrapper' );
}
return [ $ret, $lastError, $lastErrno, $errflags ];
}
/**
* @param string $sql
* @param string $fname
* @return string
*/
private function getCommentedSql( $sql, $fname ) {
// 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).
// NOTE: Don't add varying ids such as request id or session id to the comment.
// It would break aggregation of similar queries in analysis tools (see T193050#7512149)
$encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
return preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
}
/**
@ -1470,24 +1377,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*
* @param string $sql
* @param string $fname
* @param int $flags
* @return bool Whether an implicit transaction was started
* @throws DBError
*/
private function beginIfImplied( $sql, $fname, $flags ) {
private function beginIfImplied( $sql, $fname ) {
if (
!$this->fieldHasBit( $flags, self::QUERY_IGNORE_DBO_TRX ) &&
!$this->trxLevel() &&
$this->getFlag( self::DBO_TRX ) &&
$this->isTransactableQuery( $sql )
) {
$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
$this->transactionManager->turnOnAutomatic();
return true;
}
return false;
}
/**
@ -1500,119 +1399,60 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*/
private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
$verb = $this->getQueryVerb( $sql );
if ( $verb === 'USE' ) {
throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
}
if ( $verb === 'ROLLBACK' ) {
// Whole transaction rollback is used for recovery
if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
return;
}
if ( $this->csmError ) {
throw new DBTransactionStateError(
$this,
"Cannot execute query from $fname while session state is out of sync",
[],
$this->csmError
"Cannot execute query from $fname while session state is out of sync.\n\n" .
$this->csmError->getMessage() . "\n" .
$this->csmError->getTraceAsString()
);
}
$this->transactionManager->assertSessionStatus( $this, $fname );
if ( $verb !== 'ROLLBACK TO SAVEPOINT' ) {
$this->transactionManager->assertTransactionStatus(
$this,
$this->deprecationLogger,
$fname
);
}
$this->transactionManager->assertTransactionStatus( $this, $this->deprecationLogger, $fname );
}
/**
* Determine how to handle a connection lost discovered during a query attempt
* Determine whether it is safe to retry queries after a database connection is lost
*
* This checks if explicit transactions, pending transaction writes, and important
* session-level state (locks, temp tables) was lost. Point-in-time read snapshot loss
* is considered acceptable for DBO_TRX logic.
*
* If state was lost, but that loss was discovered during a ROLLBACK that would have
* destroyed that state anyway, treat the error as recoverable.
*
* @param string $sql SQL query statement that encountered or caused the connection loss
* @param float $walltime How many seconds passes while attempting the query
* @param array $priorSessInfo Result of sessionStateInfo(), just before the query
* @return int Recovery approach. One of the following ERR_* class constants:
* - Database::ERR_RETRY_QUERY: reconnect silently, retry query
* - Database::ERR_ABORT_QUERY: reconnect silently, do not retry query
* - Database::ERR_ABORT_TRX: reconnect, throw error, enforce transaction rollback
* - Database::ERR_ABORT_SESSION: reconnect, throw error, enforce session rollback
* @param string $sql SQL query
* @param bool $priorWritesPending Whether there is a transaction open with
* possible write queries or transaction pre-commit/idle callbacks
* waiting on it to finish.
* @return bool True if it is safe to retry the query, false otherwise
*/
private function assessConnectionLoss( string $sql, float $walltime, array $priorSessInfo ) {
$verb = $this->getQueryVerb( $sql );
if ( $walltime < self::DROPPED_CONN_BLAME_THRESHOLD_SEC ) {
// Query failed quickly; the connection was probably lost before the query was sent
$res = self::ERR_RETRY_QUERY;
} else {
// Query took a long time; the connection was probably lost during query execution
$res = self::ERR_ABORT_QUERY;
}
// List of problems causing session/transaction state corruption
private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
$blockers = [];
// Loss of named locks breaks future callers relying on those locks for critical sections
foreach ( $priorSessInfo['sessNamedLocks'] as $lockName => $lockInfo ) {
if ( $lockInfo['trxId'] && $lockInfo['trxId'] === $priorSessInfo['trxId'] ) {
// Treat lost locks acquired during the lost transaction as a transaction state
// problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
// rollback automatically triggered server-side.
if ( $verb !== 'ROLLBACK' ) {
$res = max( $res, self::ERR_ABORT_TRX );
$blockers[] = "named lock '$lockName'";
}
} else {
// Treat lost locks acquired either during prior transactions or during no
// transaction as a session state problem.
$res = max( $res, self::ERR_ABORT_SESSION );
$blockers[] = "named lock '$lockName'";
}
if ( $this->sessionNamedLocks ) {
// Named locks were automatically released, breaking the expectations
// of callers relying on those locks for critical section enforcement
$blockers[] = 'named locks';
}
// Loss of temp tables breaks future callers relying on those tables for queries
foreach ( $priorSessInfo['sessTempTables'] as $tableName => $tableInfo ) {
if ( $tableInfo['trxId'] && $tableInfo['trxId'] === $priorSessInfo['trxId'] ) {
// Treat lost temp tables created during the lost transaction as a transaction
// state problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
// rollback automatically triggered server-side.
if ( $verb !== 'ROLLBACK' ) {
$res = max( $res, self::ERR_ABORT_TRX );
$blockers[] = "temp table '$tableName'";
}
} else {
// Treat lost temp tables created either during prior transactions or during
// no transaction as a session state problem.
$res = max( $res, self::ERR_ABORT_SESSION );
$blockers[] = "temp table '$tableName'";
}
if ( $this->sessionTempTables ) {
// Temp tables were automatically dropped, breaking the expectations
// of callers relying on those tables having been created/populated
$blockers[] = 'temp tables';
}
// Loss of transaction writes breaks future callers and DBO_TRX logic relying on those
// writes to be atomic and still pending. Connection loss on ROLLBACK (non-SAVEPOINT) is
// tolerable since rollback automatically triggered server-side.
if ( $priorSessInfo['trxWriteCallers'] && $verb !== 'ROLLBACK' ) {
$res = max( $res, self::ERR_ABORT_TRX );
$blockers[] = 'uncommitted writes';
if ( $priorWritesPending && $sql !== 'ROLLBACK' ) {
// Transaction was automatically rolled back, breaking the expectations
// of callers and DBO_TRX semantics relying on that transaction to provide
// atomic writes (point-in-time snapshot loss is acceptable for DBO_TRX)
$blockers[] = 'transaction writes';
}
if ( $priorSessInfo['trxPreCommitCbCallers'] && $verb !== 'ROLLBACK' ) {
$res = max( $res, self::ERR_ABORT_TRX );
$blockers[] = 'pre-commit callbacks';
}
if ( $priorSessInfo['trxExplicit'] && $verb !== 'ROLLBACK' && $sql !== 'COMMIT' ) {
// Transaction automatically rolled back, breaking the expectations of callers
// relying on that transaction to provide atomic writes, serializability, or use
// one point-in-time snapshot for all reads. Assume that connection loss is OK
// with ROLLBACK (non-SAVEPOINT). Likewise for COMMIT (T127428).
$res = max( $res, self::ERR_ABORT_TRX );
if ( $this->transactionManager->explicitTrxActive() && $sql !== 'ROLLBACK' && $sql !== 'COMMIT' ) {
// Transaction was automatically rolled back, breaking the expectations of
// callers relying on that transaction to provide atomic writes, serializability,
// or read results consistent with a single point-in-time snapshot. Disconnection
// on ROLLBACK is not an issue, since the intended result of rolling back the
// transaction was in fact achieved. Disconnection on COMMIT of an empty transaction
// is also not an issue, for similar reasons (T127428).
$blockers[] = 'explicit transaction';
}
@ -1625,9 +1465,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
'db_log_category' => 'connection'
] )
);
return false;
}
return $res;
return true;
}
/**
@ -1638,6 +1480,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
// https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
// https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
$this->sessionTempTables = [];
$this->sessionDirtyTempTables = [];
// 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->sessionNamedLocks = [];
@ -1664,11 +1507,25 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
// Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
// If callback suppression is set then the array will remain unhandled.
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
// Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener().
// If callback suppression is set then the array will remain unhandled.
// Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
}
/**
* Checks whether the cause of the error is detected to be a timeout.
*
* It returns false by default, and not all engines support detecting this yet.
* If this returns false, it will be treated as a generic query error.
*
* @stable to override
* @param string $error Error text
* @param int $errno Error number
* @return bool
*/
protected function wasQueryTimeout( $error, $errno ) {
return false;
}
/**
* Report a query error. Log the error, and if neither the object ignore
* flag nor the $ignoreErrors flag is set, throw a DBQueryError.
@ -1721,7 +1578,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @return DBError
*/
private function getQueryException( $error, $errno, $sql, $fname ) {
if ( $this->isQueryTimeoutError( $errno ) ) {
if ( $this->wasQueryTimeout( $error, $errno ) ) {
return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
} elseif ( $this->isConnectionError( $errno ) ) {
return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
@ -4040,35 +3897,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @stable to override
* @param int|string $errno
* @return bool Whether the given query error was a connection drop
* @since 1.38
*/
protected function isConnectionError( $errno ) {
return false;
}
/**
* Checks whether the cause of the error is detected to be a timeout.
*
* It returns false by default, and not all engines support detecting this yet.
* If this returns false, it will be treated as a generic query error.
*
* @stable to override
* @param int $errno Error number
* @return bool
*/
protected function isQueryTimeoutError( $errno ) {
return false;
}
/**
* @stable to override
* @param int|string $errno
* @return bool Whether it is known that the last query 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 foreign key violations
* @since 1.31
*/
protected function isKnownStatementRollbackError( $errno ) {
protected function wasKnownStatementRollbackError() {
return false; // don't know; it could have caused a transaction rollback
}
@ -4540,12 +4381,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
);
}
} else {
// Put the transaction into an error state if it's not already in one
$trxError = new DBUnexpectedError(
$this,
"Uncancelable atomic section canceled (got $fname)"
);
$this->transactionManager->setTransactionError( $trxError );
$this->transactionManager->setTransactionErrorFromStatus( $this, $fname );
}
} finally {
// Fix up callbacks owned by the sections that were just cancelled.
@ -4690,15 +4526,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
if ( !$this->trxLevel() ) {
$this->transactionManager->setTrxStatusToNone();
$this->transactionManager->clearPreEndCallbacks();
if ( $this->transactionManager->trxLevel() <= TransactionManager::STATUS_TRX_ERROR ) {
$this->connLogger->info(
"$fname: acknowledged server-side transaction loss on {db_server}",
$this->getLogContext()
);
}
return;
}
@ -4739,53 +4567,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$this->transactionManager = $transactionManager;
}
public function flushSession( $fname = __METHOD__, $owner = null ) {
if ( $this->ownerId !== null && $owner !== $this->ownerId ) {
throw new DBUnexpectedError(
$this,
"$fname: Database is owned by ID '{$this->ownerId}' (got '$owner')"
);
}
if ( $this->trxLevel() ) {
// Any existing transaction should have been rolled back already
throw new DBUnexpectedError(
$this,
"$fname: transaction still in progress (not yet rolled back)"
);
}
// If the session state was already lost due to either an unacknowledged session
// state loss error (e.g. dropped connection) or an explicit connection close call,
// then there is nothing to do here. Note that such cases, even temporary tables and
// server-side config variables are lost (the invocation of this method is assumed to
// imply that such losses are tolerable).
if ( $this->transactionManager->sessionStatus() <= TransactionManager::STATUS_SESS_ERROR ) {
$this->connLogger->info(
"$fname: acknowledged server-side session loss on {db_server}",
$this->getLogContext()
);
} elseif ( $this->isOpen() ) {
// Connection handle exists; server-side session state must be flushed
$this->doFlushSession( $fname );
$this->sessionNamedLocks = [];
}
$this->transactionManager->clearSessionError();
}
/**
* Reset the server-side session state for named locks and table locks
*
* Connection and query errors will be suppressed and logged
*
* @param string $fname
* @since 1.38
*/
protected function doFlushSession( $fname ) {
// no-op
}
public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
$this->transactionManager->onFlushSnapshot( $this, $fname, $flush, $this->getTransactionRoundId() );
$this->commit( $fname, self::FLUSHING_INTERNAL );
@ -4933,15 +4714,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/**
* Get the replica DB lag when the current transaction started
*
* This is useful given that transactions might use point-in-time read snapshots,
* in which case the lag estimate should be recorded just before the transaction
* establishes the read snapshot (either BEGIN or the first SELECT/write query).
* This is useful when transactions might use snapshot isolation
* (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
* 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.
*
* If snapshots are not used, it is still safe to be pessimistic.
* This returns null if the lag status for this transaction was not yet recorded.
*
* This returns null if there is no transaction or the lag status was not yet recorded.
*
* @return array|null ('lag': seconds or false, 'since': UNIX timestamp of BEGIN) or null
* @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
* @since 1.27
*/
final protected function getRecordedTransactionLagStatus() {
@ -5307,19 +5087,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
$lockTsUnix = $this->doLock( $lockName, $method, $timeout );
if ( $lockTsUnix !== null ) {
$locked = true;
$this->sessionNamedLocks[$lockName] = [
'ts' => $lockTsUnix,
'trxId' => $this->transactionManager->getTrxId()
];
$this->sessionNamedLocks[$lockName] = $lockTsUnix;
} else {
$locked = false;
$this->queryLogger->info(
__METHOD__ . " failed to acquire lock '{lockname}'",
$this->queryLogger->info( __METHOD__ . " failed to acquire lock '{lockname}'",
[
'lockname' => $lockName,
'db_log_category' => 'locking'
]
);
] );
}
if ( $this->fieldHasBit( $flags, self::LOCK_TIMESTAMP ) ) {
@ -5629,13 +5404,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* try {
* //...send a query that changes the session/transaction state...
* } catch ( DBError $e ) {
* // Rely on assertQueryIsCurrentlyAllowed()/canRecoverFromDisconnect() to ensure
* // the rollback of incomplete transactions and the prohibition of reconnections
* // that mask a loss of session state (e.g. named locks and temp tables)
* $this->completeCriticalSection( __METHOD__, $cs );
* throw $expectedException;
* }
* try {
* //...send another query that changes the session/transaction state...
* } catch ( DBError $trxError ) {
* // Require ROLLBACK before allowing any other queries from outside callers
* // Inform assertQueryIsCurrentlyAllowed() that the transaction must be rolled
* // back (e.g. even if the error was a pre-query check or normally recoverable)
* $this->completeCriticalSection( __METHOD__, $cs, $trxError );
* throw $expectedException;
* }

View file

@ -170,12 +170,11 @@ abstract class DatabaseMysqlBase extends Database {
// @phan-suppress-next-next-line PhanRedundantCondition
// If kept for safety and to avoid broken query
if ( $set ) {
$sql = 'SET ' . implode( ', ', $set );
$flags = self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX;
list( $ret, $err, $errno ) = $this->executeQuery( $sql, __METHOD__, $flags );
if ( $ret === false ) {
$this->reportQueryError( $err, $errno, $sql, __METHOD__ );
}
$this->query(
'SET ' . implode( ', ', $set ),
__METHOD__,
self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX
);
}
} catch ( RuntimeException $e ) {
throw $this->newExceptionAfterConnectError( $e->getMessage() );
@ -261,6 +260,12 @@ abstract class DatabaseMysqlBase extends Database {
*/
abstract protected function mysqlError( $conn = null );
protected function wasQueryTimeout( $error, $errno ) {
// https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
// https://phabricator.wikimedia.org/T170638
return in_array( $errno, [ 2062, 3024 ] );
}
protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
$row = $this->getReplicationSafetyInfo();
// For row-based-replication, the resulting changes will be relayed, not the query
@ -1114,24 +1119,6 @@ abstract class DatabaseMysqlBase extends Database {
return true;
}
protected function doFlushSession( $fname ) {
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS | self::QUERY_NO_RETRY;
// In MySQL, ROLLBACK does not automatically release table locks;
// https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
$sql = "UNLOCK TABLES";
list( $res, $err, $errno ) = $this->executeQuery( $sql, $fname, $flags );
if ( $res === false ) {
$this->reportQueryError( $err, $errno, $sql, $fname, true );
}
$sql = "RELEASE_ALL_LOCKS()";
list( $res, $err, $errno ) = $this->executeQuery( $sql, __METHOD__, $flags );
if ( $res === false ) {
$this->reportQueryError( $err, $errno, $sql, $fname, true );
}
}
/**
* @param bool $value
*/
@ -1239,20 +1226,12 @@ abstract class DatabaseMysqlBase extends Database {
}
protected function isConnectionError( $errno ) {
// https://mariadb.com/kb/en/mariadb-error-codes/
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ], true );
return $errno == 2013 || $errno == 2006;
}
protected function isQueryTimeoutError( $errno ) {
// https://mariadb.com/kb/en/mariadb-error-codes/
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
return in_array( $errno, [ 3024, 1969 ], true );
}
protected function wasKnownStatementRollbackError() {
$errno = $this->lastErrno();
protected function isKnownStatementRollbackError( $errno ) {
// https://mariadb.com/kb/en/mariadb-error-codes/
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
if ( $errno === 1205 ) { // lock wait timeout
// Note that this is uncached to avoid stale values of SET is used
$res = $this->query(
@ -1265,7 +1244,9 @@ abstract class DatabaseMysqlBase extends Database {
// https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
return ( $row && !$row->Value );
}
return in_array( $errno, [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ], true );
// See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
return in_array( $errno, [ 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ], true );
}
/**

View file

@ -661,12 +661,7 @@ __INDEXATTR__;
return in_array( $errno, $codes, true );
}
protected function isQueryTimeoutError( $errno ) {
// https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
return ( $errno === '57014' );
}
protected function isKnownStatementRollbackError( $errno ) {
protected function wasKnownStatementRollbackError() {
return false; // transaction has to be rolled-back from error state
}
@ -1334,20 +1329,6 @@ SQL;
return ( $row->released === 't' );
}
protected function doFlushSession( $fname ) {
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS | self::QUERY_NO_RETRY;
// In Postgres, ROLLBACK already releases table locks;
// https://www.postgresql.org/docs/9.4/sql-lock.html
// https://www.postgresql.org/docs/9.1/functions-admin.html
$sql = "pg_advisory_unlock_all()";
list( $res, $err, $errno ) = $this->executeQuery( $sql, __METHOD__, $flags );
if ( $res === false ) {
$this->reportQueryError( $err, $errno, $sql, $fname, true );
}
}
public function serverIsReadOnly() {
$res = $this->query(
"SHOW default_transaction_read_only",

View file

@ -83,11 +83,20 @@ class DatabaseSqlite extends Database {
} elseif ( isset( $params['dbDirectory'] ) ) {
$this->dbDir = $params['dbDirectory'];
}
parent::__construct( $params );
$this->trxMode = strtoupper( $params['trxMode'] ?? '' );
$this->initLockManager();
$lockDirectory = $this->getLockFileDirectory();
if ( $lockDirectory !== null ) {
$this->lockMgr = new FSLockManager( [
'domain' => $this->getDomainID(),
'lockDirectory' => $lockDirectory
] );
} else {
$this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
}
}
protected static function getAttributes() {
@ -224,29 +233,12 @@ class DatabaseSqlite extends Database {
return null;
}
/**
* Initialize/reset the lock manager instance
*/
private function initLockManager() {
$lockDirectory = $this->getLockFileDirectory();
if ( $lockDirectory !== null ) {
$this->lockMgr = new FSLockManager( [
'domain' => $this->getDomainID(),
'lockDirectory' => $lockDirectory
] );
} else {
$this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
}
}
/**
* Does not actually close the connection, just destroys the reference for GC to do its work
* @return bool
*/
protected function closeConnection() {
$this->conn = null;
// Release all locks, via FSLockManager::__destruct, as the baset class expects
$this->lockMgr = null;
return true;
}
@ -628,7 +620,7 @@ class DatabaseSqlite extends Database {
return $errno == 17; // SQLITE_SCHEMA;
}
protected function isKnownStatementRollbackError( $errno ) {
protected function wasKnownStatementRollbackError() {
// ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
// ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
// https://sqlite.org/lang_createtable.html#uniqueconst
@ -1080,17 +1072,6 @@ class DatabaseSqlite extends Database {
protected function doHandleSessionLossPreconnect() {
$this->sessionAttachedDbs = [];
// Release all locks, via FSLockManager::__destruct, as the baset class expects
$this->lockMgr = null;
// Create a new lock manager instance
$this->initLockManager();
}
protected function doFlushSession( $fname ) {
// Release all locks, via FSLockManager::__destruct, as the baset class expects
$this->lockMgr = null;
// Create a new lock manager instance
$this->initLockManager();
}
/**

View file

@ -1977,30 +1977,14 @@ interface IDatabase {
*
* @param string $fname Calling function name
* @param string $flush Flush flag, set to a situationally valid IDatabase::FLUSHING_*
* constant to disable warnings about explicitly rolling back implicit transactions.
* This will silently break any ongoing explicit transaction. Only set the flush flag
* if you are sure that it is safe to ignore these warnings in your context.
* constant to disable warnings about calling rollback when no transaction is in
* progress. This will silently break any ongoing explicit transaction. Only set the
* flush flag if you are sure that it is safe to ignore these warnings in your context.
* @throws DBError If an error occurs, {@see query}
* @since 1.23 Added $flush parameter
*/
public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE );
/**
* Release important session-level state (named lock, table locks) as post-rollback cleanup
*
* This should only be called by a load balancer or if the handle is not attached to one.
* Also, there must be no chance that a future caller will still be expecting some of the
* lost session state.
*
* Connection and query errors will be suppressed and logged
*
* @param string $fname Calling function name
* @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
* @throws DBError If an error occurs, {@see query}
* @since 1.38
*/
public function flushSession( $fname = __METHOD__, $owner = null );
/**
* Commit any transaction but error out if writes or callbacks are pending
*

View file

@ -225,11 +225,12 @@ interface IMaintainableDatabase extends IDatabase {
);
/**
* Check if lockTables() locks are transaction-level locks instead of session-level
* Checks if table locks acquired by lockTables() are transaction-bound in their scope
*
* Transaction-level table locks can only be acquired within a transaction and get released
* when that transaction terminates. Session-level table locks are acquired outside of any
* transaction and are incompatible with transactions.
* Transaction-bound table locks will be released when the current transaction terminates.
* Table locks that are not bound to a transaction are not effected by BEGIN/COMMIT/ROLLBACK
* and will last until either lockTables()/unlockTables() is called or the TCP connection to
* the database is closed.
*
* @return bool
* @since 1.29

View file

@ -40,11 +40,6 @@ class TransactionManager {
/** @var int No transaction is active */
public const STATUS_TRX_NONE = 3;
/** Session is in a error state requiring a reset */
public const STATUS_SESS_ERROR = 1;
/** Session is in a normal state */
public const STATUS_SESS_OK = 2;
/** @var float Guess of how many seconds it takes to replicate a small insert */
private const TINY_WRITE_SEC = 0.010;
/** @var float Consider a write slow if it took more than this many seconds */
@ -61,14 +56,11 @@ class TransactionManager {
private $trxTimestamp = null;
/** @var int Transaction status */
private $trxStatus = self::STATUS_TRX_NONE;
/** @var Throwable|null The cause of any unresolved transaction state error, or, null */
/** @var Throwable|null The last error that caused the status to become STATUS_TRX_ERROR */
private $trxStatusCause;
/** @var array|null Details of the last statement-rollback error for the last transaction */
/** @var array|null Error details of the last statement-only rollback */
private $trxStatusIgnoredCause;
/** @var Throwable|null The cause of any unresolved session state loss error, or, null */
private $sessionError;
/** @var string[] Write query callers of the current transaction */
private $trxWriteCallers = [];
/** @var float Seconds spent in write queries for the current transaction */
@ -126,13 +118,6 @@ class TransactionManager {
return ( $this->trxId != '' ) ? 1 : 0;
}
/**
* @return string
*/
public function getTrxId(): string {
return $this->trxId;
}
/**
* TODO: This should be removed once all usages have been migrated here
* @param string $mode One of IDatabase::TRANSACTION_* values
@ -143,7 +128,6 @@ class TransactionManager {
$nextTrxId = ( $nextTrxId !== null ? $nextTrxId++ : mt_rand() ) % 0xffff;
$this->trxId = sprintf( '%06x', mt_rand( 0, 0xffffff ) ) . sprintf( '%04x', $nextTrxId );
$this->trxStatus = self::STATUS_TRX_OK;
$this->trxStatusCause = null;
$this->trxStatusIgnoredCause = null;
$this->trxWriteDuration = 0.0;
$this->trxWriteQueryCount = 0;
@ -190,14 +174,11 @@ class TransactionManager {
public function setTrxStatusToOk() {
$this->trxStatus = self::STATUS_TRX_OK;
$this->trxStatusCause = null;
$this->trxStatusIgnoredCause = null;
}
public function setTrxStatusToNone() {
$this->trxStatus = self::STATUS_TRX_NONE;
$this->trxStatusCause = null;
$this->trxStatusIgnoredCause = null;
}
public function assertTransactionStatus( IDatabase $db, $deprecationLogger, $fname ) {
@ -218,14 +199,14 @@ class TransactionManager {
}
}
public function assertSessionStatus( IDatabase $db, $fname ) {
if ( $this->sessionError ) {
throw new DBSessionStateError(
public function setTransactionErrorFromStatus( $db, $fname ) {
if ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
// Put the transaction into an error state if it's not already in one
$trxError = new DBUnexpectedError(
$db,
"Cannot execute query from $fname while session status is ERROR",
[],
$this->sessionError
"Uncancelable atomic section canceled (got $fname)"
);
$this->setTransactionError( $trxError );
}
}
@ -248,32 +229,6 @@ class TransactionManager {
$this->trxStatusIgnoredCause = $trxStatusIgnoredCause;
}
/**
* Get the status of the current session (ephemeral server-side state tied to the connection)
*
* @return int One of the STATUS_SESSION_* class constants
*/
public function sessionStatus() {
// Check if an unresolved error still exists
return ( $this->sessionError ) ? self::STATUS_SESS_ERROR : self::STATUS_SESS_OK;
}
/**
* Flag the session as needing a reset due to an error, if not already flagged
*
* @param Throwable $sessionError
*/
public function setSessionError( Throwable $sessionError ) {
$this->sessionError = $this->sessionError ?? $sessionError;
}
/**
* Unflag the session as needing a reset due to an error
*/
public function clearSessionError() {
$this->sessionError = null;
}
/**
* @param int $rtt
* @return float Time to apply writes to replicas based on trxWrite* fields
@ -871,20 +826,6 @@ class TransactionManager {
return $fnames;
}
/**
* List the methods that have precommit callbacks for the current transaction
*
* @return string[]
*/
public function pendingPreCommitCallbackCallers(): array {
$fnames = $this->pendingWriteCallers();
foreach ( $this->trxPreCommitOrIdleCallbacks as $callback ) {
$fnames[] = $callback[1];
}
return $fnames;
}
public function isEndCallbacksSuppressed(): bool {
return $this->trxEndCallbacksSuppressed;
}

View file

@ -1,29 +0,0 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Database
*/
namespace Wikimedia\Rdbms;
/**
* @newable
* @ingroup Database
*/
class DBSessionStateError extends DBTransactionError {
}

View file

@ -282,19 +282,6 @@ interface ILBFactory {
*/
public function rollbackPrimaryChanges( $fname = __METHOD__ );
/**
* Release important session-level state (named lock, table locks) as post-rollback cleanup
*
* This only applies to the instantiated tracked load balancer instances.
*
* This should only be called by application entry point functions, since there must be
* no chance that a future caller will still be expecting some of the lost session state.
*
* @param string $fname Caller name
* @since 1.37
*/
public function flushPrimarySessions( $fname = __METHOD__ );
/**
* Check if an explicit transaction round is active
*

View file

@ -121,7 +121,6 @@ abstract class LBFactory implements ILBFactory {
private const ROUND_ROLLING_BACK = 'within-rollback';
private const ROUND_COMMIT_CALLBACKS = 'within-commit-callbacks';
private const ROUND_ROLLBACK_CALLBACKS = 'within-rollback-callbacks';
private const ROUND_ROLLBACK_SESSIONS = 'within-rollback-session';
private static $loggerFields =
[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
@ -337,16 +336,6 @@ abstract class LBFactory implements ILBFactory {
$this->trxRoundStage = self::ROUND_CURSORY;
}
final public function flushPrimarySessions( $fname = __METHOD__ ) {
/** @noinspection PhpUnusedLocalVariableInspection */
$scope = ScopedCallback::newScopedIgnoreUserAbort();
// Release named locks and table locks on all primary DB connections
$this->trxRoundStage = self::ROUND_ROLLBACK_SESSIONS;
$this->forEachLBCallMethod( 'flushPrimarySessions', [ $fname, $this->id ] );
$this->trxRoundStage = self::ROUND_CURSORY;
}
/**
* @return Exception|null
*/

View file

@ -636,7 +636,6 @@ interface ILoadBalancer {
/**
* Issue ROLLBACK only on primary, only if queries were done on connection
*
* @param string $fname Caller name
* @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
* @throws DBExpectedError
@ -644,18 +643,6 @@ interface ILoadBalancer {
*/
public function rollbackPrimaryChanges( $fname = __METHOD__, $owner = null );
/**
* Release/destroy session-level named locks, table locks, and temp tables
*
* Only call this function right after calling rollbackPrimaryChanges()
*
* @param string $fname Caller name
* @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
* @throws DBExpectedError
* @since 1.38
*/
public function flushPrimarySessions( $fname = __METHOD__, $owner = null );
/**
* Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshots
*

View file

@ -1910,11 +1910,9 @@ class LoadBalancer implements ILoadBalancer {
$restore = ( $this->trxRoundId !== false );
$this->trxRoundId = false;
$this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise
$this->forEachOpenPrimaryConnection(
static function ( IDatabase $conn ) use ( $fname ) {
$conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
}
);
$this->forEachOpenPrimaryConnection( static function ( IDatabase $conn ) use ( $fname ) {
$conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
} );
if ( $restore ) {
// Unmark handles as participating in this explicit transaction round
$this->forEachOpenPrimaryConnection( function ( Database $conn ) {
@ -1924,20 +1922,6 @@ class LoadBalancer implements ILoadBalancer {
$this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
}
public function flushPrimarySessions( $fname = __METHOD__, $owner = null ) {
$this->assertTransactionRoundStage( [ self::ROUND_CURSORY ] );
if ( $this->hasPrimaryChanges() ) {
// Any transaction should have been rolled back beforehand
throw new DBTransactionError( null, "Cannot reset session while writes are pending" );
}
$this->forEachOpenPrimaryConnection(
static function ( IDatabase $conn ) use ( $fname ) {
$conn->flushSession( $fname );
}
);
}
/**
* @param string|string[] $stage
* @throws DBTransactionError

View file

@ -195,10 +195,8 @@ class DatabaseTestHelper extends Database {
return $this->lastError ? $this->lastError['error'] : 'test';
}
protected function isKnownStatementRollbackError( $errno ) {
return ( $this->lastError['errno'] ?? 0 ) === $errno
? ( $this->lastError['wasKnownStatementRollbackError'] ?? false )
: false;
protected function wasKnownStatementRollbackError() {
return $this->lastError['wasKnownStatementRollbackError'] ?? false;
}
public function fieldInfo( $table, $field ) {

View file

@ -1,240 +0,0 @@
<?php
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DBQueryDisconnectedError;
use Wikimedia\Rdbms\DBQueryTimeoutError;
use Wikimedia\Rdbms\DBSessionStateError;
use Wikimedia\Rdbms\DBTransactionStateError;
use Wikimedia\Rdbms\DBUnexpectedError;
use Wikimedia\Rdbms\TransactionManager;
/**
* @group mysql
* @group Database
* @group medium
*/
class DatabaseMysqlTest extends \MediaWikiIntegrationTestCase {
/** @var DatabaseMysqlBase */
protected $conn;
protected function setUp(): void {
parent::setUp();
if ( !extension_loaded( 'mysqli' ) ) {
$this->markTestSkipped( 'No MySQL support detected' );
}
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
if ( $lb->getServerType( 0 ) !== 'mysql' ) {
$this->markTestSkipped( 'No MySQL $wgLBFactoryConf config detected' );
}
$this->conn = $this->newConnection();
}
/**
* @covers Database::query()
*/
public function testQueryTimeout() {
try {
$this->conn->query(
'SET STATEMENT max_statement_time=0.001 FOR SELECT sleep(1) FROM dual',
__METHOD__
);
$this->fail( "No DBQueryTimeoutError caught" );
} catch ( DBQueryTimeoutError $e ) {
$this->assertInstanceOf( DBQueryTimeoutError::class, $e );
}
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->assertSame( 'x', $row->v, "Still connected/usable" );
}
/**
* @covers Database::query()
*/
public function testConnectionKill() {
try {
$this->conn->query( 'KILL (SELECT connection_id())', __METHOD__ );
$this->fail( "No DBQueryDisconnectedError caught" );
} catch ( DBQueryDisconnectedError $e ) {
$this->assertInstanceOf( DBQueryDisconnectedError::class, $e );
}
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->assertSame( 'x', $row->v, "Recovered" );
}
/**
* @covers Database::query()
* @covers Database::rollback()
* @covers Database::flushSession()
*/
public function testConnectionLoss() {
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
$encId = intval( $row->id );
$adminConn = $this->newConnection();
$adminConn->query( "KILL $encId", __METHOD__ );
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->assertSame( 'x', $row->v, "Recovered" );
$this->conn->startAtomic( __METHOD__ );
$this->assertSame( 1, $this->conn->trxLevel(), "Transaction exists" );
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
$encId = intval( $row->id );
$adminConn->query( "KILL $encId", __METHOD__ );
try {
$this->conn->query( 'SELECT 1', __METHOD__ );
$this->fail( "No DBQueryDisconnectedError caught" );
} catch ( DBQueryDisconnectedError $e ) {
$this->assertTrue( $this->conn->isOpen(), "Reconnected" );
try {
$this->conn->endAtomic( __METHOD__ );
$this->fail( "No DBUnexpectedError caught" );
} catch ( DBUnexpectedError $e ) {
$this->assertInstanceOf( DBUnexpectedError::class, $e );
}
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
try {
$this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->fail( "No DBTransactionStateError caught" );
} catch ( DBTransactionStateError $e ) {
$this->assertInstanceOf( DBTransactionStateError::class, $e );
}
$this->assertSame( 0, $this->conn->trxLevel(), "Transaction lost" );
$this->conn->rollback( __METHOD__ );
$this->assertSame( 0, $this->conn->trxLevel(), "No transaction" );
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->assertSame( 'x', $row->v, "Recovered" );
}
$this->conn->lock( 'session_lock', __METHOD__, 0 );
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
$encId = intval( $row->id );
$adminConn->query( "KILL $encId", __METHOD__ );
try {
$this->conn->query( 'SELECT 1', __METHOD__ );
$this->fail( "No DBQueryDisconnectedError caught" );
} catch ( DBQueryDisconnectedError $e ) {
$this->assertInstanceOf( DBQueryDisconnectedError::class, $e );
}
$this->assertTrue( $this->conn->isOpen(), "Reconnected" );
try {
$this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->fail( "No DBSessionStateError caught" );
} catch ( DBSessionStateError $e ) {
$this->assertInstanceOf( DBSessionStateError::class, $e );
}
$this->assertTrue( $this->conn->isOpen(), "Connection remains" );
$this->conn->rollback( __METHOD__ );
$this->conn->flushSession( __METHOD__ );
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->assertSame( 'x', $row->v, "Recovered" );
$adminConn->close( __METHOD__ );
}
/**
* @covers Database::query()
* @covers Database::cancelAtomic()
* @covers Database::rollback()
* @covers Database::flushSession()
*/
public function testTransactionError() {
$this->assertSame( TransactionManager::STATUS_TRX_NONE, $this->conn->trxStatus() );
$this->conn->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
$this->assertSame( TransactionManager::STATUS_TRX_OK, $this->conn->trxStatus() );
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
$encId = intval( $row->id );
try {
$this->conn->lock( 'trx_lock', __METHOD__, 0 );
$this->assertSame( TransactionManager::STATUS_TRX_OK, $this->conn->trxStatus() );
$this->conn->query( "SELECT invalid query()", __METHOD__ );
$this->fail( "No DBQueryError caught" );
} catch ( DBQueryError $e ) {
$this->assertInstanceOf( DBQueryError::class, $e );
}
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
try {
$this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->fail( "No DBTransactionStateError caught" );
} catch ( DBTransactionStateError $e ) {
$this->assertInstanceOf( DBTransactionStateError::class, $e );
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
$this->assertSame( 1, $this->conn->trxLevel(), "Transaction remains" );
$this->assertTrue( $this->conn->isOpen(), "Connection remains" );
}
$adminConn = $this->newConnection();
$adminConn->query( "KILL $encId", __METHOD__ );
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
try {
$this->conn->query( 'SELECT 1', __METHOD__ );
$this->fail( "No DBTransactionStateError caught" );
} catch ( DBTransactionStateError $e ) {
$this->assertInstanceOf( DBTransactionStateError::class, $e );
}
$this->assertSame(
1,
$this->conn->trxLevel(),
"Transaction loss not yet detected (due to STATUS_TRX_ERROR)"
);
$this->assertTrue(
$this->conn->isOpen(),
"Connection loss not detected (due to STATUS_TRX_ERROR)"
);
$this->conn->cancelAtomic( __METHOD__ );
$this->assertSame( 0, $this->conn->trxLevel(), "No transaction remains" );
$this->assertTrue( $this->conn->isOpen(), "Reconnected" );
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
$this->assertSame( 'x', $row->v, "Recovered" );
$this->conn->rollback( __METHOD__ );
$adminConn->close( __METHOD__ );
}
/**
* @return DatabaseMysqlBase
*/
private function newConnection() {
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
/** @var DatabaseMysqlBase $conn */
$conn = Database::factory(
'mysql',
array_merge(
$lb->getServerInfo( 0 ),
[
'dbname' => null,
'schema' => null,
'tablePrefix' => '',
]
)
);
return $conn;
}
public function tearDown(): void {
$this->conn->close( __METHOD__ );
parent::tearDown();
}
}