Merge "foreign-resources.yaml: Add jquery.chosen"
[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
30 /**
31 * @group Database
32 * @covers \Wikimedia\Rdbms\LoadBalancer
33 */
34 class LoadBalancerTest extends MediaWikiTestCase {
35 private function makeServerConfig( $flags = DBO_DEFAULT ) {
36 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
37
38 return [
39 'host' => $wgDBserver,
40 'dbname' => $wgDBname,
41 'tablePrefix' => $this->dbPrefix(),
42 'user' => $wgDBuser,
43 'password' => $wgDBpassword,
44 'type' => $wgDBtype,
45 'dbDirectory' => $wgSQLiteDataDir,
46 'load' => 0,
47 'flags' => $flags
48 ];
49 }
50
51 /**
52 * @covers LoadBalancer::getLocalDomainID()
53 * @covers LoadBalancer::resolveDomainID()
54 */
55 public function testWithoutReplica() {
56 global $wgDBname;
57
58 $called = false;
59 $lb = new LoadBalancer( [
60 // Simulate web request with DBO_TRX
61 'servers' => [ $this->makeServerConfig( DBO_TRX ) ],
62 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
63 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
64 'chronologyCallback' => function () use ( &$called ) {
65 $called = true;
66 }
67 ] );
68
69 $ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() );
70 $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
71 $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
72 $this->assertSame( 'my_test_wiki', $lb->resolveDomainID( 'my_test_wiki' ) );
73 $this->assertSame( $ld->getId(), $lb->resolveDomainID( false ) );
74 $this->assertSame( $ld->getId(), $lb->resolveDomainID( $ld ) );
75 $this->assertFalse( $called );
76
77 $dbw = $lb->getConnection( DB_MASTER );
78 $this->assertTrue( $called );
79 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
80 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
81 $this->assertWriteAllowed( $dbw );
82
83 $dbr = $lb->getConnection( DB_REPLICA );
84 $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
85 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
86
87 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
88 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
89 $this->assertFalse(
90 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
91 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
92 $this->assertNotEquals(
93 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
94
95 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
96 $this->assertFalse(
97 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
98 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
99 $this->assertNotEquals(
100 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
101
102 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
103 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
104 }
105
106 $lb->closeAll();
107 }
108
109 public function testWithReplica() {
110 global $wgDBserver;
111
112 // Simulate web request with DBO_TRX
113 $lb = $this->newMultiServerLocalLoadBalancer( DBO_TRX );
114
115 $dbw = $lb->getConnection( DB_MASTER );
116 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
117 $this->assertEquals(
118 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
119 $dbw->getLBInfo( 'clusterMasterHost' ),
120 'cluster master set' );
121 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
122 $this->assertWriteAllowed( $dbw );
123
124 $dbr = $lb->getConnection( DB_REPLICA );
125 $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
126 $this->assertEquals(
127 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
128 $dbr->getLBInfo( 'clusterMasterHost' ),
129 'cluster master set' );
130 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
131 $this->assertWriteForbidden( $dbr );
132
133 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
134 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
135 $this->assertFalse(
136 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
137 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
138 $this->assertNotEquals(
139 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
140
141 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
142 $this->assertFalse(
143 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
144 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
145 $this->assertNotEquals(
146 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
147
148 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
149 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
150 }
151
152 $lb->closeAll();
153 }
154
155 private function newSingleServerLocalLoadBalancer() {
156 global $wgDBname;
157
158 return new LoadBalancer( [
159 'servers' => [ $this->makeServerConfig() ],
160 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
161 ] );
162 }
163
164 private function newMultiServerLocalLoadBalancer( $flags = DBO_DEFAULT ) {
165 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
166
167 $servers = [
168 [ // master
169 'host' => $wgDBserver,
170 'dbname' => $wgDBname,
171 'tablePrefix' => $this->dbPrefix(),
172 'user' => $wgDBuser,
173 'password' => $wgDBpassword,
174 'type' => $wgDBtype,
175 'dbDirectory' => $wgSQLiteDataDir,
176 'load' => 0,
177 'flags' => $flags
178 ],
179 [ // emulated replica
180 'host' => $wgDBserver,
181 'dbname' => $wgDBname,
182 'tablePrefix' => $this->dbPrefix(),
183 'user' => $wgDBuser,
184 'password' => $wgDBpassword,
185 'type' => $wgDBtype,
186 'dbDirectory' => $wgSQLiteDataDir,
187 'load' => 100,
188 'flags' => $flags
189 ]
190 ];
191
192 return new LoadBalancer( [
193 'servers' => $servers,
194 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
195 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
196 'loadMonitorClass' => LoadMonitorNull::class
197 ] );
198 }
199
200 private function assertWriteForbidden( Database $db ) {
201 try {
202 $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ );
203 $this->fail( 'Write operation should have failed!' );
204 } catch ( DBError $ex ) {
205 // check that the exception message contains "Write operation"
206 $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
207
208 if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) {
209 // re-throw original error, to preserve stack trace
210 throw $ex;
211 }
212 }
213 }
214
215 private function assertWriteAllowed( Database $db ) {
216 $table = $db->tableName( 'some_table' );
217 // Trigger a transaction so that rollback() will remove all the tables.
218 // Don't do this for MySQL/Oracle as they auto-commit transactions for DDL
219 // statements such as CREATE TABLE.
220 $useAtomicSection = in_array( $db->getType(), [ 'sqlite', 'postgres', 'mssql' ], true );
221 try {
222 $db->dropTable( 'some_table' ); // clear for sanity
223 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
224
225 if ( $useAtomicSection ) {
226 $db->startAtomic( __METHOD__ );
227 }
228 // Use only basic SQL and trivial types for these queries for compatibility
229 $this->assertNotSame(
230 false,
231 $db->query( "CREATE TABLE $table (id INT, time INT)", __METHOD__ ),
232 "table created"
233 );
234 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
235 $this->assertNotSame(
236 false,
237 $db->query( "DELETE FROM $table WHERE id=57634126", __METHOD__ ),
238 "delete query"
239 );
240 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
241 } finally {
242 if ( !$useAtomicSection ) {
243 // Drop the table to clean up, ignoring any error.
244 $db->dropTable( 'some_table' );
245 }
246 // Rollback the atomic section for sqlite's benefit.
247 $db->rollback( __METHOD__, 'flush' );
248 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
249 }
250 }
251
252 public function testServerAttributes() {
253 $servers = [
254 [ // master
255 'dbname' => 'my_unittest_wiki',
256 'tablePrefix' => 'unittest_',
257 'type' => 'sqlite',
258 'dbDirectory' => "some_directory",
259 'load' => 0
260 ]
261 ];
262
263 $lb = new LoadBalancer( [
264 'servers' => $servers,
265 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
266 'loadMonitorClass' => LoadMonitorNull::class
267 ] );
268
269 $this->assertTrue( $lb->getServerAttributes( 0 )[Database::ATTR_DB_LEVEL_LOCKING] );
270
271 $servers = [
272 [ // master
273 'host' => 'db1001',
274 'user' => 'wikiuser',
275 'password' => 'none',
276 'dbname' => 'my_unittest_wiki',
277 'tablePrefix' => 'unittest_',
278 'type' => 'mysql',
279 'load' => 100
280 ],
281 [ // emulated replica
282 'host' => 'db1002',
283 'user' => 'wikiuser',
284 'password' => 'none',
285 'dbname' => 'my_unittest_wiki',
286 'tablePrefix' => 'unittest_',
287 'type' => 'mysql',
288 'load' => 100
289 ]
290 ];
291
292 $lb = new LoadBalancer( [
293 'servers' => $servers,
294 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
295 'loadMonitorClass' => LoadMonitorNull::class
296 ] );
297
298 $this->assertFalse( $lb->getServerAttributes( 1 )[Database::ATTR_DB_LEVEL_LOCKING] );
299 }
300
301 /**
302 * @covers LoadBalancer::openConnection()
303 * @covers LoadBalancer::getAnyOpenConnection()
304 */
305 function testOpenConnection() {
306 $lb = $this->newSingleServerLocalLoadBalancer();
307
308 $i = $lb->getWriterIndex();
309 $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
310
311 $conn1 = $lb->getConnection( $i );
312 $this->assertNotEquals( null, $conn1 );
313 $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
314 $this->assertFalse( $conn1->getFlag( DBO_TRX ) );
315
316 $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
317 $this->assertNotEquals( null, $conn2 );
318 $this->assertFalse( $conn2->getFlag( DBO_TRX ) );
319
320 if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) {
321 $this->assertEquals( null,
322 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
323 $this->assertEquals( $conn1,
324 $lb->getConnection(
325 $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ), $lb::CONN_TRX_AUTOCOMMIT );
326 } else {
327 $this->assertEquals( $conn2,
328 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
329 $this->assertEquals( $conn2,
330 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ) );
331
332 $conn2->startAtomic( __METHOD__ );
333 try {
334 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
335 $conn2->endAtomic( __METHOD__ );
336 $this->fail( "No exception thrown." );
337 } catch ( DBUnexpectedError $e ) {
338 $this->assertEquals(
339 'Handle requested with CONN_TRX_AUTOCOMMIT yet it has a transaction',
340 $e->getMessage()
341 );
342 }
343 $conn2->endAtomic( __METHOD__ );
344 }
345
346 $lb->closeAll();
347 }
348
349 public function testTransactionCallbackChains() {
350 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
351
352 $servers = [
353 [
354 'host' => $wgDBserver,
355 'dbname' => $wgDBname,
356 'tablePrefix' => $this->dbPrefix(),
357 'user' => $wgDBuser,
358 'password' => $wgDBpassword,
359 'type' => $wgDBtype,
360 'dbDirectory' => $wgSQLiteDataDir,
361 'load' => 0,
362 'flags' => DBO_TRX // simulate a web request with DBO_TRX
363 ],
364 ];
365
366 $lb = new LoadBalancer( [
367 'servers' => $servers,
368 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
369 ] );
370
371 $conn1 = $lb->openConnection( $lb->getWriterIndex(), false );
372 $conn2 = $lb->openConnection( $lb->getWriterIndex(), '' );
373
374 $count = 0;
375 $lb->forEachOpenMasterConnection( function () use ( &$count ) {
376 ++$count;
377 } );
378 $this->assertEquals( 2, $count, 'Connection handle count' );
379
380 $tlCalls = 0;
381 $lb->setTransactionListener( 'test-listener', function () use ( &$tlCalls ) {
382 ++$tlCalls;
383 } );
384
385 $lb->beginMasterChanges( __METHOD__ );
386 $bc = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
387 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
388 $bc['a'] = 1;
389 $conn2->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
390 $bc['b'] = 1;
391 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
392 $bc['c'] = 1;
393 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
394 $bc['d'] = 1;
395 } );
396 } );
397 } );
398 } );
399 $lb->finalizeMasterChanges();
400 $lb->approveMasterChanges( [] );
401 $lb->commitMasterChanges( __METHOD__ );
402 $lb->runMasterTransactionIdleCallbacks();
403 $lb->runMasterTransactionListenerCallbacks();
404
405 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $bc );
406 $this->assertEquals( 2, $tlCalls );
407
408 $tlCalls = 0;
409 $lb->beginMasterChanges( __METHOD__ );
410 $ac = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
411 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
412 $ac['a'] = 1;
413 $conn2->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
414 $ac['b'] = 1;
415 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
416 $ac['c'] = 1;
417 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
418 $ac['d'] = 1;
419 } );
420 } );
421 } );
422 } );
423 $lb->finalizeMasterChanges();
424 $lb->approveMasterChanges( [] );
425 $lb->commitMasterChanges( __METHOD__ );
426 $lb->runMasterTransactionIdleCallbacks();
427 $lb->runMasterTransactionListenerCallbacks();
428
429 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $ac );
430 $this->assertEquals( 2, $tlCalls );
431
432 $conn1->close();
433 $conn2->close();
434 }
435
436 public function testDBConnRefReadsMasterAndReplicaRoles() {
437 $lb = $this->newSingleServerLocalLoadBalancer();
438
439 $rConn = $lb->getConnectionRef( DB_REPLICA );
440 $wConn = $lb->getConnectionRef( DB_MASTER );
441 $wConn2 = $lb->getConnectionRef( 0 );
442
443 $v = [ 'value' => '1', '1' ];
444 $sql = 'SELECT MAX(1) AS value';
445 foreach ( [ $rConn, $wConn, $wConn2 ] as $conn ) {
446 $conn->clearFlag( $conn::DBO_TRX );
447
448 $res = $conn->query( $sql, __METHOD__ );
449 $this->assertEquals( $v, $conn->fetchRow( $res ) );
450
451 $res = $conn->query( $sql, __METHOD__, $conn::QUERY_REPLICA_ROLE );
452 $this->assertEquals( $v, $conn->fetchRow( $res ) );
453 }
454
455 $wConn->getScopedLockAndFlush( 'key', __METHOD__, 1 );
456 $wConn2->getScopedLockAndFlush( 'key2', __METHOD__, 1 );
457 }
458
459 /**
460 * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
461 */
462 public function testDBConnRefWritesReplicaRole() {
463 $lb = $this->newSingleServerLocalLoadBalancer();
464
465 $rConn = $lb->getConnectionRef( DB_REPLICA );
466
467 $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
468 }
469
470 /**
471 * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
472 */
473 public function testDBConnRefWritesReplicaRoleIndex() {
474 $lb = $this->newMultiServerLocalLoadBalancer();
475
476 $rConn = $lb->getConnectionRef( 1 );
477
478 $rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
479 }
480
481 /**
482 * @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
483 */
484 public function testDBConnRefWritesReplicaRoleInsert() {
485 $lb = $this->newMultiServerLocalLoadBalancer();
486
487 $rConn = $lb->getConnectionRef( DB_REPLICA );
488
489 $rConn->insert( 'test', [ 't' => 1 ], __METHOD__ );
490 }
491 }