Merge "build: Update eslint-config-wikimedia to 0.7.2"
[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() {
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' => DBO_TRX // REPEATABLE-READ for consistency
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 'servers' => [ $this->makeServerConfig() ],
61 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
62 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
63 'chronologyCallback' => function () use ( &$called ) {
64 $called = true;
65 }
66 ] );
67
68 $ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() );
69 $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
70 $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
71 $this->assertSame( 'my_test_wiki', $lb->resolveDomainID( 'my_test_wiki' ) );
72 $this->assertSame( $ld->getId(), $lb->resolveDomainID( false ) );
73 $this->assertSame( $ld->getId(), $lb->resolveDomainID( $ld ) );
74
75 $this->assertFalse( $called );
76 $dbw = $lb->getConnection( DB_MASTER );
77 $this->assertTrue( $called );
78 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
79 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
80 $this->assertWriteAllowed( $dbw );
81
82 $dbr = $lb->getConnection( DB_REPLICA );
83 $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
84 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
85
86 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
87 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
88 $this->assertFalse(
89 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
90 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
91 $this->assertNotEquals(
92 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
93
94 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
95 $this->assertFalse(
96 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
97 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
98 $this->assertNotEquals(
99 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
100
101 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
102 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
103 }
104
105 $lb->closeAll();
106 }
107
108 public function testWithReplica() {
109 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
110
111 $servers = [
112 [ // master
113 'host' => $wgDBserver,
114 'dbname' => $wgDBname,
115 'tablePrefix' => $this->dbPrefix(),
116 'user' => $wgDBuser,
117 'password' => $wgDBpassword,
118 'type' => $wgDBtype,
119 'dbDirectory' => $wgSQLiteDataDir,
120 'load' => 0,
121 'flags' => DBO_TRX // REPEATABLE-READ for consistency
122 ],
123 [ // emulated replica
124 'host' => $wgDBserver,
125 'dbname' => $wgDBname,
126 'tablePrefix' => $this->dbPrefix(),
127 'user' => $wgDBuser,
128 'password' => $wgDBpassword,
129 'type' => $wgDBtype,
130 'dbDirectory' => $wgSQLiteDataDir,
131 'load' => 100,
132 'flags' => DBO_TRX // REPEATABLE-READ for consistency
133 ]
134 ];
135
136 $lb = new LoadBalancer( [
137 'servers' => $servers,
138 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
139 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
140 'loadMonitorClass' => LoadMonitorNull::class
141 ] );
142
143 $dbw = $lb->getConnection( DB_MASTER );
144 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
145 $this->assertEquals(
146 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
147 $dbw->getLBInfo( 'clusterMasterHost' ),
148 'cluster master set' );
149 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
150 $this->assertWriteAllowed( $dbw );
151
152 $dbr = $lb->getConnection( DB_REPLICA );
153 $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
154 $this->assertEquals(
155 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
156 $dbr->getLBInfo( 'clusterMasterHost' ),
157 'cluster master set' );
158 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
159 $this->assertWriteForbidden( $dbr );
160
161 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
162 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
163 $this->assertFalse(
164 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
165 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
166 $this->assertNotEquals(
167 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
168
169 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
170 $this->assertFalse(
171 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
172 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
173 $this->assertNotEquals(
174 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
175
176 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
177 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
178 }
179
180 $lb->closeAll();
181 }
182
183 private function assertWriteForbidden( Database $db ) {
184 try {
185 $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ );
186 $this->fail( 'Write operation should have failed!' );
187 } catch ( DBError $ex ) {
188 // check that the exception message contains "Write operation"
189 $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
190
191 if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) {
192 // re-throw original error, to preserve stack trace
193 throw $ex;
194 }
195 }
196 }
197
198 private function assertWriteAllowed( Database $db ) {
199 $table = $db->tableName( 'some_table' );
200 // Trigger a transaction so that rollback() will remove all the tables.
201 // Don't do this for MySQL/Oracle as they auto-commit transactions for DDL
202 // statements such as CREATE TABLE.
203 $useAtomicSection = in_array( $db->getType(), [ 'sqlite', 'postgres', 'mssql' ], true );
204 try {
205 $db->dropTable( 'some_table' ); // clear for sanity
206 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
207
208 if ( $useAtomicSection ) {
209 $db->startAtomic( __METHOD__ );
210 }
211 // Use only basic SQL and trivial types for these queries for compatibility
212 $this->assertNotSame(
213 false,
214 $db->query( "CREATE TABLE $table (id INT, time INT)", __METHOD__ ),
215 "table created"
216 );
217 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
218 $this->assertNotSame(
219 false,
220 $db->query( "DELETE FROM $table WHERE id=57634126", __METHOD__ ),
221 "delete query"
222 );
223 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
224 } finally {
225 if ( !$useAtomicSection ) {
226 // Drop the table to clean up, ignoring any error.
227 $db->dropTable( 'some_table' );
228 }
229 // Rollback the atomic section for sqlite's benefit.
230 $db->rollback( __METHOD__, 'flush' );
231 $this->assertNotEquals( $db::STATUS_TRX_ERROR, $db->trxStatus() );
232 }
233 }
234
235 public function testServerAttributes() {
236 $servers = [
237 [ // master
238 'dbname' => 'my_unittest_wiki',
239 'tablePrefix' => 'unittest_',
240 'type' => 'sqlite',
241 'dbDirectory' => "some_directory",
242 'load' => 0
243 ]
244 ];
245
246 $lb = new LoadBalancer( [
247 'servers' => $servers,
248 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
249 'loadMonitorClass' => LoadMonitorNull::class
250 ] );
251
252 $this->assertTrue( $lb->getServerAttributes( 0 )[Database::ATTR_DB_LEVEL_LOCKING] );
253
254 $servers = [
255 [ // master
256 'host' => 'db1001',
257 'user' => 'wikiuser',
258 'password' => 'none',
259 'dbname' => 'my_unittest_wiki',
260 'tablePrefix' => 'unittest_',
261 'type' => 'mysql',
262 'load' => 100
263 ],
264 [ // emulated replica
265 'host' => 'db1002',
266 'user' => 'wikiuser',
267 'password' => 'none',
268 'dbname' => 'my_unittest_wiki',
269 'tablePrefix' => 'unittest_',
270 'type' => 'mysql',
271 'load' => 100
272 ]
273 ];
274
275 $lb = new LoadBalancer( [
276 'servers' => $servers,
277 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
278 'loadMonitorClass' => LoadMonitorNull::class
279 ] );
280
281 $this->assertFalse( $lb->getServerAttributes( 1 )[Database::ATTR_DB_LEVEL_LOCKING] );
282 }
283
284 /**
285 * @covers LoadBalancer::openConnection()
286 * @covers LoadBalancer::getAnyOpenConnection()
287 */
288 function testOpenConnection() {
289 global $wgDBname;
290
291 $lb = new LoadBalancer( [
292 'servers' => [ $this->makeServerConfig() ],
293 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
294 ] );
295
296 $i = $lb->getWriterIndex();
297 $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
298 $conn1 = $lb->getConnection( $i );
299 $this->assertNotEquals( null, $conn1 );
300 $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
301 $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
302 $this->assertNotEquals( null, $conn2 );
303 if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) {
304 $this->assertEquals( null,
305 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
306 $this->assertEquals( $conn1,
307 $lb->getConnection(
308 $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ), $lb::CONN_TRX_AUTOCOMMIT );
309 } else {
310 $this->assertEquals( $conn2,
311 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
312 $this->assertEquals( $conn2,
313 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ) );
314
315 $conn2->startAtomic( __METHOD__ );
316 try {
317 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
318 $conn2->endAtomic( __METHOD__ );
319 $this->fail( "No exception thrown." );
320 } catch ( DBUnexpectedError $e ) {
321 $this->assertEquals(
322 'Wikimedia\Rdbms\LoadBalancer::openConnection: ' .
323 'CONN_TRX_AUTOCOMMIT handle has a transaction.',
324 $e->getMessage()
325 );
326 }
327 $conn2->endAtomic( __METHOD__ );
328 }
329
330 $lb->closeAll();
331 }
332
333 public function testTransactionCallbackChains() {
334 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
335
336 $servers = [
337 [
338 'host' => $wgDBserver,
339 'dbname' => $wgDBname,
340 'tablePrefix' => $this->dbPrefix(),
341 'user' => $wgDBuser,
342 'password' => $wgDBpassword,
343 'type' => $wgDBtype,
344 'dbDirectory' => $wgSQLiteDataDir,
345 'load' => 0,
346 'flags' => DBO_TRX // REPEATABLE-READ for consistency
347 ],
348 ];
349
350 $lb = new LoadBalancer( [
351 'servers' => $servers,
352 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
353 ] );
354
355 $conn1 = $lb->openConnection( $lb->getWriterIndex(), false );
356 $conn2 = $lb->openConnection( $lb->getWriterIndex(), '' );
357
358 $count = 0;
359 $lb->forEachOpenMasterConnection( function () use ( &$count ) {
360 ++$count;
361 } );
362 $this->assertEquals( 2, $count, 'Connection handle count' );
363
364 $tlCalls = 0;
365 $lb->setTransactionListener( 'test-listener', function () use ( &$tlCalls ) {
366 ++$tlCalls;
367 } );
368
369 $lb->beginMasterChanges( __METHOD__ );
370 $bc = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
371 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
372 $bc['a'] = 1;
373 $conn2->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
374 $bc['b'] = 1;
375 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
376 $bc['c'] = 1;
377 $conn1->onTransactionPreCommitOrIdle( function () use ( &$bc, $conn1, $conn2 ) {
378 $bc['d'] = 1;
379 } );
380 } );
381 } );
382 } );
383 $lb->finalizeMasterChanges();
384 $lb->approveMasterChanges( [] );
385 $lb->commitMasterChanges( __METHOD__ );
386 $lb->runMasterTransactionIdleCallbacks();
387 $lb->runMasterTransactionListenerCallbacks();
388
389 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $bc );
390 $this->assertEquals( 2, $tlCalls );
391
392 $tlCalls = 0;
393 $lb->beginMasterChanges( __METHOD__ );
394 $ac = array_fill_keys( [ 'a', 'b', 'c', 'd' ], 0 );
395 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
396 $ac['a'] = 1;
397 $conn2->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
398 $ac['b'] = 1;
399 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
400 $ac['c'] = 1;
401 $conn1->onTransactionCommitOrIdle( function () use ( &$ac, $conn1, $conn2 ) {
402 $ac['d'] = 1;
403 } );
404 } );
405 } );
406 } );
407 $lb->finalizeMasterChanges();
408 $lb->approveMasterChanges( [] );
409 $lb->commitMasterChanges( __METHOD__ );
410 $lb->runMasterTransactionIdleCallbacks();
411 $lb->runMasterTransactionListenerCallbacks();
412
413 $this->assertEquals( array_fill_keys( [ 'a', 'b', 'c', 'd' ], 1 ), $ac );
414 $this->assertEquals( 2, $tlCalls );
415
416 $conn1->close();
417 $conn2->close();
418 }
419 }