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