Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / tests / phpunit / includes / db / LoadBalancerTest.php
1 <?php
2
3 /**
4 * Holds tests for LoadBalancer MediaWiki class.
5 *
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.
10 *
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.
15 *
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
20 *
21 * @file
22 */
23
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;
29 use Wikimedia\TestingAccessWrapper;
30
31 /**
32 * @group Database
33 * @group medium
34 * @covers \Wikimedia\Rdbms\LoadBalancer
35 */
36 class LoadBalancerTest extends MediaWikiTestCase {
37 private function makeServerConfig( $flags = DBO_DEFAULT ) {
38 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
39
40 return [
41 'host' => $wgDBserver,
42 'dbname' => $wgDBname,
43 'tablePrefix' => $this->dbPrefix(),
44 'user' => $wgDBuser,
45 'password' => $wgDBpassword,
46 'type' => $wgDBtype,
47 'dbDirectory' => $wgSQLiteDataDir,
48 'load' => 0,
49 'flags' => $flags
50 ];
51 }
52
53 /**
54 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
55 * @covers \Wikimedia\Rdbms\LoadBalancer::getLocalDomainID()
56 * @covers \Wikimedia\Rdbms\LoadBalancer::resolveDomainID()
57 * @covers \Wikimedia\Rdbms\LoadBalancer::haveIndex()
58 * @covers \Wikimedia\Rdbms\LoadBalancer::isNonZeroLoad()
59 */
60 public function testWithoutReplica() {
61 global $wgDBname;
62
63 $called = false;
64 $lb = new LoadBalancer( [
65 // Simulate web request with DBO_TRX
66 'servers' => [ $this->makeServerConfig( DBO_TRX ) ],
67 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
68 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
69 'chronologyCallback' => function () use ( &$called ) {
70 $called = true;
71 }
72 ] );
73
74 $this->assertEquals( 1, $lb->getServerCount() );
75 $this->assertFalse( $lb->hasReplicaServers() );
76 $this->assertFalse( $lb->hasStreamingReplicaServers() );
77
78 $this->assertTrue( $lb->haveIndex( 0 ) );
79 $this->assertFalse( $lb->haveIndex( 1 ) );
80 $this->assertFalse( $lb->isNonZeroLoad( 0 ) );
81 $this->assertFalse( $lb->isNonZeroLoad( 1 ) );
82
83 $ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() );
84 $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
85 $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
86 $this->assertSame( 'my_test_wiki', $lb->resolveDomainID( 'my_test_wiki' ) );
87 $this->assertSame( $ld->getId(), $lb->resolveDomainID( false ) );
88 $this->assertSame( $ld->getId(), $lb->resolveDomainID( $ld ) );
89 $this->assertFalse( $called );
90
91 $dbw = $lb->getConnection( DB_MASTER );
92 $this->assertTrue( $called );
93 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
94 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
95 $this->assertWriteAllowed( $dbw );
96
97 $dbr = $lb->getConnection( DB_REPLICA );
98 $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
99 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
100
101 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
102 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
103 $this->assertFalse(
104 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
105 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
106 $this->assertNotEquals(
107 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
108
109 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
110 $this->assertFalse(
111 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
112 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
113 $this->assertNotEquals(
114 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
115
116 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
117 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
118 }
119
120 $lb->closeAll();
121 }
122
123 /**
124 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
125 * @covers \Wikimedia\Rdbms\LoadBalancer::getReaderIndex()
126 * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
127 * @covers \Wikimedia\Rdbms\LoadBalancer::haveIndex()
128 * @covers \Wikimedia\Rdbms\LoadBalancer::isNonZeroLoad()
129 * @covers \Wikimedia\Rdbms\LoadBalancer::getServerName()
130 * @covers \Wikimedia\Rdbms\LoadBalancer::getServerInfo()
131 * @covers \Wikimedia\Rdbms\LoadBalancer::getServerType()
132 * @covers \Wikimedia\Rdbms\LoadBalancer::getServerAttributes()
133 */
134 public function testWithReplica() {
135 global $wgDBserver;
136
137 // Simulate web request with DBO_TRX
138 $lb = $this->newMultiServerLocalLoadBalancer( [], [ 'flags' => DBO_TRX ] );
139
140 $this->assertEquals( 8, $lb->getServerCount() );
141 $this->assertTrue( $lb->hasReplicaServers() );
142 $this->assertTrue( $lb->hasStreamingReplicaServers() );
143
144 $this->assertTrue( $lb->haveIndex( 0 ) );
145 $this->assertTrue( $lb->haveIndex( 1 ) );
146 $this->assertFalse( $lb->isNonZeroLoad( 0 ) );
147 $this->assertTrue( $lb->isNonZeroLoad( 1 ) );
148
149 for ( $i = 0; $i < $lb->getServerCount(); ++$i ) {
150 $this->assertType( 'string', $lb->getServerName( $i ) );
151 $this->assertType( 'array', $lb->getServerInfo( $i ) );
152 $this->assertType( 'string', $lb->getServerType( $i ) );
153 $this->assertType( 'array', $lb->getServerAttributes( $i ) );
154 }
155
156 $dbw = $lb->getConnection( DB_MASTER );
157 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
158 $this->assertEquals(
159 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
160 $dbw->getLBInfo( 'clusterMasterHost' ),
161 'cluster master set' );
162 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
163 $this->assertWriteAllowed( $dbw );
164
165 $dbr = $lb->getConnection( DB_REPLICA );
166 $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
167 $this->assertTrue( $dbr->isReadOnly(), 'replica shows as replica' );
168 $this->assertEquals(
169 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
170 $dbr->getLBInfo( 'clusterMasterHost' ),
171 'cluster master set' );
172 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
173 $this->assertWriteForbidden( $dbr );
174 $this->assertEquals( $dbr->getLBInfo( 'serverIndex' ), $lb->getReaderIndex() );
175
176 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
177 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
178 $this->assertFalse(
179 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
180 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
181 $this->assertNotEquals(
182 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
183
184 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
185 $this->assertFalse(
186 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
187 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
188 $this->assertNotEquals(
189 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
190
191 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
192 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
193 }
194
195 $lb->closeAll();
196 }
197
198 private function newSingleServerLocalLoadBalancer() {
199 global $wgDBname;
200
201 return new LoadBalancer( [
202 'servers' => [ $this->makeServerConfig() ],
203 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
204 ] );
205 }
206
207 private function newMultiServerLocalLoadBalancer(
208 $lbExtra = [], $srvExtra = [], $masterOnly = false
209 ) {
210 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
211
212 $servers = [
213 // Master DB
214 0 => $srvExtra + [
215 'host' => $wgDBserver,
216 'dbname' => $wgDBname,
217 'tablePrefix' => $this->dbPrefix(),
218 'user' => $wgDBuser,
219 'password' => $wgDBpassword,
220 'type' => $wgDBtype,
221 'dbDirectory' => $wgSQLiteDataDir,
222 'load' => $masterOnly ? 100 : 0,
223 ],
224 // Main replica DBs
225 1 => $srvExtra + [
226 'host' => $wgDBserver,
227 'dbname' => $wgDBname,
228 'tablePrefix' => $this->dbPrefix(),
229 'user' => $wgDBuser,
230 'password' => $wgDBpassword,
231 'type' => $wgDBtype,
232 'dbDirectory' => $wgSQLiteDataDir,
233 'load' => $masterOnly ? 0 : 100,
234 ],
235 2 => $srvExtra + [
236 'host' => $wgDBserver,
237 'dbname' => $wgDBname,
238 'tablePrefix' => $this->dbPrefix(),
239 'user' => $wgDBuser,
240 'password' => $wgDBpassword,
241 'type' => $wgDBtype,
242 'dbDirectory' => $wgSQLiteDataDir,
243 'load' => $masterOnly ? 0 : 100,
244 ],
245 // RC replica DBs
246 3 => $srvExtra + [
247 'host' => $wgDBserver,
248 'dbname' => $wgDBname,
249 'tablePrefix' => $this->dbPrefix(),
250 'user' => $wgDBuser,
251 'password' => $wgDBpassword,
252 'type' => $wgDBtype,
253 'dbDirectory' => $wgSQLiteDataDir,
254 'load' => 0,
255 'groupLoads' => [
256 'recentchanges' => 100,
257 'watchlist' => 100
258 ],
259 ],
260 // Logging replica DBs
261 4 => $srvExtra + [
262 'host' => $wgDBserver,
263 'dbname' => $wgDBname,
264 'tablePrefix' => $this->dbPrefix(),
265 'user' => $wgDBuser,
266 'password' => $wgDBpassword,
267 'type' => $wgDBtype,
268 'dbDirectory' => $wgSQLiteDataDir,
269 'load' => 0,
270 'groupLoads' => [
271 'logging' => 100
272 ],
273 ],
274 5 => $srvExtra + [
275 'host' => $wgDBserver,
276 'dbname' => $wgDBname,
277 'tablePrefix' => $this->dbPrefix(),
278 'user' => $wgDBuser,
279 'password' => $wgDBpassword,
280 'type' => $wgDBtype,
281 'dbDirectory' => $wgSQLiteDataDir,
282 'load' => 0,
283 'groupLoads' => [
284 'logging' => 100
285 ],
286 ],
287 // Maintenance query replica DBs
288 6 => $srvExtra + [
289 'host' => $wgDBserver,
290 'dbname' => $wgDBname,
291 'tablePrefix' => $this->dbPrefix(),
292 'user' => $wgDBuser,
293 'password' => $wgDBpassword,
294 'type' => $wgDBtype,
295 'dbDirectory' => $wgSQLiteDataDir,
296 'load' => 0,
297 'groupLoads' => [
298 'vslow' => 100
299 ],
300 ],
301 // Replica DB that only has a copy of some static tables
302 7 => $srvExtra + [
303 'host' => $wgDBserver,
304 'dbname' => $wgDBname,
305 'tablePrefix' => $this->dbPrefix(),
306 'user' => $wgDBuser,
307 'password' => $wgDBpassword,
308 'type' => $wgDBtype,
309 'dbDirectory' => $wgSQLiteDataDir,
310 'load' => 0,
311 'groupLoads' => [
312 'archive' => 100
313 ],
314 'is static' => true
315 ]
316 ];
317
318 return new LoadBalancer( $lbExtra + [
319 'servers' => $servers,
320 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
321 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
322 'loadMonitorClass' => LoadMonitorNull::class
323 ] );
324 }
325
326 private function assertWriteForbidden( Database $db ) {
327 try {
328 $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ );
329 $this->fail( 'Write operation should have failed!' );
330 } catch ( DBError $ex ) {
331 // check that the exception message contains "Write operation"
332 $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
333
334 if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) {
335 // re-throw original error, to preserve stack trace
336 throw $ex;
337 }
338 }
339 }
340
341 private function assertWriteAllowed( Database $db ) {
342 $table = $db->tableName( 'some_table' );
343 // Trigger a transaction so that rollback() will remove all the tables.
344 // Don't do this for MySQL/Oracle as they auto-commit transactions for DDL
345 // statements such as CREATE TABLE.
346 $useAtomicSection = in_array( $db->getType(), [ 'sqlite', 'postgres', 'mssql' ], true );
347 try {
348 $db->dropTable( 'some_table' ); // clear for sanity
349 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
350
351 if ( $useAtomicSection ) {
352 $db->startAtomic( __METHOD__ );
353 }
354 // Use only basic SQL and trivial types for these queries for compatibility
355 $this->assertNotSame(
356 false,
357 $db->query( "CREATE TABLE $table (id INT, time INT)", __METHOD__ ),
358 "table created"
359 );
360 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
361 $this->assertNotSame(
362 false,
363 $db->query( "DELETE FROM $table WHERE id=57634126", __METHOD__ ),
364 "delete query"
365 );
366 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
367 } finally {
368 if ( !$useAtomicSection ) {
369 // Drop the table to clean up, ignoring any error.
370 $db->dropTable( 'some_table' );
371 }
372 // Rollback the atomic section for sqlite's benefit.
373 $db->rollback( __METHOD__, 'flush' );
374 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
375 }
376 }
377
378 public function testServerAttributes() {
379 $servers = [
380 [ // master
381 'dbname' => 'my_unittest_wiki',
382 'tablePrefix' => 'unittest_',
383 'type' => 'sqlite',
384 'dbDirectory' => "some_directory",
385 'load' => 0
386 ]
387 ];
388
389 $lb = new LoadBalancer( [
390 'servers' => $servers,
391 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
392 'loadMonitorClass' => LoadMonitorNull::class
393 ] );
394
395 $this->assertTrue( $lb->getServerAttributes( 0 )[Database::ATTR_DB_LEVEL_LOCKING] );
396
397 $servers = [
398 [ // master
399 'host' => 'db1001',
400 'user' => 'wikiuser',
401 'password' => 'none',
402 'dbname' => 'my_unittest_wiki',
403 'tablePrefix' => 'unittest_',
404 'type' => 'mysql',
405 'load' => 100
406 ],
407 [ // emulated replica
408 'host' => 'db1002',
409 'user' => 'wikiuser',
410 'password' => 'none',
411 'dbname' => 'my_unittest_wiki',
412 'tablePrefix' => 'unittest_',
413 'type' => 'mysql',
414 'load' => 100
415 ]
416 ];
417
418 $lb = new LoadBalancer( [
419 'servers' => $servers,
420 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
421 'loadMonitorClass' => LoadMonitorNull::class
422 ] );
423
424 $this->assertFalse( $lb->getServerAttributes( 1 )[Database::ATTR_DB_LEVEL_LOCKING] );
425 }
426
427 /**
428 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
429 * @covers \Wikimedia\Rdbms\LoadBalancer::openConnection()
430 * @covers \Wikimedia\Rdbms\LoadBalancer::getAnyOpenConnection()
431 * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
432 */
433 function testOpenConnection() {
434 $lb = $this->newSingleServerLocalLoadBalancer();
435
436 $i = $lb->getWriterIndex();
437 $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
438
439 $conn1 = $lb->getConnection( $i );
440 $this->assertNotEquals( null, $conn1 );
441 $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
442 $this->assertFalse( $conn1->getFlag( DBO_TRX ) );
443
444 $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
445 $this->assertNotEquals( null, $conn2 );
446 $this->assertFalse( $conn2->getFlag( DBO_TRX ) );
447
448 if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) {
449 $this->assertEquals( null,
450 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
451 $this->assertEquals( $conn1,
452 $lb->getConnection(
453 $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ), $lb::CONN_TRX_AUTOCOMMIT );
454 } else {
455 $this->assertEquals( $conn2,
456 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
457 $this->assertEquals( $conn2,
458 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ) );
459
460 $conn2->startAtomic( __METHOD__ );
461 try {
462 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
463 $conn2->endAtomic( __METHOD__ );
464 $this->fail( "No exception thrown." );
465 } catch ( DBUnexpectedError $e ) {
466 $this->assertEquals(
467 'Handle requested with CONN_TRX_AUTOCOMMIT yet it has a transaction',
468 $e->getMessage()
469 );
470 }
471 $conn2->endAtomic( __METHOD__ );
472 }
473
474 $lb->closeAll();
475 }
476
477 /**
478 * @covers \Wikimedia\Rdbms\LoadBalancer::openConnection()
479 * @covers \Wikimedia\Rdbms\LoadBalancer::getWriterIndex()
480 * @covers \Wikimedia\Rdbms\LoadBalancer::forEachOpenMasterConnection()
481 * @covers \Wikimedia\Rdbms\LoadBalancer::setTransactionListener()
482 * @covers \Wikimedia\Rdbms\LoadBalancer::beginMasterChanges()
483 * @covers \Wikimedia\Rdbms\LoadBalancer::finalizeMasterChanges()
484 * @covers \Wikimedia\Rdbms\LoadBalancer::approveMasterChanges()
485 * @covers \Wikimedia\Rdbms\LoadBalancer::commitMasterChanges()
486 * @covers \Wikimedia\Rdbms\LoadBalancer::runMasterTransactionIdleCallbacks()
487 * @covers \Wikimedia\Rdbms\LoadBalancer::runMasterTransactionListenerCallbacks()
488 */
489 public function testTransactionCallbackChains() {
490 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
491
492 $servers = [
493 [
494 'host' => $wgDBserver,
495 'dbname' => $wgDBname,
496 'tablePrefix' => $this->dbPrefix(),
497 'user' => $wgDBuser,
498 'password' => $wgDBpassword,
499 'type' => $wgDBtype,
500 'dbDirectory' => $wgSQLiteDataDir,
501 'load' => 0,
502 'flags' => DBO_TRX // simulate a web request with DBO_TRX
503 ],
504 ];
505
506 $lb = new LoadBalancer( [
507 'servers' => $servers,
508 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
509 ] );
510
511 $conn1 = $lb->openConnection( $lb->getWriterIndex(), false );
512 $conn2 = $lb->openConnection( $lb->getWriterIndex(), '' );
513
514 $count = 0;
515 $lb->forEachOpenMasterConnection( function () use ( &$count ) {
516 ++$count;
517 } );
518 $this->assertEquals( 2, $count, 'Connection handle count' );
519
520 $tlCalls = 0;
521 $lb->setTransactionListener( 'test-listener', function () use ( &$tlCalls ) {
522 ++$tlCalls;
523 } );
524
525 $lb->beginMasterChanges( __METHOD__ );
526 $bc = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
527 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
528 $bc['a'] = 1;
529 $conn2->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
530 $bc['b'] = 1;
531 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
532 $bc['c'] = 1;
533 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
534 $bc['d'] = 1;
535 } );
536 } );
537 } );
538 } );
539 $lb->finalizeMasterChanges();
540 $lb->approveMasterChanges( [] );
541 $lb->commitMasterChanges( __METHOD__ );
542 $lb->runMasterTransactionIdleCallbacks();
543 $lb->runMasterTransactionListenerCallbacks();
544
545 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $bc );
546 $this->assertEquals( 2, $tlCalls );
547
548 $tlCalls = 0;
549 $lb->beginMasterChanges( __METHOD__ );
550 $ac = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
551 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
552 $ac['a'] = 1;
553 $conn2->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
554 $ac['b'] = 1;
555 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
556 $ac['c'] = 1;
557 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
558 $ac['d'] = 1;
559 } );
560 } );
561 } );
562 } );
563 $lb->finalizeMasterChanges();
564 $lb->approveMasterChanges( [] );
565 $lb->commitMasterChanges( __METHOD__ );
566 $lb->runMasterTransactionIdleCallbacks();
567 $lb->runMasterTransactionListenerCallbacks();
568
569 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $ac );
570 $this->assertEquals( 2, $tlCalls );
571
572 $conn1->close();
573 $conn2->close();
574 }
575
576 /**
577 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
578 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
579 */
580 public function testDBConnRefReadsMasterAndReplicaRoles() {
581 $lb = $this->newSingleServerLocalLoadBalancer();
582
583 $rConn = $lb->getConnectionRef( DB_REPLICA );
584 $wConn = $lb->getConnectionRef( DB_MASTER );
585 $wConn2 = $lb->getConnectionRef( 0 );
586
587 $v = [ 'value' => '1', '1' ];
588 $sql = 'SELECT MAX(1) AS value';
589 foreach ( [ $rConn, $wConn, $wConn2 ] as $conn ) {
590 $conn->clearFlag( $conn::DBO_TRX );
591
592 $res = $conn->query( $sql, __METHOD__ );
593 $this->assertEquals( $v, $conn->fetchRow( $res ) );
594
595 $res = $conn->query( $sql, __METHOD__, $conn::QUERY_REPLICA_ROLE );
596 $this->assertEquals( $v, $conn->fetchRow( $res ) );
597 }
598
599 $wConn->getScopedLockAndFlush( 'key', __METHOD__, 1 );
600 $wConn2->getScopedLockAndFlush( 'key2', __METHOD__, 1 );
601 }
602
603 /**
604 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
605 * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
606 */
607 public function testDBConnRefWritesReplicaRole() {
608 $lb = $this->newSingleServerLocalLoadBalancer();
609
610 $rConn = $lb->getConnectionRef( DB_REPLICA );
611
612 $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
613 }
614
615 /**
616 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
617 * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
618 */
619 public function testDBConnRefWritesReplicaRoleIndex() {
620 $lb = $this->newMultiServerLocalLoadBalancer();
621
622 $rConn = $lb->getConnectionRef( 1 );
623
624 $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
625 }
626
627 /**
628 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
629 * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
630 */
631 public function testDBConnRefWritesReplicaRoleInsert() {
632 $lb = $this->newMultiServerLocalLoadBalancer();
633
634 $rConn = $lb->getConnectionRef( DB_REPLICA );
635
636 $rConn->insert( 'test', [ 't' => 1 ], __METHOD__ );
637 }
638
639 /**
640 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
641 */
642 public function testGetConnectionRefDefaultGroup() {
643 $lb = $this->newMultiServerLocalLoadBalancer( [ 'defaultGroup' => 'vslow' ] );
644 $lbWrapper = TestingAccessWrapper::newFromObject( $lb );
645
646 $rVslow = $lb->getConnectionRef( DB_REPLICA );
647 $vslowIndexPicked = $rVslow->getLBInfo( 'serverIndex' );
648
649 $this->assertSame( $vslowIndexPicked, $lbWrapper->getExistingReaderIndex( 'vslow' ) );
650 }
651
652 /**
653 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
654 */
655 public function testGetConnectionRefUnknownDefaultGroup() {
656 $lb = $this->newMultiServerLocalLoadBalancer( [ 'defaultGroup' => 'invalid' ] );
657
658 $this->assertInstanceOf(
659 IDatabase::class,
660 $lb->getConnectionRef( DB_REPLICA )
661 );
662 }
663
664 /**
665 * @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
666 * @covers \Wikimedia\Rdbms\LoadBalancer::getMaintenanceConnectionRef()
667 */
668 public function testQueryGroupIndex() {
669 $lb = $this->newMultiServerLocalLoadBalancer( [ 'defaultGroup' => false ] );
670 /** @var LoadBalancer $lbWrapper */
671 $lbWrapper = TestingAccessWrapper::newFromObject( $lb );
672
673 $rGeneric = $lb->getConnectionRef( DB_REPLICA );
674 $mainIndexPicked = $rGeneric->getLBInfo( 'serverIndex' );
675
676 $this->assertEquals(
677 $mainIndexPicked,
678 $lbWrapper->getExistingReaderIndex( $lb::GROUP_GENERIC )
679 );
680 $this->assertTrue( in_array( $mainIndexPicked, [ 1, 2 ] ) );
681 for ( $i = 0; $i < 300; ++$i ) {
682 $rLog = $lb->getConnectionRef( DB_REPLICA, [] );
683 $this->assertEquals(
684 $mainIndexPicked,
685 $rLog->getLBInfo( 'serverIndex' ),
686 "Main index unchanged" );
687 }
688
689 $rRC = $lb->getConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
690 $rWL = $lb->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
691 $rRCMaint = $lb->getMaintenanceConnectionRef( DB_REPLICA, [ 'recentchanges' ] );
692 $rWLMaint = $lb->getMaintenanceConnectionRef( DB_REPLICA, [ 'watchlist' ] );
693
694 $this->assertEquals( 3, $rRC->getLBInfo( 'serverIndex' ) );
695 $this->assertEquals( 3, $rWL->getLBInfo( 'serverIndex' ) );
696 $this->assertEquals( 3, $rRCMaint->getLBInfo( 'serverIndex' ) );
697 $this->assertEquals( 3, $rWLMaint->getLBInfo( 'serverIndex' ) );
698
699 $rLog = $lb->getConnectionRef( DB_REPLICA, [ 'logging', 'watchlist' ] );
700 $logIndexPicked = $rLog->getLBInfo( 'serverIndex' );
701
702 $this->assertEquals( $logIndexPicked, $lbWrapper->getExistingReaderIndex( 'logging' ) );
703 $this->assertTrue( in_array( $logIndexPicked, [ 4, 5 ] ) );
704
705 for ( $i = 0; $i < 300; ++$i ) {
706 $rLog = $lb->getConnectionRef( DB_REPLICA, [ 'logging', 'watchlist' ] );
707 $this->assertEquals(
708 $logIndexPicked, $rLog->getLBInfo( 'serverIndex' ), "Index unchanged" );
709 }
710
711 $rVslow = $lb->getConnectionRef( DB_REPLICA, [ 'vslow', 'logging' ] );
712 $vslowIndexPicked = $rVslow->getLBInfo( 'serverIndex' );
713
714 $this->assertEquals( $vslowIndexPicked, $lbWrapper->getExistingReaderIndex( 'vslow' ) );
715 $this->assertEquals( 6, $vslowIndexPicked );
716 }
717
718 public function testNonZeroMasterLoad() {
719 $lb = $this->newMultiServerLocalLoadBalancer( [], [ 'flags' => DBO_DEFAULT ], true );
720 // Make sure that no infinite loop occurs (T226678)
721 $rGeneric = $lb->getConnectionRef( DB_REPLICA );
722 $this->assertEquals( $lb->getWriterIndex(), $rGeneric->getLBInfo( 'serverIndex' ) );
723 }
724
725 /**
726 * @covers \Wikimedia\Rdbms\LoadBalancer::getLazyConnectionRef
727 */
728 public function testGetLazyConnectionRef() {
729 $lb = $this->newMultiServerLocalLoadBalancer();
730
731 $rMaster = $lb->getLazyConnectionRef( DB_MASTER );
732 $rReplica = $lb->getLazyConnectionRef( 1 );
733 $this->assertFalse( $lb->getAnyOpenConnection( 0 ) );
734 $this->assertFalse( $lb->getAnyOpenConnection( 1 ) );
735
736 $rMaster->getType();
737 $rReplica->getType();
738 $rMaster->getDomainID();
739 $rReplica->getDomainID();
740 $this->assertFalse( $lb->getAnyOpenConnection( 0 ) );
741 $this->assertFalse( $lb->getAnyOpenConnection( 1 ) );
742
743 $rMaster->query( "SELECT 1", __METHOD__ );
744 $this->assertNotFalse( $lb->getAnyOpenConnection( 0 ) );
745
746 $rReplica->query( "SELECT 1", __METHOD__ );
747 $this->assertNotFalse( $lb->getAnyOpenConnection( 0 ) );
748 $this->assertNotFalse( $lb->getAnyOpenConnection( 1 ) );
749 }
750 }