3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
19 * @author Antoine Musso
20 * @copyright © 2013 Antoine Musso
21 * @copyright © 2013 Wikimedia Foundation Inc.
24 use MediaWiki\WikiMap\WikiMap
;
25 use Wikimedia\ObjectCache\HashBagOStuff
;
26 use Wikimedia\Rdbms\ChronologyProtector
;
27 use Wikimedia\Rdbms\Database
;
28 use Wikimedia\Rdbms\DatabaseDomain
;
29 use Wikimedia\Rdbms\IDatabase
;
30 use Wikimedia\Rdbms\IDatabaseForOwner
;
31 use Wikimedia\Rdbms\ILBFactory
;
32 use Wikimedia\Rdbms\IMaintainableDatabase
;
33 use Wikimedia\Rdbms\IReadableDatabase
;
34 use Wikimedia\Rdbms\LBFactoryMulti
;
35 use Wikimedia\Rdbms\LBFactorySimple
;
36 use Wikimedia\Rdbms\LoadBalancer
;
37 use Wikimedia\Rdbms\LoadMonitorNull
;
38 use Wikimedia\Rdbms\MySQLPrimaryPos
;
39 use Wikimedia\TestingAccessWrapper
;
43 * @covers \Wikimedia\Rdbms\ChronologyProtector
44 * @covers \Wikimedia\Rdbms\DatabaseMySQL
45 * @covers \Wikimedia\Rdbms\DatabasePostgres
46 * @covers \Wikimedia\Rdbms\DatabaseSqlite
47 * @covers \Wikimedia\Rdbms\LBFactory
48 * @covers \Wikimedia\Rdbms\LBFactory
49 * @covers \Wikimedia\Rdbms\LBFactoryMulti
50 * @covers \Wikimedia\Rdbms\LBFactorySimple
51 * @covers \Wikimedia\Rdbms\LoadBalancer
53 class LBFactoryTest
extends MediaWikiIntegrationTestCase
{
55 private function getPrimaryServerConfig() {
56 global $wgDBserver, $wgDBport, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype;
57 global $wgSQLiteDataDir;
60 'serverName' => 'db1',
61 'host' => $wgDBserver,
63 'dbname' => $wgDBname,
65 'password' => $wgDBpassword,
67 'dbDirectory' => $wgSQLiteDataDir,
69 'flags' => DBO_TRX
// REPEATABLE-READ for consistency
73 public function testLBFactorySimpleServer() {
74 $servers = [ $this->getPrimaryServerConfig() ];
75 $factory = new LBFactorySimple( [ 'servers' => $servers ] );
76 $lb = $factory->getMainLB();
78 $dbw = $lb->getConnection( DB_PRIMARY
);
79 $this->assertNotFalse( $dbw );
80 $dbr = $lb->getConnection( DB_REPLICA
);
81 $this->assertNotFalse( $dbr );
83 $this->assertSame( 'DEFAULT', $lb->getClusterName() );
88 public function testLBFactorySimpleServers() {
89 $primaryConfig = $this->getPrimaryServerConfig();
90 $fakeReplica = [ 'serverName' => 'db2', 'load' => 100 ] +
$primaryConfig;
97 $factory = new LBFactorySimple( [
98 'servers' => $servers,
99 'loadMonitor' => [ 'class' => LoadMonitorNull
::class ],
101 $lb = $factory->getMainLB();
103 $dbw = $lb->getConnection( DB_PRIMARY
);
104 $this->assertNotFalse( $dbw );
105 $dbr = $lb->getConnection( DB_REPLICA
);
106 $this->assertNotFalse( $dbr );
108 $factory->shutdown();
111 public function testLBFactoryMultiConns() {
112 $factory = $this->newLBFactoryMultiLBs();
114 $this->assertSame( 's3', $factory->getMainLB()->getClusterName() );
116 $lb = $factory->getMainLB();
117 $dbw = $lb->getConnection( DB_PRIMARY
);
118 $this->assertNotFalse( $dbw );
119 $dbr = $lb->getConnection( DB_REPLICA
);
120 $this->assertNotFalse( $dbr );
122 // Destructor should trigger without round stage errors
126 public function testLBFactoryMultiRoundCallbacks() {
128 $countLBsFunc = static function ( LBFactoryMulti
$factory ) {
130 foreach ( $factory->getAllLBs() as $lb ) {
137 $factory = $this->newLBFactoryMultiLBs();
138 $this->assertSame( 0, $countLBsFunc( $factory ) );
139 $dbw = $factory->getMainLB()->getConnection( DB_PRIMARY
);
140 $this->assertSame( 1, $countLBsFunc( $factory ) );
141 // Test that LoadBalancer instances made during pre-commit callbacks in do not
142 // throw DBTransactionError due to transaction ROUND_* stages being mismatched.
143 $factory->beginPrimaryChanges( __METHOD__
);
144 $dbw->onTransactionPreCommitOrIdle( static function () use ( $factory, &$called ) {
146 // Trigger s1 LoadBalancer instantiation during "finalize" stage.
147 // There is no s1wiki DB to select so it is not in getConnection(),
148 // but this fools getMainLB() at least.
149 $factory->getMainLB( 's1wiki' )->getConnection( DB_PRIMARY
);
151 $factory->commitPrimaryChanges( __METHOD__
);
152 $this->assertSame( 1, $called );
153 $this->assertEquals( 2, $countLBsFunc( $factory ) );
154 $factory->shutdown();
155 $factory->closeAll( __METHOD__
);
158 $factory = $this->newLBFactoryMultiLBs();
159 $this->assertSame( 0, $countLBsFunc( $factory ) );
160 $dbw = $factory->getMainLB()->getConnection( DB_PRIMARY
);
161 $this->assertSame( 1, $countLBsFunc( $factory ) );
162 // Test that LoadBalancer instances made during pre-commit callbacks in do not
163 // throw DBTransactionError due to transaction ROUND_* stages being mismatched.hrow
164 // DBTransactionError due to transaction ROUND_* stages being mismatched.
165 $factory->beginPrimaryChanges( __METHOD__
);
166 // phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
167 $dbw->query( "SELECT 1 as t", __METHOD__
);
168 $dbw->onTransactionResolution( static function () use ( $factory, &$called ) {
170 // Trigger s1 LoadBalancer instantiation during "finalize" stage.
171 // There is no s1wiki DB to select so it is not in getConnection(),
172 // but this fools getMainLB() at least.
173 $factory->getMainLB( 's1wiki' )->getConnection( DB_PRIMARY
);
175 $factory->commitPrimaryChanges( __METHOD__
);
176 $this->assertSame( 1, $called );
177 $this->assertEquals( 2, $countLBsFunc( $factory ) );
178 $factory->shutdown();
179 $factory->closeAll( __METHOD__
);
181 $factory = $this->newLBFactoryMultiLBs();
182 $dbw = $factory->getMainLB()->getConnection( DB_PRIMARY
);
183 // DBTransactionError should not be thrown
185 $dbw->onTransactionPreCommitOrIdle( static function () use ( &$ran ) {
188 $factory->commitPrimaryChanges( __METHOD__
);
189 $this->assertSame( 1, $ran );
191 $factory->shutdown();
192 $factory->closeAll( __METHOD__
);
195 public function testLBFactoryMultiRoundTransactionSnapshots() {
196 $factory = $this->newLBFactoryMultiLBs();
197 $dbr = $factory->getMainLB()->getConnection( DB_REPLICA
);
198 $dbw = $factory->getMainLB()->getConnection( DB_PRIMARY
);
200 $dbr->begin( __METHOD__
, $dbr::TRANSACTION_INTERNAL
);
201 $this->assertSame( 1, $dbr->trxLevel() );
202 $this->assertSame( 0, $dbw->trxLevel() );
204 $factory->beginPrimaryChanges( __METHOD__
);
205 $this->assertSame( 0, $dbr->trxLevel() );
206 $this->assertSame( 0, $dbw->trxLevel() );
208 $dbr->begin( __METHOD__
, $dbr::TRANSACTION_INTERNAL
);
209 $dbw->begin( __METHOD__
, $dbw::TRANSACTION_INTERNAL
);
210 $this->assertSame( 1, $dbr->trxLevel() );
211 $this->assertSame( 1, $dbw->trxLevel() );
213 $factory->commitPrimaryChanges( __METHOD__
);
214 $this->assertSame( 0, $dbr->trxLevel() );
215 $this->assertSame( 0, $dbw->trxLevel() );
217 $factory->beginPrimaryChanges( __METHOD__
);
218 // phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
219 $dbr->query( 'SELECT 1', __METHOD__
);
220 $this->assertSame( 1, $dbr->trxLevel() );
221 $this->assertSame( 0, $dbw->trxLevel() );
223 $factory->commitPrimaryChanges( __METHOD__
);
224 $this->assertSame( 0, $dbr->trxLevel() );
225 $this->assertSame( 0, $dbw->trxLevel() );
228 private function newLBFactoryMultiLBs() {
229 global $wgDBserver, $wgDBport, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype;
230 global $wgSQLiteDataDir;
232 return new LBFactoryMulti( [
247 'serverTemplate' => [
249 'dbname' => $wgDBname,
251 'password' => $wgDBpassword,
253 'dbDirectory' => $wgSQLiteDataDir,
254 'flags' => DBO_DEFAULT
257 'test-db1' => $wgDBserver,
258 'test-db2' => $wgDBserver,
259 'test-db3' => $wgDBserver,
260 'test-db4' => $wgDBserver
262 'loadMonitor' => [ 'class' => LoadMonitorNull
::class ],
267 * @covers \Wikimedia\Rdbms\ChronologyProtector
269 public function testChronologyProtector() {
270 $now = microtime( true );
272 $hasChangesFunc = static function ( $mockDB ) {
273 $p = $mockDB->writesOrCallbacksPending();
274 $last = $mockDB->lastDoneWrites();
276 return is_float( $last ) ||
$p;
279 // (a) First HTTP request
280 $m1Pos = new MySQLPrimaryPos( 'db1034-bin.000976/843431247', $now );
281 $m2Pos = new MySQLPrimaryPos( 'db1064-bin.002400/794074907', $now );
284 /** @var IDatabaseForOwner|\PHPUnit\Framework\MockObject\MockObject $mockDB1 */
285 $mockDB1 = $this->createMock( IDatabaseForOwner
::class );
286 $mockDB1->method( 'writesOrCallbacksPending' )->willReturn( true );
287 $mockDB1->method( 'lastDoneWrites' )->willReturn( $now );
288 // Load balancer for primary DB 1
289 $lb1 = $this->createMock( LoadBalancer
::class );
290 $lb1->method( 'getConnection' )->willReturn( $mockDB1 );
291 $lb1->method( 'getServerCount' )->willReturn( 2 );
292 $lb1->method( 'hasReplicaServers' )->willReturn( true );
293 $lb1->method( 'hasStreamingReplicaServers' )->willReturn( true );
294 $lb1->method( 'getAnyOpenConnection' )->willReturn( $mockDB1 );
295 $lb1->method( 'hasOrMadeRecentPrimaryChanges' )->willReturnCallback(
296 static function () use ( $mockDB1, $hasChangesFunc ) {
297 return $hasChangesFunc( $mockDB1 );
300 $lb1->method( 'getPrimaryPos' )->willReturn( $m1Pos );
301 $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
303 /** @var IDatabaseForOwner|\PHPUnit\Framework\MockObject\MockObject $mockDB2 */
304 $mockDB2 = $this->createMock( IDatabaseForOwner
::class );
305 $mockDB2->method( 'writesOrCallbacksPending' )->willReturn( true );
306 $mockDB2->method( 'lastDoneWrites' )->willReturn( $now );
307 // Load balancer for primary DB 2
308 $lb2 = $this->createMock( LoadBalancer
::class );
309 $lb2->method( 'getConnection' )->willReturn( $mockDB2 );
310 $lb2->method( 'getServerCount' )->willReturn( 2 );
311 $lb2->method( 'hasReplicaServers' )->willReturn( true );
312 $lb2->method( 'hasStreamingReplicaServers' )->willReturn( true );
313 $lb2->method( 'getAnyOpenConnection' )->willReturn( $mockDB2 );
314 $lb2->method( 'hasOrMadeRecentPrimaryChanges' )->willReturnCallback(
315 static function () use ( $mockDB2, $hasChangesFunc ) {
316 return $hasChangesFunc( $mockDB2 );
319 $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
320 $lb2->method( 'getPrimaryPos' )->willReturn( $m2Pos );
322 $bag = new HashBagOStuff();
323 $cp = new ChronologyProtector( $bag, null, false );
324 $cp->setRequestInfo( [
325 'IPAddress' => '127.0.0.1',
326 'UserAgent' => 'Totally-Not-Firefox',
327 'ChronologyClientId' => 'random_id',
330 $mockDB1->expects( $this->once() )->method( 'writesOrCallbacksPending' );
331 $mockDB1->expects( $this->once() )->method( 'lastDoneWrites' );
332 $mockDB2->expects( $this->once() )->method( 'writesOrCallbacksPending' );
333 $mockDB2->expects( $this->once() )->method( 'lastDoneWrites' );
335 // Nothing to wait for on first HTTP request start
336 $sPos1 = $cp->getSessionPrimaryPos( $lb1 );
337 $sPos2 = $cp->getSessionPrimaryPos( $lb2 );
338 // Record positions in stash on first HTTP request end
339 $cp->stageSessionPrimaryPos( $lb1 );
340 $cp->stageSessionPrimaryPos( $lb2 );
342 $cp->persistSessionReplicationPositions( $cpIndex );
344 $this->assertNull( $sPos1 );
345 $this->assertNull( $sPos2 );
346 $this->assertSame( 1, $cpIndex, "CP write index set" );
348 // (b) Second HTTP request
350 // Load balancer for primary DB 1
351 $lb1 = $this->createMock( LoadBalancer
::class );
352 $lb1->method( 'getServerCount' )->willReturn( 2 );
353 $lb1->method( 'hasReplicaServers' )->willReturn( true );
354 $lb1->method( 'hasStreamingReplicaServers' )->willReturn( true );
355 $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
356 // Load balancer for primary DB 2
357 $lb2 = $this->createMock( LoadBalancer
::class );
358 $lb2->method( 'getServerCount' )->willReturn( 2 );
359 $lb2->method( 'hasReplicaServers' )->willReturn( true );
360 $lb2->method( 'hasStreamingReplicaServers' )->willReturn( true );
361 $lb2->method( 'getServerName' )->with( 0 )->willReturn( 'master2' );
363 $cp = new ChronologyProtector( $bag, null, false );
366 'IPAddress' => '127.0.0.1',
367 'UserAgent' => 'Totally-Not-Firefox',
368 'ChronologyClientId' => 'random_id',
369 'ChronologyPositionIndex' => $cpIndex
372 // Get last positions to be reached on second HTTP request start
373 $sPos1 = $cp->getSessionPrimaryPos( $lb1 );
374 $sPos2 = $cp->getSessionPrimaryPos( $lb2 );
375 // Shutdown (nothing to record)
376 $cp->stageSessionPrimaryPos( $lb1 );
377 $cp->stageSessionPrimaryPos( $lb2 );
379 $cp->persistSessionReplicationPositions( $cpIndex );
381 $this->assertNotNull( $sPos1 );
382 $this->assertNotNull( $sPos2 );
383 $this->assertSame( $m1Pos->__toString(), $sPos1->__toString() );
384 $this->assertSame( $m2Pos->__toString(), $sPos2->__toString() );
385 $this->assertNull( $cpIndex, "CP write index retained" );
387 $this->assertEquals( 'random_id', $cp->getClientId() );
390 private function newLBFactoryMulti( array $baseOverride = [], array $serverOverride = [] ) {
391 global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBprefix, $wgDBtype;
392 global $wgSQLiteDataDir;
394 return new LBFactoryMulti( $baseOverride +
[
395 'sectionsByDB' => [],
401 'serverTemplate' => $serverOverride +
[
402 'dbname' => $wgDBname,
403 'tablePrefix' => $wgDBprefix,
405 'password' => $wgDBpassword,
407 'dbDirectory' => $wgSQLiteDataDir,
408 'flags' => DBO_DEFAULT
411 'test-db1' => $wgDBserver,
413 'loadMonitor' => [ 'class' => LoadMonitorNull
::class ],
414 'localDomain' => new DatabaseDomain( $wgDBname, null, $wgDBprefix ),
415 'agent' => 'MW-UNIT-TESTS'
419 public function testNiceDomains() {
422 if ( $this->getDb()->databasesAreIndependent() ) {
423 self
::markTestSkipped( "Skipping tests about selecting DBs: not applicable" );
427 $factory = $this->newLBFactoryMulti(
431 $lb = $factory->getMainLB();
433 $db = $lb->getConnection( DB_PRIMARY
);
435 WikiMap
::getCurrentWikiId(),
440 /** @var IMaintainableDatabase $db */
441 $db = $lb->getConnection( DB_PRIMARY
, [], $lb::DOMAIN_ANY
);
446 'Null domain ID handle used'
450 'Null domain ID handle used'
455 'Main domain ID handle used; prefix is empty though'
458 $this->quoteTable( $db, 'page' ),
459 $db->tableName( 'page' ),
460 "Correct full table name"
463 $this->quoteTable( $db, $wgDBname ) . '.' . $this->quoteTable( $db, 'page' ),
464 $db->tableName( "$wgDBname.page" ),
465 "Correct full table name"
468 $this->quoteTable( $db, 'nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
469 $db->tableName( 'nice_db.page' ),
470 "Correct full table name"
475 $factory->setLocalDomainPrefix( 'my_' );
476 $db = $lb->getConnection( DB_PRIMARY
); // local domain connection
478 $this->assertEquals( $wgDBname, $db->getDBname() );
484 $this->quoteTable( $db, 'my_page' ),
485 $db->tableName( 'page' ),
486 "Correct full table name"
489 $this->quoteTable( $db, 'other_nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
490 $db->tableName( 'other_nice_db.page' ),
491 "Correct full table name"
494 $factory->closeAll( __METHOD__
);
498 public function testTrickyDomain() {
501 if ( $this->getDb()->databasesAreIndependent() ) {
502 self
::markTestSkipped( "Skipping tests about selecting DBs: not applicable" );
506 $dbname = 'unittest-domain'; // explodes if DB is selected
507 $factory = $this->newLBFactoryMulti(
508 [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ],
510 'dbname' => 'do_not_select_me' // explodes if DB is selected
513 $lb = $factory->getMainLB();
514 /** @var IMaintainableDatabase $db */
515 $db = $lb->getConnection( DB_PRIMARY
, [], $lb::DOMAIN_ANY
);
517 $this->assertSame( '', $db->getDomainID(), "Null domain used" );
520 $this->quoteTable( $db, 'page' ),
521 $db->tableName( 'page' ),
522 "Correct full table name"
526 $this->quoteTable( $db, $dbname ) . '.' . $this->quoteTable( $db, 'page' ),
527 $db->tableName( "$dbname.page" ),
528 "Correct full table name"
532 $this->quoteTable( $db, 'nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
533 $db->tableName( 'nice_db.page' ),
534 "Correct full table name"
539 $factory->setLocalDomainPrefix( 'my_' );
540 $db = $lb->getConnection( DB_PRIMARY
, [], "$wgDBname-my_" );
543 $this->quoteTable( $db, 'my_page' ),
544 $db->tableName( 'page' ),
545 "Correct full table name"
548 $this->quoteTable( $db, 'other_nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
549 $db->tableName( 'other_nice_db.page' ),
550 "Correct full table name"
553 $this->quoteTable( $db, 'garbage-db' ) . '.' . $this->quoteTable( $db, 'page' ),
554 $db->tableName( 'garbage-db.page' ),
555 "Correct full table name"
558 $factory->closeAll( __METHOD__
);
562 public function testInvalidSelectDB() {
563 if ( $this->getDb()->databasesAreIndependent() ) {
564 $this->markTestSkipped( "Not applicable per databasesAreIndependent()" );
567 $dbname = 'unittest-domain'; // explodes if DB is selected
568 $factory = $this->newLBFactoryMulti(
569 [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ],
571 'dbname' => 'do_not_select_me' // explodes if DB is selected
574 $lb = $factory->getMainLB();
575 /** @var IDatabase $db */
576 $db = $lb->getConnection( DB_PRIMARY
, [], $lb::DOMAIN_ANY
);
578 $this->expectException( \Wikimedia\Rdbms\DBUnexpectedError
::class );
579 $db->selectDomain( 'garbagedb' );
582 public function testInvalidSelectDBIndependent() {
583 $dbname = 'unittest-domain'; // explodes if DB is selected
584 $factory = $this->newLBFactoryMulti(
585 [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ],
587 // Explodes with SQLite and Postgres during open/USE
588 'dbname' => 'bad_dir/do_not_select_me'
591 $lb = $factory->getMainLB();
593 // FIXME: this should probably be lower (T235311)
594 $this->expectException( \Wikimedia\Rdbms\DBConnectionError
::class );
595 if ( !$factory->getMainLB()->getServerAttributes( 0 )[Database
::ATTR_DB_IS_FILE
] ) {
596 $this->markTestSkipped( "Not applicable per ATTR_DB_IS_FILE" );
599 /** @var IDatabase $db */
600 $this->assertNotNull( $lb->getConnectionInternal( DB_PRIMARY
, [], $lb::DOMAIN_ANY
) );
603 public function testInvalidSelectDBIndependent2() {
604 $dbname = 'unittest-domain'; // explodes if DB is selected
605 $factory = $this->newLBFactoryMulti(
606 [ 'localDomain' => ( new DatabaseDomain( $dbname, null, '' ) )->getId() ],
608 // Explodes with SQLite and Postgres during open/USE
609 'dbname' => 'bad_dir/do_not_select_me'
612 $lb = $factory->getMainLB();
614 // FIXME: this should probably be lower (T235311)
615 $this->expectException( \Wikimedia\Rdbms\DBExpectedError
::class );
616 if ( !$lb->getConnection( DB_PRIMARY
)->databasesAreIndependent() ) {
617 $this->markTestSkipped( "Not applicable per databasesAreIndependent()" );
620 $db = $lb->getConnectionInternal( DB_PRIMARY
);
621 $db->selectDomain( 'garbage-db' );
624 public function testRedefineLocalDomain() {
627 if ( $this->getDb()->databasesAreIndependent() ) {
628 self
::markTestSkipped( "Skipping tests about selecting DBs: not applicable" );
632 $factory = $this->newLBFactoryMulti(
636 $lb = $factory->getMainLB();
638 $conn1 = $lb->getConnection( DB_PRIMARY
);
640 WikiMap
::getCurrentWikiId(),
641 $conn1->getDomainID()
645 $factory->redefineLocalDomain( 'somedb-prefix_' );
646 $this->assertEquals( 'somedb-prefix_', $factory->getLocalDomainID() );
648 $domain = new DatabaseDomain( $wgDBname, null, 'pref_' );
649 $factory->redefineLocalDomain( $domain );
651 /** @var LoadBalancer $lbWrapper */
652 $lbWrapper = TestingAccessWrapper
::newFromObject( $lb );
653 $n = iterator_count( $lbWrapper->getOpenConnections() );
654 $this->assertSame( 0, $n, "Connections closed" );
656 $conn2 = $lb->getConnection( DB_PRIMARY
);
659 $conn2->getDomainID()
663 $factory->closeAll( __METHOD__
);
667 public function testVirtualDomains() {
669 'localDomain' => ( new DatabaseDomain( 'localdomain', null, '' ) )->getId(),
683 'virtualDomains' => [ 'virtualdomain1', 'virtualdomain2', 'virtualdomain3', 'virtualdomain4' ],
684 'virtualDomainsMapping' => [
685 'virtualdomain1' => [ 'db' => 'extdomain', 'cluster' => 'extension1' ],
686 'virtualdomain2' => [ 'db' => false, 'cluster' => 'extension1' ],
687 'virtualdomain3' => [ 'db' => 'shareddb' ],
690 $factory = $this->newLBFactoryMulti( $baseOverrides );
691 $db1 = $factory->getPrimaryDatabase( 'virtualdomain1' );
698 $factory->getAutoCommitPrimaryConnection( 'virtualdomain1' )->getDomainID()
702 $factory->getLoadBalancer( 'virtualdomain1' )->getClusterName()
705 $db2 = $factory->getPrimaryDatabase( 'virtualdomain2' );
712 $factory->getAutoCommitPrimaryConnection( 'virtualdomain2' )->getDomainID()
716 $factory->getLoadBalancer( 'virtualdomain2' )->getClusterName()
719 $db3 = $factory->getPrimaryDatabase( 'virtualdomain3' );
726 $factory->getAutoCommitPrimaryConnection( 'virtualdomain3' )->getDomainID()
730 $factory->getLoadBalancer( 'virtualdomain3' )->getClusterName()
733 $db4 = $factory->getPrimaryDatabase( 'virtualdomain4' );
740 $factory->getAutoCommitPrimaryConnection( 'virtualdomain4' )->getDomainID()
744 $factory->getLoadBalancer( 'virtualdomain4' )->getClusterName()
748 private function quoteTable( IReadableDatabase
$db, $table ) {
749 if ( $db->getType() === 'sqlite' ) {
752 return $db->addIdentifierQuotes( $table );
756 public function testGetChronologyProtectorTouched() {
757 $store = new HashBagOStuff
;
758 $chronologyProtector = new ChronologyProtector( $store, '', false );
759 $chronologyProtector->setRequestInfo( [ 'ChronologyClientId' => 'ii' ] );
761 // 2019-02-05T05:03:20Z
762 $mockWallClock = 1549343000.0;
763 $priorTime = $mockWallClock; // reference time
764 $chronologyProtector->setMockTime( $mockWallClock );
766 $cpWrap = TestingAccessWrapper
::newFromObject( $chronologyProtector );
769 $cpWrap->mergePositions(
772 [ ILBFactory
::CLUSTER_MAIN_DEFAULT
=> $priorTime ]
777 $lbFactory = $this->newLBFactoryMulti( [ 'chronologyProtector' => $chronologyProtector ] );
778 $mockWallClock +
= 1.0;
779 $touched = $chronologyProtector->getTouched( $lbFactory->getMainLB() );
780 $this->assertEquals( $priorTime, $touched );
783 public function testReconfigureWithOneReplica() {
784 $primaryConfig = $this->getPrimaryServerConfig();
785 $fakeReplica = [ 'load' => 100, 'serverName' => 'replica' ] +
$primaryConfig;
787 $conf = [ 'servers' => [
792 // Configure an LBFactory with one replica
793 $factory = new LBFactorySimple( $conf );
794 $lb = $factory->getMainLB();
795 $this->assertSame( 2, $lb->getServerCount() );
797 $con = $lb->getConnectionInternal( DB_REPLICA
);
798 $ref = $lb->getConnection( DB_REPLICA
);
800 // Call reconfigure with the same config, should have no effect
801 $factory->reconfigure( $conf );
802 $this->assertSame( 2, $lb->getServerCount() );
803 $this->assertTrue( $con->isOpen() );
804 $this->assertTrue( $ref->isOpen() );
806 // Call reconfigure with empty config, should have no effect
807 $factory->reconfigure( [] );
808 $this->assertSame( 2, $lb->getServerCount() );
809 $this->assertTrue( $con->isOpen() );
810 $this->assertTrue( $ref->isOpen() );
812 // Reconfigure the LBFactory to only have a single server.
813 $conf['servers'] = [ $this->getPrimaryServerConfig() ];
814 $factory->reconfigure( $conf );
816 // The LoadBalancer should have been reconfigured automatically.
817 $this->assertSame( 1, $lb->getServerCount() );
819 // Reconfiguring should not close connections immediately.
820 $this->assertTrue( $con->isOpen() );
822 // Connection refs should detect the config change, close the old connection,
823 // and get a new connection.
824 $this->assertTrue( $ref->isOpen() );
826 // The old connection should have been closed by DBConnRef.
827 $this->assertFalse( $con->isOpen() );
830 public function testReconfigureWithThreeReplicas() {
831 $primaryConfig = $this->getPrimaryServerConfig();
832 $replica1Config = [ 'serverName' => 'db2', 'load' => 0 ] +
$primaryConfig;
833 $replica2Config = [ 'serverName' => 'db3', 'load' => 1 ] +
$primaryConfig;
834 $replica3Config = [ 'serverName' => 'db4', 'load' => 1 ] +
$primaryConfig;
836 $conf = [ 'servers' => [
843 // Configure an LBFactory with two replicas
844 $factory = new LBFactorySimple( $conf );
845 $lb = $factory->getMainLB();
846 $this->assertSame( 4, $lb->getServerCount() );
847 $this->assertSame( 'db1', $lb->getServerName( 0 ) );
848 $this->assertSame( 'db2', $lb->getServerName( 1 ) );
849 $this->assertSame( 'db3', $lb->getServerName( 2 ) );
850 $this->assertSame( 'db4', $lb->getServerName( 3 ) );
852 $con = $lb->getConnectionInternal( DB_REPLICA
);
853 $ref = $lb->getConnection( DB_REPLICA
);
855 // Call reconfigure with the same config, should have no effect
856 $factory->reconfigure( $conf );
857 $this->assertSame( 4, $lb->getServerCount() );
858 $this->assertSame( 'db1', $lb->getServerName( 0 ) );
859 $this->assertSame( 'db2', $lb->getServerName( 1 ) );
860 $this->assertSame( 'db3', $lb->getServerName( 2 ) );
861 $this->assertSame( 'db4', $lb->getServerName( 3 ) );
862 $this->assertTrue( $con->isOpen() );
863 $this->assertTrue( $ref->isOpen() );
865 // Call reconfigure with empty config, should have no effect
866 $factory->reconfigure( [] );
867 $this->assertSame( 4, $lb->getServerCount() );
868 $this->assertSame( 'db1', $lb->getServerName( 0 ) );
869 $this->assertSame( 'db2', $lb->getServerName( 1 ) );
870 $this->assertSame( 'db3', $lb->getServerName( 2 ) );
871 $this->assertSame( 'db4', $lb->getServerName( 3 ) );
872 $this->assertTrue( $con->isOpen() );
873 $this->assertTrue( $ref->isOpen() );
875 // Reconfigure the LBFactory to only have a two servers (server indexes shifted).
876 $conf['servers'] = [ $primaryConfig, $replica2Config, $replica3Config ];
877 $factory->reconfigure( $conf );
878 // The LoadBalancer should have been reconfigured automatically.
879 $this->assertSame( 3, $lb->getServerCount() );
880 $this->assertSame( 'db1', $lb->getServerName( 0 ) );
881 $this->assertSame( false, $lb->getServerInfo( 1 ) );
882 $this->assertSame( 'db3', $lb->getServerName( 2 ) );
883 $this->assertSame( 'db4', $lb->getServerName( 3 ) );
884 // Reconfiguring should not close connections immediately.
885 $this->assertTrue( $con->isOpen() );
886 // Connection refs should detect the config change, close the old connection,
887 // and get a new connection.
888 $this->assertTrue( $ref->isOpen() );
889 // The old connection should have been closed by DBConnRef.
890 $this->assertFalse( $con->isOpen() );
893 public function testAutoReconfigure() {
894 $primaryConfig = $this->getPrimaryServerConfig();
895 $fakeReplica = [ 'load' => 100, 'serverName' => 'replica1' ] +
$primaryConfig;
904 // The config callback should return $conf, reflecting changes
905 // made to the local variable.
906 $conf['configCallback'] = static function () use ( &$conf ) {
912 unset( $conf['servers'][1] );
917 // Configure an LBFactory with one replica
918 $factory = new LBFactorySimple( $conf );
920 $lb = $factory->getMainLB();
921 $this->assertSame( 2, $lb->getServerCount() );
923 $con = $lb->getConnectionInternal( DB_REPLICA
);
924 $ref = $lb->getConnection( DB_REPLICA
);
926 // Nothing changed, autoReconfigure() should do nothing.
927 $factory->autoReconfigure();
929 $this->assertSame( 2, $lb->getServerCount() );
930 $this->assertTrue( $con->isOpen() );
931 $this->assertTrue( $ref->isOpen() );
933 // Now autoReconfigure() should detect the change and reconfigure all LoadBalancers.
934 $factory->autoReconfigure();
936 // The LoadBalancer should have been reconfigured now.
937 $this->assertSame( 1, $lb->getServerCount() );
939 // Reconfiguring should not close connections immediately.
940 $this->assertTrue( $con->isOpen() );
942 // Connection refs should detect the config change, close the old connection,
943 // and get a new connection.
944 $this->assertTrue( $ref->isOpen() );
946 // The old connection should have been called by DBConnRef.
947 $this->assertFalse( $con->isOpen() );
950 public function testSetWaitForReplicationListener() {
951 $factory = $this->newLBFactoryMultiLBs();
953 $allLBs = iterator_to_array( $factory->getAllLBs() );
954 $this->assertCount( 0, $allLBs );
957 $callback = static function () use ( &$runs ) {
960 $factory->setWaitForReplicationListener( 'test', $callback );
962 $this->assertSame( 0, $runs );
963 $factory->waitForReplication();
964 $this->assertSame( 1, $runs );
966 $factory->getMainLB();
967 $allLBs = iterator_to_array( $factory->getAllLBs() );
968 $this->assertCount( 1, $allLBs );
969 $factory->waitForReplication();
970 $this->assertSame( 2, $runs );