Merge "mediawiki.content.json: Remove file and author annotations"
[mediawiki.git] / tests / phpunit / includes / db / LBFactoryTest.php
blob13e8915c7070bbfb04c1fb8d9e996b069de50f03
1 <?php
2 /**
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
18 * @file
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;
41 /**
42 * @group Database
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;
59 return [
60 'serverName' => 'db1',
61 'host' => $wgDBserver,
62 'port' => $wgDBport,
63 'dbname' => $wgDBname,
64 'user' => $wgDBuser,
65 'password' => $wgDBpassword,
66 'type' => $wgDBtype,
67 'dbDirectory' => $wgSQLiteDataDir,
68 'load' => 0,
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() );
85 $factory->shutdown();
88 public function testLBFactorySimpleServers() {
89 $primaryConfig = $this->getPrimaryServerConfig();
90 $fakeReplica = [ 'serverName' => 'db2', 'load' => 100 ] + $primaryConfig;
92 $servers = [
93 $primaryConfig,
94 $fakeReplica
97 $factory = new LBFactorySimple( [
98 'servers' => $servers,
99 'loadMonitor' => [ 'class' => LoadMonitorNull::class ],
100 ] );
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
123 unset( $factory );
126 public function testLBFactoryMultiRoundCallbacks() {
127 $called = 0;
128 $countLBsFunc = static function ( LBFactoryMulti $factory ) {
129 $count = 0;
130 foreach ( $factory->getAllLBs() as $lb ) {
131 ++$count;
134 return $count;
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 ) {
145 ++$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 );
150 } );
151 $factory->commitPrimaryChanges( __METHOD__ );
152 $this->assertSame( 1, $called );
153 $this->assertEquals( 2, $countLBsFunc( $factory ) );
154 $factory->shutdown();
155 $factory->closeAll( __METHOD__ );
157 $called = 0;
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 ) {
169 ++$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 );
174 } );
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
184 $ran = 0;
185 $dbw->onTransactionPreCommitOrIdle( static function () use ( &$ran ) {
186 ++$ran;
187 } );
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( [
233 'sectionsByDB' => [
234 's1wiki' => 's1',
235 'DEFAULT' => 's3'
237 'sectionLoads' => [
238 's1' => [
239 'test-db3' => 0,
240 'test-db4' => 100,
242 's3' => [
243 'test-db1' => 0,
244 'test-db2' => 100,
247 'serverTemplate' => [
248 'port' => $wgDBport,
249 'dbname' => $wgDBname,
250 'user' => $wgDBuser,
251 'password' => $wgDBpassword,
252 'type' => $wgDBtype,
253 'dbDirectory' => $wgSQLiteDataDir,
254 'flags' => DBO_DEFAULT
256 'hostsByName' => [
257 'test-db1' => $wgDBserver,
258 'test-db2' => $wgDBserver,
259 'test-db3' => $wgDBserver,
260 'test-db4' => $wgDBserver
262 'loadMonitor' => [ 'class' => LoadMonitorNull::class ],
263 ] );
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 );
283 // Primary DB 1
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' );
302 // Primary DB 2
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',
328 ] );
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 );
341 $cpIndex = null;
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 );
364 $cp->setRequestInfo(
366 'IPAddress' => '127.0.0.1',
367 'UserAgent' => 'Totally-Not-Firefox',
368 'ChronologyClientId' => 'random_id',
369 'ChronologyPositionIndex' => $cpIndex
370 ] );
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 );
378 $cpIndex = null;
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' => [],
396 'sectionLoads' => [
397 'DEFAULT' => [
398 'test-db1' => 1,
401 'serverTemplate' => $serverOverride + [
402 'dbname' => $wgDBname,
403 'tablePrefix' => $wgDBprefix,
404 'user' => $wgDBuser,
405 'password' => $wgDBpassword,
406 'type' => $wgDBtype,
407 'dbDirectory' => $wgSQLiteDataDir,
408 'flags' => DBO_DEFAULT
410 'hostsByName' => [
411 'test-db1' => $wgDBserver,
413 'loadMonitor' => [ 'class' => LoadMonitorNull::class ],
414 'localDomain' => new DatabaseDomain( $wgDBname, null, $wgDBprefix ),
415 'agent' => 'MW-UNIT-TESTS'
416 ] );
419 public function testNiceDomains() {
420 global $wgDBname;
422 if ( $this->getDb()->databasesAreIndependent() ) {
423 self::markTestSkipped( "Skipping tests about selecting DBs: not applicable" );
424 return;
427 $factory = $this->newLBFactoryMulti(
431 $lb = $factory->getMainLB();
433 $db = $lb->getConnection( DB_PRIMARY );
434 $this->assertEquals(
435 WikiMap::getCurrentWikiId(),
436 $db->getDomainID()
438 unset( $db );
440 /** @var IMaintainableDatabase $db */
441 $db = $lb->getConnection( DB_PRIMARY, [], $lb::DOMAIN_ANY );
443 $this->assertSame(
445 $db->getDomainID(),
446 'Null domain ID handle used'
448 $this->assertNull(
449 $db->getDBname(),
450 'Null domain ID handle used'
452 $this->assertSame(
454 $db->tablePrefix(),
455 'Main domain ID handle used; prefix is empty though'
457 $this->assertEquals(
458 $this->quoteTable( $db, 'page' ),
459 $db->tableName( 'page' ),
460 "Correct full table name"
462 $this->assertEquals(
463 $this->quoteTable( $db, $wgDBname ) . '.' . $this->quoteTable( $db, 'page' ),
464 $db->tableName( "$wgDBname.page" ),
465 "Correct full table name"
467 $this->assertEquals(
468 $this->quoteTable( $db, 'nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
469 $db->tableName( 'nice_db.page' ),
470 "Correct full table name"
473 unset( $db );
475 $factory->setLocalDomainPrefix( 'my_' );
476 $db = $lb->getConnection( DB_PRIMARY ); // local domain connection
478 $this->assertEquals( $wgDBname, $db->getDBname() );
479 $this->assertEquals(
480 "$wgDBname-my_",
481 $db->getDomainID()
483 $this->assertEquals(
484 $this->quoteTable( $db, 'my_page' ),
485 $db->tableName( 'page' ),
486 "Correct full table name"
488 $this->assertEquals(
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__ );
495 $factory->destroy();
498 public function testTrickyDomain() {
499 global $wgDBname;
501 if ( $this->getDb()->databasesAreIndependent() ) {
502 self::markTestSkipped( "Skipping tests about selecting DBs: not applicable" );
503 return;
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" );
519 $this->assertEquals(
520 $this->quoteTable( $db, 'page' ),
521 $db->tableName( 'page' ),
522 "Correct full table name"
525 $this->assertEquals(
526 $this->quoteTable( $db, $dbname ) . '.' . $this->quoteTable( $db, 'page' ),
527 $db->tableName( "$dbname.page" ),
528 "Correct full table name"
531 $this->assertEquals(
532 $this->quoteTable( $db, 'nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
533 $db->tableName( 'nice_db.page' ),
534 "Correct full table name"
537 unset( $db );
539 $factory->setLocalDomainPrefix( 'my_' );
540 $db = $lb->getConnection( DB_PRIMARY, [], "$wgDBname-my_" );
542 $this->assertEquals(
543 $this->quoteTable( $db, 'my_page' ),
544 $db->tableName( 'page' ),
545 "Correct full table name"
547 $this->assertEquals(
548 $this->quoteTable( $db, 'other_nice_db' ) . '.' . $this->quoteTable( $db, 'page' ),
549 $db->tableName( 'other_nice_db.page' ),
550 "Correct full table name"
552 $this->assertEquals(
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__ );
559 $factory->destroy();
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() {
625 global $wgDBname;
627 if ( $this->getDb()->databasesAreIndependent() ) {
628 self::markTestSkipped( "Skipping tests about selecting DBs: not applicable" );
629 return;
632 $factory = $this->newLBFactoryMulti(
636 $lb = $factory->getMainLB();
638 $conn1 = $lb->getConnection( DB_PRIMARY );
639 $this->assertEquals(
640 WikiMap::getCurrentWikiId(),
641 $conn1->getDomainID()
643 unset( $conn1 );
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 );
657 $this->assertEquals(
658 $domain->getId(),
659 $conn2->getDomainID()
661 unset( $conn2 );
663 $factory->closeAll( __METHOD__ );
664 $factory->destroy();
667 public function testVirtualDomains() {
668 $baseOverrides = [
669 'localDomain' => ( new DatabaseDomain( 'localdomain', null, '' ) )->getId(),
670 'sectionLoads' => [
671 'DEFAULT' => [
672 'test-db1' => 1,
674 'shareddb' => [
675 'test-db1' => 1,
678 'externalLoads' => [
679 'extension1' => [
680 'test-db1' => 1,
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' );
692 $this->assertEquals(
693 'extdomain',
694 $db1->getDomainID()
696 $this->assertEquals(
697 'extdomain',
698 $factory->getAutoCommitPrimaryConnection( 'virtualdomain1' )->getDomainID()
700 $this->assertEquals(
701 'extension1',
702 $factory->getLoadBalancer( 'virtualdomain1' )->getClusterName()
705 $db2 = $factory->getPrimaryDatabase( 'virtualdomain2' );
706 $this->assertEquals(
707 'localdomain',
708 $db2->getDomainID()
710 $this->assertEquals(
711 'localdomain',
712 $factory->getAutoCommitPrimaryConnection( 'virtualdomain2' )->getDomainID()
714 $this->assertEquals(
715 'extension1',
716 $factory->getLoadBalancer( 'virtualdomain2' )->getClusterName()
719 $db3 = $factory->getPrimaryDatabase( 'virtualdomain3' );
720 $this->assertEquals(
721 'shareddb',
722 $db3->getDomainID()
724 $this->assertEquals(
725 'shareddb',
726 $factory->getAutoCommitPrimaryConnection( 'virtualdomain3' )->getDomainID()
728 $this->assertEquals(
729 'DEFAULT',
730 $factory->getLoadBalancer( 'virtualdomain3' )->getClusterName()
733 $db4 = $factory->getPrimaryDatabase( 'virtualdomain4' );
734 $this->assertEquals(
735 'localdomain',
736 $db4->getDomainID()
738 $this->assertEquals(
739 'localdomain',
740 $factory->getAutoCommitPrimaryConnection( 'virtualdomain4' )->getDomainID()
742 $this->assertEquals(
743 'DEFAULT',
744 $factory->getLoadBalancer( 'virtualdomain4' )->getClusterName()
748 private function quoteTable( IReadableDatabase $db, $table ) {
749 if ( $db->getType() === 'sqlite' ) {
750 return $table;
751 } else {
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 );
767 $cpWrap->store->set(
768 $cpWrap->key,
769 $cpWrap->mergePositions(
770 false,
772 [ ILBFactory::CLUSTER_MAIN_DEFAULT => $priorTime ]
774 3600
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' => [
788 $primaryConfig,
789 $fakeReplica
790 ] ];
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' => [
837 $primaryConfig,
838 $replica1Config,
839 $replica2Config,
840 $replica3Config
841 ] ];
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;
897 $conf = [
898 'servers' => [
899 $primaryConfig,
900 $fakeReplica
904 // The config callback should return $conf, reflecting changes
905 // made to the local variable.
906 $conf['configCallback'] = static function () use ( &$conf ) {
907 static $calls = 0;
908 $calls++;
909 if ( $calls == 1 ) {
910 return $conf;
911 } else {
912 unset( $conf['servers'][1] );
913 return $conf;
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 );
956 $runs = 0;
957 $callback = static function () use ( &$runs ) {
958 ++$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 );