wiki.techinc.nl/tests/phpunit/unit/includes/libs/rdbms/database/DBConnRefTest.php
Aaron Schulz c1e1512698 rdbms: improve LoadBalancer connection pool reuse (ii)
Make DBConnRef enforce the DB domain selected during its lifetime
and allow more nested and successive use of the same connection handle
via DBConnRef. This can avoid extra connections in some cases where
getConnection()/getConnectionRef() is used.

Also:
* Reduce the number of connection pools arrays from six to two
* Merge getLocalConnection()/getForeignConnection() into one method
* Expand various related code comments

Since LoadBalancer::getReadOnlyReason() no longer user the local domain
but rather DOMAIN_ANY, it should not result in "USE" errors if the local
domain does not have a database on the server.

This version of the patch removes the unused reuseConnectionInternal()
method (the method was previously added back to the patch by mistake).

Bug: T226595
Change-Id: I62502f4de4f86a54f25be1699c4d1a1c1baee60b
2022-10-18 17:23:44 -07:00

232 lines
6.6 KiB
PHP

<?php
use PHPUnit\Framework\MockObject\MockObject;
use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IResultWrapper;
/**
* @covers Wikimedia\Rdbms\DBConnRef
*/
class DBConnRefTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
/**
* @return ILoadBalancer|MockObject
*/
private function getLoadBalancerMock() {
// getConnection() and getConnectionInternal() should keep returning the same connection
// on every call, unless that connection was closed. Then they should return a new
// connection.
$conn = $this->getDatabaseMock();
$getDatabaseMock = function () use ( &$conn ) {
if ( !$conn->isOpen() ) {
$conn = $this->getDatabaseMock();
}
return $conn;
};
$lb = $this->createMock( ILoadBalancer::class );
$lb->method( 'getConnection' )->willReturnCallback( $getDatabaseMock );
$lb->method( 'getConnectionInternal' )->willReturnCallback( $getDatabaseMock );
$lb->method( 'getConnectionRef' )->willReturnCallback(
function () use ( $lb ) {
return $this->getDBConnRef( $lb );
}
);
return $lb;
}
/**
* @return IDatabase
*/
private function getDatabaseMock() {
$db = $this->createMock( IDatabase::class );
$open = true;
$db->method( 'select' )->willReturnCallback( static function () use ( &$open ) {
if ( !$open ) {
throw new LogicException( "Not open" );
}
return new FakeResultWrapper( [] );
} );
$db->method( 'close' )->willReturnCallback( static function () use ( &$open ) {
$open = false;
return true;
} );
$db->method( 'isOpen' )->willReturnCallback( static function () use ( &$open ) {
return $open;
} );
return $db;
}
/**
* @param ILoadBalancer|null $lb
* @return IDatabase
*/
private function getDBConnRef( ILoadBalancer $lb = null ) {
$lb = $lb ?: $this->getLoadBalancerMock();
return new DBConnRef( $lb, [ DB_PRIMARY, [], 'mywiki', 0 ], DB_PRIMARY );
}
/**
* Test that bumping the modification counter causes the wrapped connection
* to be discarded and re-aquired.
*/
public function testModCount() {
$lb = $this->getLoadBalancerMock();
$lb->expects( $this->exactly( 3 ) )->method( 'getConnectionInternal' );
$params = [ DB_PRIMARY, [], 'mywiki', 0 ];
$modcount = 0;
$ref = new DBConnRef( $lb, $params, DB_PRIMARY, $modcount );
$ref->select( 'test', '*' );
$ref->select( 'test', '*' );
$modcount++; // cause second call to getConnectionInternal
$ref->select( 'test', '*' );
$ref->select( 'test', '*' );
$modcount++; // cause third call to getConnectionInternal
$ref->select( 'test', '*' );
$ref->select( 'test', '*' );
}
public function testConstruct() {
$lb = $this->createMock( ILoadBalancer::class );
$lb->expects( $this->once() )
->method( 'getConnectionInternal' )
->with( DB_PRIMARY, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT )
->willReturn( $this->getDatabaseMock() );
$ref = new DBConnRef(
$lb,
[ DB_PRIMARY, [ 'test' ], 'dummy', $lb::CONN_TRX_AUTOCOMMIT ],
DB_PRIMARY
);
$this->assertInstanceOf( IResultWrapper::class, $ref->select( 'whatever', '*' ) );
$this->assertEquals( DB_PRIMARY, $ref->getReferenceRole() );
$ref2 = new DBConnRef(
$lb,
[ DB_PRIMARY, [ 'test' ], 'dummy', $lb::CONN_TRX_AUTOCOMMIT ],
DB_REPLICA
);
$this->assertEquals( DB_REPLICA, $ref2->getReferenceRole() );
}
public function testDestruct() {
$lb = $this->getLoadBalancerMock();
$this->innerMethodForTestDestruct( $lb );
}
private function innerMethodForTestDestruct( ILoadBalancer $lb ) {
$ref = $lb->getConnectionRef( DB_REPLICA );
$this->assertInstanceOf( IResultWrapper::class, $ref->select( 'whatever', '*' ) );
}
public function testConstruct_failure() {
$this->expectException( InvalidArgumentException::class );
$lb = $this->getLoadBalancerMock();
new DBConnRef( $lb, 17, DB_REPLICA ); // bad constructor argument
}
/**
* @covers Wikimedia\Rdbms\DBConnRef::getDomainId
*/
public function testGetDomainID() {
$lb = $this->createMock( ILoadBalancer::class );
// getDomainID is optimized to not create a connection
$lb->expects( $this->never() )
->method( 'getConnection' );
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( 'dummy', $ref->getDomainID() );
}
/**
* @covers Wikimedia\Rdbms\DBConnRef::select
*/
public function testSelect() {
// select should get passed through normally
$ref = $this->getDBConnRef();
$this->assertInstanceOf( IResultWrapper::class, $ref->select( 'whatever', '*' ) );
}
public function testToString() {
$ref = $this->getDBConnRef();
$this->assertIsString( $ref->__toString() );
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_PRIMARY, [], 'test', 0 ], DB_PRIMARY );
$this->assertIsString( $ref->__toString() );
}
/**
* @covers Wikimedia\Rdbms\DBConnRef::close
*/
public function testClose() {
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_PRIMARY );
$this->expectException( \Wikimedia\Rdbms\DBUnexpectedError::class );
$ref->close();
}
/**
* @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
*/
public function testGetReferenceRole() {
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
$ref = new DBConnRef( $lb, [ DB_PRIMARY, [], 'dummy', 0 ], DB_PRIMARY );
$this->assertSame( DB_PRIMARY, $ref->getReferenceRole() );
$ref = new DBConnRef( $lb, [ 1, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
$ref = new DBConnRef( $lb, [ 0, [], 'dummy', 0 ], DB_PRIMARY );
$this->assertSame( DB_PRIMARY, $ref->getReferenceRole() );
}
/**
* @covers Wikimedia\Rdbms\DBConnRef::getReferenceRole
* @dataProvider provideRoleExceptions
*/
public function testRoleExceptions( $method, $args ) {
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->expectException( Wikimedia\Rdbms\DBReadOnlyRoleError::class );
$ref->$method( ...$args );
}
public function provideRoleExceptions() {
return [
[ 'insert', [ 'table', [ 'a' => 1 ] ] ],
[ 'update', [ 'table', [ 'a' => 1 ], [ 'a' => 2 ] ] ],
[ 'delete', [ 'table', [ 'a' => 1 ] ] ],
[ 'replace', [ 'table', [ 'a' ], [ 'a' => 1 ] ] ],
[ 'upsert', [ 'table', [ 'a' => 1 ], [ 'a' ], [ 'a = a + 1' ] ] ],
[ 'lock', [ 'k', 'method' ] ],
[ 'unlock', [ 'k', 'method' ] ],
[ 'getScopedLockAndFlush', [ 'k', 'method', 1 ] ]
];
}
}