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