7e58555550802045a47fecd077389d602d432825
[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 $lb = new LoadBalancer( [
55 'servers' => [ $this->makeServerConfig() ],
56 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
57 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
58 ] );
59
60 $ld = DatabaseDomain::newFromId( $lb->getLocalDomainID() );
61 $this->assertEquals( $wgDBname, $ld->getDatabase(), 'local domain DB set' );
62 $this->assertEquals( $this->dbPrefix(), $ld->getTablePrefix(), 'local domain prefix set' );
63
64 $dbw = $lb->getConnection( DB_MASTER );
65 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
66 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
67 $this->assertWriteAllowed( $dbw );
68
69 $dbr = $lb->getConnection( DB_REPLICA );
70 $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
71 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
72
73 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
74 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
75 $this->assertFalse(
76 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
77 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
78 $this->assertNotEquals(
79 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
80
81 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
82 $this->assertFalse(
83 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
84 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
85 $this->assertNotEquals(
86 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
87
88 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
89 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
90 }
91
92 $lb->closeAll();
93 }
94
95 public function testWithReplica() {
96 global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
97
98 $servers = [
99 [ // master
100 'host' => $wgDBserver,
101 'dbname' => $wgDBname,
102 'tablePrefix' => $this->dbPrefix(),
103 'user' => $wgDBuser,
104 'password' => $wgDBpassword,
105 'type' => $wgDBtype,
106 'dbDirectory' => $wgSQLiteDataDir,
107 'load' => 0,
108 'flags' => DBO_TRX // REPEATABLE-READ for consistency
109 ],
110 [ // emulated replica
111 'host' => $wgDBserver,
112 'dbname' => $wgDBname,
113 'tablePrefix' => $this->dbPrefix(),
114 'user' => $wgDBuser,
115 'password' => $wgDBpassword,
116 'type' => $wgDBtype,
117 'dbDirectory' => $wgSQLiteDataDir,
118 'load' => 100,
119 'flags' => DBO_TRX // REPEATABLE-READ for consistency
120 ]
121 ];
122
123 $lb = new LoadBalancer( [
124 'servers' => $servers,
125 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() ),
126 'queryLogger' => MediaWiki\Logger\LoggerFactory::getInstance( 'DBQuery' ),
127 'loadMonitorClass' => LoadMonitorNull::class
128 ] );
129
130 $dbw = $lb->getConnection( DB_MASTER );
131 $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
132 $this->assertEquals(
133 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
134 $dbw->getLBInfo( 'clusterMasterHost' ),
135 'cluster master set' );
136 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
137 $this->assertWriteAllowed( $dbw );
138
139 $dbr = $lb->getConnection( DB_REPLICA );
140 $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
141 $this->assertEquals(
142 ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
143 $dbr->getLBInfo( 'clusterMasterHost' ),
144 'cluster master set' );
145 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
146 $this->assertWriteForbidden( $dbr );
147
148 if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
149 $dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
150 $this->assertFalse(
151 $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
152 $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on master" );
153 $this->assertNotEquals(
154 $dbw, $dbwAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
155
156 $dbrAuto = $lb->getConnection( DB_REPLICA, [], false, $lb::CONN_TRX_AUTOCOMMIT );
157 $this->assertFalse(
158 $dbrAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTOCOMMIT" );
159 $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX still set on replica" );
160 $this->assertNotEquals(
161 $dbr, $dbrAuto, "CONN_TRX_AUTOCOMMIT uses separate connection" );
162
163 $dbwAuto2 = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTOCOMMIT );
164 $this->assertEquals( $dbwAuto2, $dbwAuto, "CONN_TRX_AUTOCOMMIT reuses connections" );
165 }
166
167 $lb->closeAll();
168 }
169
170 private function assertWriteForbidden( Database $db ) {
171 try {
172 $db->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ );
173 $this->fail( 'Write operation should have failed!' );
174 } catch ( DBError $ex ) {
175 // check that the exception message contains "Write operation"
176 $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
177
178 if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) {
179 // re-throw original error, to preserve stack trace
180 throw $ex;
181 }
182 }
183 }
184
185 private function assertWriteAllowed( Database $db ) {
186 $table = $db->tableName( 'some_table' );
187 try {
188 $db->dropTable( 'some_table' ); // clear for sanity
189
190 // Trigger DBO_TRX to create a transaction so the flush below will
191 // roll everything here back in sqlite. But don't actually do the
192 // code below inside an atomic section becaue MySQL and Oracle
193 // auto-commit transactions for DDL statements like CREATE TABLE.
194 $db->startAtomic( __METHOD__ );
195 $db->endAtomic( __METHOD__ );
196
197 // Use only basic SQL and trivial types for these queries for compatibility
198 $this->assertNotSame(
199 false,
200 $db->query( "CREATE TABLE $table (id INT, time INT)", __METHOD__ ),
201 "table created"
202 );
203 $this->assertNotSame(
204 false,
205 $db->query( "DELETE FROM $table WHERE id=57634126", __METHOD__ ),
206 "delete query"
207 );
208 } finally {
209 // Drop the table to clean up, ignoring any error.
210 $db->query( "DROP TABLE $table", __METHOD__, true );
211 // Rollback the DBO_TRX transaction for sqlite's benefit.
212 $db->rollback( __METHOD__, 'flush' );
213 }
214 }
215
216 public function testServerAttributes() {
217 $servers = [
218 [ // master
219 'dbname' => 'my_unittest_wiki',
220 'tablePrefix' => 'unittest_',
221 'type' => 'sqlite',
222 'dbDirectory' => "some_directory",
223 'load' => 0
224 ]
225 ];
226
227 $lb = new LoadBalancer( [
228 'servers' => $servers,
229 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
230 'loadMonitorClass' => LoadMonitorNull::class
231 ] );
232
233 $this->assertTrue( $lb->getServerAttributes( 0 )[Database::ATTR_DB_LEVEL_LOCKING] );
234
235 $servers = [
236 [ // master
237 'host' => 'db1001',
238 'user' => 'wikiuser',
239 'password' => 'none',
240 'dbname' => 'my_unittest_wiki',
241 'tablePrefix' => 'unittest_',
242 'type' => 'mysql',
243 'load' => 100
244 ],
245 [ // emulated replica
246 'host' => 'db1002',
247 'user' => 'wikiuser',
248 'password' => 'none',
249 'dbname' => 'my_unittest_wiki',
250 'tablePrefix' => 'unittest_',
251 'type' => 'mysql',
252 'load' => 100
253 ]
254 ];
255
256 $lb = new LoadBalancer( [
257 'servers' => $servers,
258 'localDomain' => new DatabaseDomain( 'my_unittest_wiki', null, 'unittest_' ),
259 'loadMonitorClass' => LoadMonitorNull::class
260 ] );
261
262 $this->assertFalse( $lb->getServerAttributes( 1 )[Database::ATTR_DB_LEVEL_LOCKING] );
263 }
264
265 /**
266 * @covers LoadBalancer::openConnection()
267 * @covers LoadBalancer::getAnyOpenConnection()
268 */
269 function testOpenConnection() {
270 global $wgDBname;
271
272 $lb = new LoadBalancer( [
273 'servers' => [ $this->makeServerConfig() ],
274 'localDomain' => new DatabaseDomain( $wgDBname, null, $this->dbPrefix() )
275 ] );
276
277 $i = $lb->getWriterIndex();
278 $this->assertEquals( null, $lb->getAnyOpenConnection( $i ) );
279 $conn1 = $lb->getConnection( $i );
280 $this->assertNotEquals( null, $conn1 );
281 $this->assertEquals( $conn1, $lb->getAnyOpenConnection( $i ) );
282 $conn2 = $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT );
283 $this->assertNotEquals( null, $conn2 );
284 if ( $lb->getServerAttributes( $i )[Database::ATTR_DB_LEVEL_LOCKING] ) {
285 $this->assertEquals( null,
286 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
287 $this->assertEquals( $conn1,
288 $lb->getConnection(
289 $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ), $lb::CONN_TRX_AUTOCOMMIT );
290 } else {
291 $this->assertEquals( $conn2,
292 $lb->getAnyOpenConnection( $i, $lb::CONN_TRX_AUTOCOMMIT ) );
293 $this->assertEquals( $conn2,
294 $lb->getConnection( $i, [], false, $lb::CONN_TRX_AUTOCOMMIT ) );
295 }
296
297 $lb->closeAll();
298 }
299 }