4 * Holds tests for LoadBalancer MediaWiki class.
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
24 use Wikimedia\Rdbms\DBError
;
25 use Wikimedia\Rdbms\DatabaseDomain
;
26 use Wikimedia\Rdbms\Database
;
27 use Wikimedia\Rdbms\LoadBalancer
;
28 use Wikimedia\Rdbms\LoadMonitorNull
;
32 * @covers \Wikimedia\Rdbms\LoadBalancer
34 class LoadBalancerTest
extends MediaWikiTestCase
{
35 private function makeServerConfig() {
36 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
39 'host' => $wgDBserver,
40 'dbname' => $wgDBname,
41 'tablePrefix' => $this->dbPrefix(),
43 'password' => $wgDBpassword,
45 'dbDirectory' => $wgSQLiteDataDir,
47 'flags' => DBO_TRX
// REPEATABLE-READ for consistency
51 public function testWithoutReplica() {
55 $lb = new LoadBalancer( [
56 'servers' => [ $this->makeServerConfig() ],
57 'queryLogger' => MediaWiki\Logger\LoggerFactory
::getInstance( 'DBQuery' ),
58 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
59 'chronologyCallback' => function () use ( &$called ) {
64 $ld = DatabaseDomain
::newFromId( $lb->getLocalDomainID() );
65 $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
66 $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
68 $this->assertFalse( $called );
69 $dbw = $lb->getConnection( DB_MASTER
);
70 $this->assertTrue( $called );
71 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
72 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX
), "DBO_TRX set on master" );
73 $this->assertWriteAllowed( $dbw );
75 $dbr = $lb->getConnection( DB_REPLICA
);
76 $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
77 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX
), "DBO_TRX set on replica" );
79 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING
] ) {
80 $dbwAuto = $lb->getConnection( DB_MASTER
, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
82 $dbwAuto->getFlag( $dbw::DBO_TRX
), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
83 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX
), "DBO_TRX still set on master" );
84 $this->assertNotEquals(
85 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
87 $dbrAuto = $lb->getConnection( DB_REPLICA
, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
89 $dbrAuto->getFlag( $dbw::DBO_TRX
), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
90 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX
), "DBO_TRX still set on replica" );
91 $this->assertNotEquals(
92 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
94 $dbwAuto2 = $lb->getConnection( DB_MASTER
, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
95 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
101 public function testWithReplica() {
102 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
106 'host' => $wgDBserver,
107 'dbname' => $wgDBname,
108 'tablePrefix' => $this->dbPrefix(),
110 'password' => $wgDBpassword,
112 'dbDirectory' => $wgSQLiteDataDir,
114 'flags' => DBO_TRX
// REPEATABLE-READ for consistency
116 [ // emulated replica
117 'host' => $wgDBserver,
118 'dbname' => $wgDBname,
119 'tablePrefix' => $this->dbPrefix(),
121 'password' => $wgDBpassword,
123 'dbDirectory' => $wgSQLiteDataDir,
125 'flags' => DBO_TRX
// REPEATABLE-READ for consistency
129 $lb = new LoadBalancer( [
130 'servers' => $servers,
131 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
132 'queryLogger' => MediaWiki\Logger\LoggerFactory
::getInstance( 'DBQuery' ),
133 'loadMonitorClass' => LoadMonitorNull
::class
136 $dbw = $lb->getConnection( DB_MASTER
);
137 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
139 ( $wgDBserver != '' ) ?
$wgDBserver : 'localhost',
140 $dbw->getLBInfo( 'clusterMasterHost' ),
141 'cluster master set' );
142 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX
), "DBO_TRX set on master" );
143 $this->assertWriteAllowed( $dbw );
145 $dbr = $lb->getConnection( DB_REPLICA
);
146 $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
148 ( $wgDBserver != '' ) ?
$wgDBserver : 'localhost',
149 $dbr->getLBInfo( 'clusterMasterHost' ),
150 'cluster master set' );
151 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX
), "DBO_TRX set on replica" );
152 $this->assertWriteForbidden( $dbr );
154 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING
] ) {
155 $dbwAuto = $lb->getConnection( DB_MASTER
, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
157 $dbwAuto->getFlag( $dbw::DBO_TRX
), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
158 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX
), "DBO_TRX still set on master" );
159 $this->assertNotEquals(
160 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
162 $dbrAuto = $lb->getConnection( DB_REPLICA
, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
164 $dbrAuto->getFlag( $dbw::DBO_TRX
), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
165 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX
), "DBO_TRX still set on replica" );
166 $this->assertNotEquals(
167 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
169 $dbwAuto2 = $lb->getConnection( DB_MASTER
, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
170 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
176 private function assertWriteForbidden( Database
$db ) {
178 $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__
);
179 $this->fail( 'Write operation should have failed!' );
180 } catch ( DBError
$ex ) {
181 // check that the exception message contains "Write operation"
182 $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
184 if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) {
185 // re-throw original error, to preserve stack trace
191 private function assertWriteAllowed( Database
$db ) {
192 $table = $db->tableName( 'some_table' );
194 $db->dropTable( 'some_table' ); // clear for sanity
196 // Trigger DBO_TRX to create a transaction so the flush below will
197 // roll everything here back in sqlite. But don't actually do the
198 // code below inside an atomic section becaue MySQL and Oracle
199 // auto-commit transactions for DDL statements like CREATE TABLE.
200 $db->startAtomic( __METHOD__
);
201 $db->endAtomic( __METHOD__
);
203 // Use only basic SQL and trivial types for these queries for compatibility
204 $this->assertNotSame(
206 $db->query( "CREATE TABLE $table (id INT, time INT)", __METHOD__
),
209 $this->assertNotSame(
211 $db->query( "DELETE FROM $table WHERE id=57634126", __METHOD__
),
215 // Drop the table to clean up, ignoring any error.
216 $db->query( "DROP TABLE $table", __METHOD__
, true );
217 // Rollback the DBO_TRX transaction for sqlite's benefit.
218 $db->rollback( __METHOD__
, 'flush' );
222 public function testServerAttributes() {
225 'dbname' => 'my_unittest_wiki',
226 'tablePrefix' => 'unittest_',
228 'dbDirectory' => "some_directory",
233 $lb = new LoadBalancer( [
234 'servers' => $servers,
235 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
236 'loadMonitorClass' => LoadMonitorNull
::class
239 $this->assertTrue( $lb->getServerAttributes( 0 )[Database
::ATTR_DB_LEVEL_LOCKING
] );
244 'user' => 'wikiuser',
245 'password' => 'none',
246 'dbname' => 'my_unittest_wiki',
247 'tablePrefix' => 'unittest_',
251 [ // emulated replica
253 'user' => 'wikiuser',
254 'password' => 'none',
255 'dbname' => 'my_unittest_wiki',
256 'tablePrefix' => 'unittest_',
262 $lb = new LoadBalancer( [
263 'servers' => $servers,
264 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
265 'loadMonitorClass' => LoadMonitorNull
::class
268 $this->assertFalse( $lb->getServerAttributes( 1 )[Database
::ATTR_DB_LEVEL_LOCKING
] );
272 * @covers LoadBalancer::openConnection()
273 * @covers LoadBalancer::getAnyOpenConnection()
275 function testOpenConnection() {
278 $lb = new LoadBalancer( [
279 'servers' => [ $this->makeServerConfig() ],
280 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
283 $i = $lb->getWriterIndex();
284 $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
285 $conn1 = $lb->getConnection( $i );
286 $this->assertNotEquals( null, $conn1 );
287 $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
288 $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT
);
289 $this->assertNotEquals( null, $conn2 );
290 if ( $lb->getServerAttributes( $i )[Database
::ATTR_DB_LEVEL_LOCKING
] ) {
291 $this->assertEquals( null,
292 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT
) );
293 $this->assertEquals( $conn1,
295 $i, [], false, $lb::CONN_TRX_AUTOCOMMIT
), $lb::CONN_TRX_AUTOCOMMIT
);
297 $this->assertEquals( $conn2,
298 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT
) );
299 $this->assertEquals( $conn2,
300 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT
) );
306 public function testTransactionCallbackChains() {
307 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
311 'host' => $wgDBserver,
312 'dbname' => $wgDBname,
313 'tablePrefix' => $this->dbPrefix(),
315 'password' => $wgDBpassword,
317 'dbDirectory' => $wgSQLiteDataDir,
319 'flags' => DBO_TRX
// REPEATABLE-READ for consistency
323 $lb = new LoadBalancer( [
324 'servers' => $servers,
325 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
328 $conn1 = $lb->openConnection( $lb->getWriterIndex(), false );
329 $conn2 = $lb->openConnection( $lb->getWriterIndex(), '' );
332 $lb->forEachOpenMasterConnection( function () use ( &$count ) {
335 $this->assertEquals( 2, $count, 'Connection handle count' );
338 $lb->setTransactionListener( 'test-listener', function () use ( &$tlCalls ) {
342 $lb->beginMasterChanges( __METHOD__
);
343 $bc = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
344 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
346 $conn2->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
348 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
350 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
356 $lb->finalizeMasterChanges();
357 $lb->approveMasterChanges( [] );
358 $lb->commitMasterChanges( __METHOD__
);
359 $lb->runMasterTransactionIdleCallbacks();
360 $lb->runMasterTransactionListenerCallbacks();
362 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $bc );
363 $this->assertEquals( 2, $tlCalls );
366 $lb->beginMasterChanges( __METHOD__
);
367 $ac = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
368 $conn1->onTransactionIdle( function () use ( &$ac, $conn1, $conn2 ) {
370 $conn2->onTransactionIdle( function () use ( &$ac, $conn1, $conn2 ) {
372 $conn1->onTransactionIdle( function () use ( &$ac, $conn1, $conn2 ) {
374 $conn1->onTransactionIdle( function () use ( &$ac, $conn1, $conn2 ) {
380 $lb->finalizeMasterChanges();
381 $lb->approveMasterChanges( [] );
382 $lb->commitMasterChanges( __METHOD__
);
383 $lb->runMasterTransactionIdleCallbacks();
384 $lb->runMasterTransactionListenerCallbacks();
386 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $ac );
387 $this->assertEquals( 2, $tlCalls );