wiki.techinc.nl/tests/phpunit/integration/includes/db/DatabaseMysqlTest.php

241 lines
7.6 KiB
PHP
Raw Normal View History

rdbms: make automatic connection recovery apply to more cases Rename canRecoverFromDisconnect() in order to better describe its function. Make it use the transaction ID and query walltime as arguments and return an ERR_* class constant instead of a bool. Avoid retries of slow queries that yield lost connection errors. Add methods and class constants to track session state errors caused by the loss of named locks or temp tables. Such errors can be resolved by a "session flush" method. Make assertQueryIsCurrentlyAllowed() better distinguish ROLLBACK queries from ROLLBACK TO SAVEPOINT queries. For some scenarios, only full tranasction ROLLBACK queries should be allowed. Add flushSession() method to Database and flushPrimarySessions() methods to LBFactory/LoadBalancer. Also: * Rename wasKnownStatementRollbackError() and make it take the error number as an argument, similar to wasConnectionError(). Add mysql error codes for query timeouts since they only cause statement rollbacks. * Rename wasConnectionError() and mark it as protected. This is an internal method with no outside callers. * Rename wasQueryTimeout(), remove some HHVM-specific code, and simplify the arguments. * Make executeQuery() use a for loop for the query retry logic to reduce code duplication. * Move the error state setting logic in executeQueryAttempt() up in order to reduce code duplication. * Move the beginIfImplied() call in executeQueryAttempt() up to the retry loop in executeQuery(). This narrows the executeQueryAttempt() concerns to sending a single query and updating tracking fields. * Make closeConnection() and doHandleSessionLossPreconnect() in DatabaseSqlite more consistent with the base class by releasing named locks. * Mark trxStatus() as @internal. Bug: T281451 Bug: T293859 Change-Id: I200f90e413b8a725828745f81925b54985c72180
2021-11-22 20:09:57 +00:00
<?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();
}
protected function tearDown(): void {
$this->conn->close( __METHOD__ );
parent::tearDown();
}
rdbms: make automatic connection recovery apply to more cases Rename canRecoverFromDisconnect() in order to better describe its function. Make it use the transaction ID and query walltime as arguments and return an ERR_* class constant instead of a bool. Avoid retries of slow queries that yield lost connection errors. Add methods and class constants to track session state errors caused by the loss of named locks or temp tables. Such errors can be resolved by a "session flush" method. Make assertQueryIsCurrentlyAllowed() better distinguish ROLLBACK queries from ROLLBACK TO SAVEPOINT queries. For some scenarios, only full tranasction ROLLBACK queries should be allowed. Add flushSession() method to Database and flushPrimarySessions() methods to LBFactory/LoadBalancer. Also: * Rename wasKnownStatementRollbackError() and make it take the error number as an argument, similar to wasConnectionError(). Add mysql error codes for query timeouts since they only cause statement rollbacks. * Rename wasConnectionError() and mark it as protected. This is an internal method with no outside callers. * Rename wasQueryTimeout(), remove some HHVM-specific code, and simplify the arguments. * Make executeQuery() use a for loop for the query retry logic to reduce code duplication. * Move the error state setting logic in executeQueryAttempt() up in order to reduce code duplication. * Move the beginIfImplied() call in executeQueryAttempt() up to the retry loop in executeQuery(). This narrows the executeQueryAttempt() concerns to sending a single query and updating tracking fields. * Make closeConnection() and doHandleSessionLossPreconnect() in DatabaseSqlite more consistent with the base class by releasing named locks. * Mark trxStatus() as @internal. Bug: T281451 Bug: T293859 Change-Id: I200f90e413b8a725828745f81925b54985c72180
2021-11-22 20:09:57 +00:00
/**
* @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_' . mt_rand(), __METHOD__, 0 );
rdbms: make automatic connection recovery apply to more cases Rename canRecoverFromDisconnect() in order to better describe its function. Make it use the transaction ID and query walltime as arguments and return an ERR_* class constant instead of a bool. Avoid retries of slow queries that yield lost connection errors. Add methods and class constants to track session state errors caused by the loss of named locks or temp tables. Such errors can be resolved by a "session flush" method. Make assertQueryIsCurrentlyAllowed() better distinguish ROLLBACK queries from ROLLBACK TO SAVEPOINT queries. For some scenarios, only full tranasction ROLLBACK queries should be allowed. Add flushSession() method to Database and flushPrimarySessions() methods to LBFactory/LoadBalancer. Also: * Rename wasKnownStatementRollbackError() and make it take the error number as an argument, similar to wasConnectionError(). Add mysql error codes for query timeouts since they only cause statement rollbacks. * Rename wasConnectionError() and mark it as protected. This is an internal method with no outside callers. * Rename wasQueryTimeout(), remove some HHVM-specific code, and simplify the arguments. * Make executeQuery() use a for loop for the query retry logic to reduce code duplication. * Move the error state setting logic in executeQueryAttempt() up in order to reduce code duplication. * Move the beginIfImplied() call in executeQueryAttempt() up to the retry loop in executeQuery(). This narrows the executeQueryAttempt() concerns to sending a single query and updating tracking fields. * Make closeConnection() and doHandleSessionLossPreconnect() in DatabaseSqlite more consistent with the base class by releasing named locks. * Mark trxStatus() as @internal. Bug: T281451 Bug: T293859 Change-Id: I200f90e413b8a725828745f81925b54985c72180
2021-11-22 20:09:57 +00:00
$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_' . mt_rand(), __METHOD__, 0 );
rdbms: make automatic connection recovery apply to more cases Rename canRecoverFromDisconnect() in order to better describe its function. Make it use the transaction ID and query walltime as arguments and return an ERR_* class constant instead of a bool. Avoid retries of slow queries that yield lost connection errors. Add methods and class constants to track session state errors caused by the loss of named locks or temp tables. Such errors can be resolved by a "session flush" method. Make assertQueryIsCurrentlyAllowed() better distinguish ROLLBACK queries from ROLLBACK TO SAVEPOINT queries. For some scenarios, only full tranasction ROLLBACK queries should be allowed. Add flushSession() method to Database and flushPrimarySessions() methods to LBFactory/LoadBalancer. Also: * Rename wasKnownStatementRollbackError() and make it take the error number as an argument, similar to wasConnectionError(). Add mysql error codes for query timeouts since they only cause statement rollbacks. * Rename wasConnectionError() and mark it as protected. This is an internal method with no outside callers. * Rename wasQueryTimeout(), remove some HHVM-specific code, and simplify the arguments. * Make executeQuery() use a for loop for the query retry logic to reduce code duplication. * Move the error state setting logic in executeQueryAttempt() up in order to reduce code duplication. * Move the beginIfImplied() call in executeQueryAttempt() up to the retry loop in executeQuery(). This narrows the executeQueryAttempt() concerns to sending a single query and updating tracking fields. * Make closeConnection() and doHandleSessionLossPreconnect() in DatabaseSqlite more consistent with the base class by releasing named locks. * Mark trxStatus() as @internal. Bug: T281451 Bug: T293859 Change-Id: I200f90e413b8a725828745f81925b54985c72180
2021-11-22 20:09:57 +00:00
$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;
}
}