Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
8.57% |
15 / 175 |
|
16.67% |
2 / 12 |
CRAP | |
0.00% |
0 / 1 |
MWLBFactory | |
8.57% |
15 / 175 |
|
16.67% |
2 / 12 |
1663.19 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
applyDefaultConfig | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
182 | |||
getDbTypesWithSchemas | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initServerInfo | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
132 | |||
assertValidServerConfigs | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
90 | |||
reportIfPrefixSet | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
reportMismatchedDBs | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
reportMismatchedPrefixes | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getLBFactoryClass | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
setDomainAliases | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
applyGlobalState | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
logDeprecation | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Generator of database load balancing objects. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Database |
22 | */ |
23 | |
24 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
25 | use MediaWiki\Config\Config; |
26 | use MediaWiki\Config\ServiceOptions; |
27 | use MediaWiki\Deferred\DeferredUpdates; |
28 | use MediaWiki\Logger\LoggerFactory; |
29 | use MediaWiki\MainConfigNames; |
30 | use Wikimedia\Rdbms\ChronologyProtector; |
31 | use Wikimedia\Rdbms\ConfiguredReadOnlyMode; |
32 | use Wikimedia\Rdbms\DatabaseDomain; |
33 | use Wikimedia\Rdbms\IDatabase; |
34 | use Wikimedia\Rdbms\ILBFactory; |
35 | use Wikimedia\RequestTimeout\CriticalSectionProvider; |
36 | |
37 | /** |
38 | * MediaWiki-specific class for generating database load balancers |
39 | * |
40 | * @internal For use by core ServiceWiring only. |
41 | * @ingroup Database |
42 | */ |
43 | class MWLBFactory { |
44 | |
45 | /** @var array Cache of already-logged deprecation messages */ |
46 | private static $loggedDeprecations = []; |
47 | |
48 | public const CORE_VIRTUAL_DOMAINS = [ 'virtual-botpasswords' ]; |
49 | |
50 | /** |
51 | * @internal For use by ServiceWiring |
52 | */ |
53 | public const APPLY_DEFAULT_CONFIG_OPTIONS = [ |
54 | MainConfigNames::DBcompress, |
55 | MainConfigNames::DBDefaultGroup, |
56 | MainConfigNames::DBmwschema, |
57 | MainConfigNames::DBname, |
58 | MainConfigNames::DBpassword, |
59 | MainConfigNames::DBport, |
60 | MainConfigNames::DBprefix, |
61 | MainConfigNames::DBserver, |
62 | MainConfigNames::DBservers, |
63 | MainConfigNames::DBssl, |
64 | MainConfigNames::DBStrictWarnings, |
65 | MainConfigNames::DBtype, |
66 | MainConfigNames::DBuser, |
67 | MainConfigNames::DebugDumpSql, |
68 | MainConfigNames::DebugLogFile, |
69 | MainConfigNames::DebugToolbar, |
70 | MainConfigNames::ExternalServers, |
71 | MainConfigNames::SQLiteDataDir, |
72 | MainConfigNames::SQLMode, |
73 | MainConfigNames::VirtualDomainsMapping, |
74 | ]; |
75 | /** |
76 | * @var ServiceOptions |
77 | */ |
78 | private $options; |
79 | /** |
80 | * @var ConfiguredReadOnlyMode |
81 | */ |
82 | private $readOnlyMode; |
83 | /** |
84 | * @var ChronologyProtector |
85 | */ |
86 | private $chronologyProtector; |
87 | /** |
88 | * @var BagOStuff |
89 | */ |
90 | private $srvCache; |
91 | /** |
92 | * @var WANObjectCache |
93 | */ |
94 | private $wanCache; |
95 | /** |
96 | * @var CriticalSectionProvider |
97 | */ |
98 | private $csProvider; |
99 | /** |
100 | * @var StatsdDataFactoryInterface |
101 | */ |
102 | private $statsdDataFactory; |
103 | /** @var string[] */ |
104 | private array $virtualDomains; |
105 | |
106 | /** |
107 | * @param ServiceOptions $options |
108 | * @param ConfiguredReadOnlyMode $readOnlyMode |
109 | * @param ChronologyProtector $chronologyProtector |
110 | * @param BagOStuff $srvCache |
111 | * @param WANObjectCache $wanCache |
112 | * @param CriticalSectionProvider $csProvider |
113 | * @param StatsdDataFactoryInterface $statsdDataFactory |
114 | * @param string[] $virtualDomains |
115 | */ |
116 | public function __construct( |
117 | ServiceOptions $options, |
118 | ConfiguredReadOnlyMode $readOnlyMode, |
119 | ChronologyProtector $chronologyProtector, |
120 | BagOStuff $srvCache, |
121 | WANObjectCache $wanCache, |
122 | CriticalSectionProvider $csProvider, |
123 | StatsdDataFactoryInterface $statsdDataFactory, |
124 | array $virtualDomains |
125 | ) { |
126 | $this->options = $options; |
127 | $this->readOnlyMode = $readOnlyMode; |
128 | $this->chronologyProtector = $chronologyProtector; |
129 | $this->srvCache = $srvCache; |
130 | $this->wanCache = $wanCache; |
131 | $this->csProvider = $csProvider; |
132 | $this->statsdDataFactory = $statsdDataFactory; |
133 | $this->virtualDomains = $virtualDomains; |
134 | } |
135 | |
136 | /** |
137 | * @param array $lbConf Config for LBFactory::__construct() |
138 | * @return array |
139 | * @internal For use with service wiring |
140 | */ |
141 | public function applyDefaultConfig( array $lbConf ) { |
142 | $this->options->assertRequiredOptions( self::APPLY_DEFAULT_CONFIG_OPTIONS ); |
143 | |
144 | $typesWithSchema = self::getDbTypesWithSchemas(); |
145 | if ( Profiler::instance() instanceof ProfilerStub ) { |
146 | $profilerCallback = null; |
147 | } else { |
148 | $profilerCallback = static function ( $section ) { |
149 | return Profiler::instance()->scopedProfileIn( $section ); |
150 | }; |
151 | } |
152 | |
153 | $lbConf += [ |
154 | 'localDomain' => new DatabaseDomain( |
155 | $this->options->get( MainConfigNames::DBname ), |
156 | $this->options->get( MainConfigNames::DBmwschema ), |
157 | $this->options->get( MainConfigNames::DBprefix ) |
158 | ), |
159 | 'profiler' => $profilerCallback, |
160 | 'trxProfiler' => Profiler::instance()->getTransactionProfiler(), |
161 | 'logger' => LoggerFactory::getInstance( 'rdbms' ), |
162 | 'errorLogger' => [ MWExceptionHandler::class, 'logException' ], |
163 | 'deprecationLogger' => [ static::class, 'logDeprecation' ], |
164 | 'statsdDataFactory' => $this->statsdDataFactory, |
165 | 'cliMode' => MW_ENTRY_POINT === 'cli', |
166 | 'readOnlyReason' => $this->readOnlyMode->getReason(), |
167 | 'defaultGroup' => $this->options->get( MainConfigNames::DBDefaultGroup ), |
168 | 'criticalSectionProvider' => $this->csProvider |
169 | ]; |
170 | |
171 | $serversCheck = []; |
172 | // When making changes here, remember to also specify MediaWiki-specific options |
173 | // for Database classes in the relevant Installer subclass. |
174 | // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams. |
175 | if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) { |
176 | if ( isset( $lbConf['servers'] ) ) { |
177 | // Server array is already explicitly configured |
178 | } elseif ( is_array( $this->options->get( MainConfigNames::DBservers ) ) ) { |
179 | $lbConf['servers'] = []; |
180 | foreach ( $this->options->get( MainConfigNames::DBservers ) as $i => $server ) { |
181 | $lbConf['servers'][$i] = self::initServerInfo( $server, $this->options ); |
182 | } |
183 | } else { |
184 | $server = self::initServerInfo( |
185 | [ |
186 | 'host' => $this->options->get( MainConfigNames::DBserver ), |
187 | 'user' => $this->options->get( MainConfigNames::DBuser ), |
188 | 'password' => $this->options->get( MainConfigNames::DBpassword ), |
189 | 'dbname' => $this->options->get( MainConfigNames::DBname ), |
190 | 'type' => $this->options->get( MainConfigNames::DBtype ), |
191 | 'load' => 1 |
192 | ], |
193 | $this->options |
194 | ); |
195 | |
196 | if ( $this->options->get( MainConfigNames::DBssl ) ) { |
197 | $server['ssl'] = true; |
198 | } |
199 | $server['flags'] |= $this->options->get( MainConfigNames::DBcompress ) ? DBO_COMPRESS : 0; |
200 | if ( $this->options->get( MainConfigNames::DBStrictWarnings ) ) { |
201 | $server['strictWarnings'] = true; |
202 | } |
203 | |
204 | $lbConf['servers'] = [ $server ]; |
205 | } |
206 | if ( !isset( $lbConf['externalClusters'] ) ) { |
207 | $lbConf['externalClusters'] = $this->options->get( MainConfigNames::ExternalServers ); |
208 | } |
209 | |
210 | $serversCheck = $lbConf['servers']; |
211 | } elseif ( $lbConf['class'] === Wikimedia\Rdbms\LBFactoryMulti::class ) { |
212 | if ( isset( $lbConf['serverTemplate'] ) ) { |
213 | if ( in_array( $lbConf['serverTemplate']['type'], $typesWithSchema, true ) ) { |
214 | $lbConf['serverTemplate']['schema'] = $this->options->get( MainConfigNames::DBmwschema ); |
215 | } |
216 | $lbConf['serverTemplate']['sqlMode'] = $this->options->get( MainConfigNames::SQLMode ); |
217 | $serversCheck = [ $lbConf['serverTemplate'] ]; |
218 | } |
219 | } |
220 | |
221 | self::assertValidServerConfigs( |
222 | $serversCheck, |
223 | $this->options->get( MainConfigNames::DBname ), |
224 | $this->options->get( MainConfigNames::DBprefix ) |
225 | ); |
226 | |
227 | $lbConf['chronologyProtector'] = $this->chronologyProtector; |
228 | $lbConf['srvCache'] = $this->srvCache; |
229 | $lbConf['wanCache'] = $this->wanCache; |
230 | $lbConf['virtualDomains'] = array_merge( $this->virtualDomains, self::CORE_VIRTUAL_DOMAINS ); |
231 | $lbConf['virtualDomainsMapping'] = $this->options->get( MainConfigNames::VirtualDomainsMapping ); |
232 | |
233 | return $lbConf; |
234 | } |
235 | |
236 | /** |
237 | * @return array |
238 | */ |
239 | private function getDbTypesWithSchemas() { |
240 | return [ 'postgres' ]; |
241 | } |
242 | |
243 | /** |
244 | * @param array $server |
245 | * @param ServiceOptions $options |
246 | * @return array |
247 | */ |
248 | private function initServerInfo( array $server, ServiceOptions $options ) { |
249 | if ( $server['type'] === 'sqlite' ) { |
250 | $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null; |
251 | // T93097: hint for how file-based databases (e.g. sqlite) should go about locking. |
252 | // See https://www.sqlite.org/lang_transaction.html |
253 | // See https://www.sqlite.org/lockingv3.html#shared_lock |
254 | $isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] ); |
255 | if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) { |
256 | // Hack to support some re-entrant invocations using sqlite |
257 | // See: T259685, T91820 |
258 | $request = \MediaWiki\Rest\EntryPoint::getMainRequest(); |
259 | if ( $request->hasHeader( 'Promise-Non-Write-API-Action' ) ) { |
260 | $isHttpRead = true; |
261 | } |
262 | } |
263 | $server += [ |
264 | 'dbDirectory' => $options->get( MainConfigNames::SQLiteDataDir ), |
265 | 'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE' |
266 | ]; |
267 | } elseif ( $server['type'] === 'postgres' ) { |
268 | $server += [ 'port' => $options->get( MainConfigNames::DBport ) ]; |
269 | } |
270 | |
271 | if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) { |
272 | $server += [ 'schema' => $options->get( MainConfigNames::DBmwschema ) ]; |
273 | } |
274 | |
275 | $flags = $server['flags'] ?? DBO_DEFAULT; |
276 | if ( $options->get( MainConfigNames::DebugDumpSql ) |
277 | || $options->get( MainConfigNames::DebugLogFile ) |
278 | || $options->get( MainConfigNames::DebugToolbar ) |
279 | ) { |
280 | $flags |= DBO_DEBUG; |
281 | } |
282 | $server['flags'] = $flags; |
283 | |
284 | $server += [ |
285 | 'tablePrefix' => $options->get( MainConfigNames::DBprefix ), |
286 | 'sqlMode' => $options->get( MainConfigNames::SQLMode ), |
287 | ]; |
288 | |
289 | return $server; |
290 | } |
291 | |
292 | /** |
293 | * @param array $servers |
294 | * @param string $ldDB Local domain database name |
295 | * @param string $ldTP Local domain prefix |
296 | */ |
297 | private function assertValidServerConfigs( array $servers, $ldDB, $ldTP ) { |
298 | foreach ( $servers as $server ) { |
299 | $type = $server['type'] ?? null; |
300 | $srvDB = $server['dbname'] ?? null; // server DB |
301 | $srvTP = $server['tablePrefix'] ?? ''; // server table prefix |
302 | |
303 | if ( $type === 'mysql' ) { |
304 | // A DB name is not needed to connect to mysql; 'dbname' is useless. |
305 | // This field only defines the DB to use for unspecified DB domains. |
306 | if ( $srvDB !== null && $srvDB !== $ldDB ) { |
307 | self::reportMismatchedDBs( $srvDB, $ldDB ); |
308 | } |
309 | } elseif ( $type === 'postgres' ) { |
310 | if ( $srvTP !== '' ) { |
311 | self::reportIfPrefixSet( $srvTP, $type ); |
312 | } |
313 | } |
314 | |
315 | if ( $srvTP !== '' && $srvTP !== $ldTP ) { |
316 | self::reportMismatchedPrefixes( $srvTP, $ldTP ); |
317 | } |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * @param string $prefix Table prefix |
323 | * @param string $dbType Database type |
324 | * @return never |
325 | */ |
326 | private function reportIfPrefixSet( $prefix, $dbType ) { |
327 | $e = new UnexpectedValueException( |
328 | "\$wgDBprefix is set to '$prefix' but the database type is '$dbType'. " . |
329 | "MediaWiki does not support using a table prefix with this RDBMS type." |
330 | ); |
331 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
332 | exit; |
333 | } |
334 | |
335 | /** |
336 | * @param string $srvDB Server config database |
337 | * @param string $ldDB Local DB domain database |
338 | * @return never |
339 | */ |
340 | private function reportMismatchedDBs( $srvDB, $ldDB ) { |
341 | $e = new UnexpectedValueException( |
342 | "\$wgDBservers has dbname='$srvDB' but \$wgDBname='$ldDB'. " . |
343 | "Set \$wgDBname to the database used by this wiki project. " . |
344 | "There is rarely a need to set 'dbname' in \$wgDBservers. " . |
345 | "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " . |
346 | "use of Database::getDomainId(), and other features are not reliable when " . |
347 | "\$wgDBservers does not match the local wiki database/prefix." |
348 | ); |
349 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
350 | exit; |
351 | } |
352 | |
353 | /** |
354 | * @param string $srvTP Server config table prefix |
355 | * @param string $ldTP Local DB domain database |
356 | * @return never |
357 | */ |
358 | private function reportMismatchedPrefixes( $srvTP, $ldTP ) { |
359 | $e = new UnexpectedValueException( |
360 | "\$wgDBservers has tablePrefix='$srvTP' but \$wgDBprefix='$ldTP'. " . |
361 | "Set \$wgDBprefix to the table prefix used by this wiki project. " . |
362 | "There is rarely a need to set 'tablePrefix' in \$wgDBservers. " . |
363 | "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " . |
364 | "use of Database::getDomainId(), and other features are not reliable when " . |
365 | "\$wgDBservers does not match the local wiki database/prefix." |
366 | ); |
367 | MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW ); |
368 | exit; |
369 | } |
370 | |
371 | /** |
372 | * Decide which LBFactory class to use. |
373 | * |
374 | * @internal For use by ServiceWiring |
375 | * @param array $config (e.g. $wgLBFactoryConf) |
376 | * @return string Class name |
377 | */ |
378 | public function getLBFactoryClass( array $config ) { |
379 | $compat = [ |
380 | // For LocalSettings.php compat after removing underscores (since 1.23). |
381 | 'LBFactory_Single' => Wikimedia\Rdbms\LBFactorySingle::class, |
382 | 'LBFactory_Simple' => Wikimedia\Rdbms\LBFactorySimple::class, |
383 | 'LBFactory_Multi' => Wikimedia\Rdbms\LBFactoryMulti::class, |
384 | // For LocalSettings.php compat after moving classes to namespaces (since 1.29). |
385 | 'LBFactorySingle' => Wikimedia\Rdbms\LBFactorySingle::class, |
386 | 'LBFactorySimple' => Wikimedia\Rdbms\LBFactorySimple::class, |
387 | 'LBFactoryMulti' => Wikimedia\Rdbms\LBFactoryMulti::class |
388 | ]; |
389 | |
390 | $class = $config['class']; |
391 | return $compat[$class] ?? $class; |
392 | } |
393 | |
394 | /** |
395 | * @param ILBFactory $lbFactory |
396 | */ |
397 | public function setDomainAliases( ILBFactory $lbFactory ) { |
398 | $domain = DatabaseDomain::newFromId( $lbFactory->getLocalDomainID() ); |
399 | // For compatibility with hyphenated $wgDBname values on older wikis, handle callers |
400 | // that assume corresponding database domain IDs and wiki IDs have identical values |
401 | $rawLocalDomain = strlen( $domain->getTablePrefix() ) |
402 | ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}" |
403 | : (string)$domain->getDatabase(); |
404 | |
405 | $lbFactory->setDomainAliases( [ $rawLocalDomain => $domain ] ); |
406 | } |
407 | |
408 | /** |
409 | * Apply global state from the current web request or other PHP process. |
410 | * |
411 | * This technically violates the principle constraint on ServiceWiring to be |
412 | * deterministic for a given site configuration. The exemption made here |
413 | * is solely to aid in debugging and influence non-nominal behaviour such |
414 | * as ChronologyProtector. That is, the state applied here must never change |
415 | * the logical destination or meaning of any database-related methods, it |
416 | * merely applies preferences and debugging information. |
417 | * |
418 | * The code here must be non-essential, with LBFactory behaving the same toward |
419 | * its consumers regardless of whether this is applied or not. |
420 | * |
421 | * For example, something may instantiate LBFactory for the current wiki without |
422 | * calling this, and its consumers must not be able to tell the difference. |
423 | * Likewise, in the future MediaWiki may instantiate service wiring and LBFactory |
424 | * for a foreign wiki in the same farm and apply the current global state to that, |
425 | * and that should be fine as well. |
426 | * |
427 | * @param ILBFactory $lbFactory |
428 | * @param Config $config |
429 | * @param IBufferingStatsdDataFactory $stats |
430 | */ |
431 | public function applyGlobalState( |
432 | ILBFactory $lbFactory, |
433 | Config $config, |
434 | IBufferingStatsdDataFactory $stats |
435 | ): void { |
436 | if ( MW_ENTRY_POINT === 'cli' ) { |
437 | $lbFactory->getMainLB()->setTransactionListener( |
438 | __METHOD__, |
439 | static function ( $trigger ) use ( $stats, $config ) { |
440 | // During maintenance scripts and PHPUnit integration tests, we let |
441 | // DeferredUpdates run immediately from addUpdate(), unless a transaction |
442 | // is active. Notify DeferredUpdates after any commit to try now. |
443 | // See DeferredUpdates::tryOpportunisticExecute for why. |
444 | if ( $trigger === IDatabase::TRIGGER_COMMIT ) { |
445 | DeferredUpdates::tryOpportunisticExecute(); |
446 | } |
447 | // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385) |
448 | MediaWiki::emitBufferedStatsdData( $stats, $config ); |
449 | } |
450 | ); |
451 | $lbFactory->setWaitForReplicationListener( |
452 | __METHOD__, |
453 | static function () use ( $stats, $config ) { |
454 | // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385) |
455 | MediaWiki::emitBufferedStatsdData( $stats, $config ); |
456 | } |
457 | ); |
458 | |
459 | } |
460 | } |
461 | |
462 | /** |
463 | * Log a database deprecation warning |
464 | * |
465 | * @param string $msg Deprecation message |
466 | */ |
467 | public static function logDeprecation( $msg ) { |
468 | if ( isset( self::$loggedDeprecations[$msg] ) ) { |
469 | return; |
470 | } |
471 | self::$loggedDeprecations[$msg] = true; |
472 | MWDebug::sendRawDeprecated( $msg, true, wfGetCaller() ); |
473 | } |
474 | } |