Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / db / MWLBFactory.php
blob322265c3587025d67bb9c0635a99503e41db3e18
1 <?php
2 /**
3 * Generator of database load balancing objects.
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.
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.
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
20 * @file
21 * @ingroup Database
24 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
25 use MediaWiki\Config\ServiceOptions;
26 use MediaWiki\Debug\MWDebug;
27 use MediaWiki\Logger\LoggerFactory;
28 use MediaWiki\MainConfigNames;
29 use Wikimedia\ObjectCache\BagOStuff;
30 use Wikimedia\ObjectCache\WANObjectCache;
31 use Wikimedia\Rdbms\ChronologyProtector;
32 use Wikimedia\Rdbms\ConfiguredReadOnlyMode;
33 use Wikimedia\Rdbms\DatabaseDomain;
34 use Wikimedia\Rdbms\ILBFactory;
35 use Wikimedia\RequestTimeout\CriticalSectionProvider;
36 use Wikimedia\Telemetry\TracerInterface;
38 /**
39 * MediaWiki-specific class for generating database load balancers
41 * @internal For use by core ServiceWiring only.
42 * @ingroup Database
44 class MWLBFactory {
46 /** Cache of already-logged deprecation messages */
47 private static array $loggedDeprecations = [];
49 public const CORE_VIRTUAL_DOMAINS = [
50 'virtual-botpasswords',
51 'virtual-interwiki',
52 'virtual-interwiki-interlanguage',
55 /**
56 * @internal For use by ServiceWiring
58 public const APPLY_DEFAULT_CONFIG_OPTIONS = [
59 MainConfigNames::DBcompress,
60 MainConfigNames::DBDefaultGroup,
61 MainConfigNames::DBmwschema,
62 MainConfigNames::DBname,
63 MainConfigNames::DBpassword,
64 MainConfigNames::DBport,
65 MainConfigNames::DBprefix,
66 MainConfigNames::DBserver,
67 MainConfigNames::DBservers,
68 MainConfigNames::DBssl,
69 MainConfigNames::DBStrictWarnings,
70 MainConfigNames::DBtype,
71 MainConfigNames::DBuser,
72 MainConfigNames::DebugDumpSql,
73 MainConfigNames::DebugLogFile,
74 MainConfigNames::DebugToolbar,
75 MainConfigNames::ExternalServers,
76 MainConfigNames::SQLiteDataDir,
77 MainConfigNames::SQLMode,
78 MainConfigNames::VirtualDomainsMapping,
80 private ServiceOptions $options;
81 private ConfiguredReadOnlyMode $readOnlyMode;
82 private ChronologyProtector $chronologyProtector;
83 private BagOStuff $srvCache;
84 private WANObjectCache $wanCache;
85 private CriticalSectionProvider $csProvider;
86 private StatsdDataFactoryInterface $statsdDataFactory;
87 private TracerInterface $tracer;
88 /** @var string[] */
89 private array $virtualDomains;
91 /**
92 * @param ServiceOptions $options
93 * @param ConfiguredReadOnlyMode $readOnlyMode
94 * @param ChronologyProtector $chronologyProtector
95 * @param BagOStuff $srvCache
96 * @param WANObjectCache $wanCache
97 * @param CriticalSectionProvider $csProvider
98 * @param StatsdDataFactoryInterface $statsdDataFactory
99 * @param string[] $virtualDomains
100 * @param TracerInterface $tracer
102 public function __construct(
103 ServiceOptions $options,
104 ConfiguredReadOnlyMode $readOnlyMode,
105 ChronologyProtector $chronologyProtector,
106 BagOStuff $srvCache,
107 WANObjectCache $wanCache,
108 CriticalSectionProvider $csProvider,
109 StatsdDataFactoryInterface $statsdDataFactory,
110 array $virtualDomains,
111 TracerInterface $tracer
113 $this->options = $options;
114 $this->readOnlyMode = $readOnlyMode;
115 $this->chronologyProtector = $chronologyProtector;
116 $this->srvCache = $srvCache;
117 $this->wanCache = $wanCache;
118 $this->csProvider = $csProvider;
119 $this->statsdDataFactory = $statsdDataFactory;
120 $this->virtualDomains = $virtualDomains;
121 $this->tracer = $tracer;
125 * @param array $lbConf Config for LBFactory::__construct()
126 * @return array
127 * @internal For use with service wiring
129 public function applyDefaultConfig( array $lbConf ): array {
130 $this->options->assertRequiredOptions( self::APPLY_DEFAULT_CONFIG_OPTIONS );
132 $typesWithSchema = self::getDbTypesWithSchemas();
133 if ( Profiler::instance() instanceof ProfilerStub ) {
134 $profilerCallback = null;
135 } else {
136 $profilerCallback = static function ( $section ) {
137 return Profiler::instance()->scopedProfileIn( $section );
141 $lbConf += [
142 'localDomain' => new DatabaseDomain(
143 $this->options->get( MainConfigNames::DBname ),
144 $this->options->get( MainConfigNames::DBmwschema ),
145 $this->options->get( MainConfigNames::DBprefix )
147 'profiler' => $profilerCallback,
148 'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
149 'logger' => LoggerFactory::getInstance( 'rdbms' ),
150 'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
151 'deprecationLogger' => [ static::class, 'logDeprecation' ],
152 'statsdDataFactory' => $this->statsdDataFactory,
153 'cliMode' => MW_ENTRY_POINT === 'cli',
154 'readOnlyReason' => $this->readOnlyMode->getReason(),
155 'defaultGroup' => $this->options->get( MainConfigNames::DBDefaultGroup ),
156 'criticalSectionProvider' => $this->csProvider,
159 $serversCheck = [];
160 // When making changes here, remember to also specify MediaWiki-specific options
161 // for Database classes in the relevant Installer subclass.
162 // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams.
163 if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) {
164 if ( isset( $lbConf['servers'] ) ) {
165 // Server array is already explicitly configured
166 } elseif ( is_array( $this->options->get( MainConfigNames::DBservers ) ) ) {
167 $lbConf['servers'] = [];
168 foreach ( $this->options->get( MainConfigNames::DBservers ) as $i => $server ) {
169 $lbConf['servers'][$i] = self::initServerInfo( $server, $this->options );
171 } else {
172 $server = self::initServerInfo(
174 'host' => $this->options->get( MainConfigNames::DBserver ),
175 'user' => $this->options->get( MainConfigNames::DBuser ),
176 'password' => $this->options->get( MainConfigNames::DBpassword ),
177 'dbname' => $this->options->get( MainConfigNames::DBname ),
178 'type' => $this->options->get( MainConfigNames::DBtype ),
179 'load' => 1
181 $this->options
184 if ( $this->options->get( MainConfigNames::DBssl ) ) {
185 $server['ssl'] = true;
187 $server['flags'] |= $this->options->get( MainConfigNames::DBcompress ) ? DBO_COMPRESS : 0;
188 if ( $this->options->get( MainConfigNames::DBStrictWarnings ) ) {
189 $server['strictWarnings'] = true;
192 $lbConf['servers'] = [ $server ];
194 if ( !isset( $lbConf['externalClusters'] ) ) {
195 $lbConf['externalClusters'] = $this->options->get( MainConfigNames::ExternalServers );
198 $serversCheck = $lbConf['servers'];
199 } elseif ( $lbConf['class'] === Wikimedia\Rdbms\LBFactoryMulti::class ) {
200 if ( isset( $lbConf['serverTemplate'] ) ) {
201 if ( in_array( $lbConf['serverTemplate']['type'], $typesWithSchema, true ) ) {
202 $lbConf['serverTemplate']['schema'] = $this->options->get( MainConfigNames::DBmwschema );
204 $lbConf['serverTemplate']['sqlMode'] = $this->options->get( MainConfigNames::SQLMode );
205 $serversCheck = [ $lbConf['serverTemplate'] ];
209 self::assertValidServerConfigs(
210 $serversCheck,
211 $this->options->get( MainConfigNames::DBname ),
212 $this->options->get( MainConfigNames::DBprefix )
215 $lbConf['chronologyProtector'] = $this->chronologyProtector;
216 $lbConf['srvCache'] = $this->srvCache;
217 $lbConf['wanCache'] = $this->wanCache;
218 $lbConf['tracer'] = $this->tracer;
219 $lbConf['virtualDomains'] = array_merge( $this->virtualDomains, self::CORE_VIRTUAL_DOMAINS );
220 $lbConf['virtualDomainsMapping'] = $this->options->get( MainConfigNames::VirtualDomainsMapping );
222 return $lbConf;
225 private function getDbTypesWithSchemas(): array {
226 return [ 'postgres' ];
230 * @param array $server
231 * @param ServiceOptions $options
232 * @return array
234 private function initServerInfo( array $server, ServiceOptions $options ): array {
235 if ( $server['type'] === 'sqlite' ) {
236 $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null;
237 // T93097: hint for how file-based databases (e.g. sqlite) should go about locking.
238 // See https://www.sqlite.org/lang_transaction.html
239 // See https://www.sqlite.org/lockingv3.html#shared_lock
240 $isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
241 if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) {
242 // Hack to support some re-entrant invocations using sqlite
243 // See: T259685, T91820
244 $request = \MediaWiki\Rest\EntryPoint::getMainRequest();
245 if ( $request->hasHeader( 'Promise-Non-Write-API-Action' ) ) {
246 $isHttpRead = true;
249 $server += [
250 'dbDirectory' => $options->get( MainConfigNames::SQLiteDataDir ),
251 'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE'
253 } elseif ( $server['type'] === 'postgres' ) {
254 $server += [ 'port' => $options->get( MainConfigNames::DBport ) ];
257 if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) {
258 $server += [ 'schema' => $options->get( MainConfigNames::DBmwschema ) ];
261 $flags = $server['flags'] ?? DBO_DEFAULT;
262 if ( $options->get( MainConfigNames::DebugDumpSql )
263 || $options->get( MainConfigNames::DebugLogFile )
264 || $options->get( MainConfigNames::DebugToolbar )
266 $flags |= DBO_DEBUG;
268 $server['flags'] = $flags;
270 $server += [
271 'tablePrefix' => $options->get( MainConfigNames::DBprefix ),
272 'sqlMode' => $options->get( MainConfigNames::SQLMode ),
275 return $server;
279 * @param array $servers
280 * @param string $ldDB Local domain database name
281 * @param string $ldTP Local domain prefix
283 private function assertValidServerConfigs( array $servers, string $ldDB, string $ldTP ): void {
284 foreach ( $servers as $server ) {
285 $type = $server['type'] ?? null;
286 $srvDB = $server['dbname'] ?? null; // server DB
287 $srvTP = $server['tablePrefix'] ?? ''; // server table prefix
289 if ( $type === 'mysql' ) {
290 // A DB name is not needed to connect to mysql; 'dbname' is useless.
291 // This field only defines the DB to use for unspecified DB domains.
292 if ( $srvDB !== null && $srvDB !== $ldDB ) {
293 self::reportMismatchedDBs( $srvDB, $ldDB );
295 } elseif ( $type === 'postgres' ) {
296 if ( $srvTP !== '' ) {
297 self::reportIfPrefixSet( $srvTP, $type );
301 if ( $srvTP !== '' && $srvTP !== $ldTP ) {
302 self::reportMismatchedPrefixes( $srvTP, $ldTP );
308 * @param string $prefix Table prefix
309 * @param string $dbType Database type
310 * @return never
312 private function reportIfPrefixSet( string $prefix, string $dbType ) {
313 $e = new UnexpectedValueException(
314 "\$wgDBprefix is set to '$prefix' but the database type is '$dbType'. " .
315 "MediaWiki does not support using a table prefix with this RDBMS type."
317 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW );
318 exit;
322 * @param string $srvDB Server config database
323 * @param string $ldDB Local DB domain database
324 * @return never
326 private function reportMismatchedDBs( string $srvDB, string $ldDB ) {
327 $e = new UnexpectedValueException(
328 "\$wgDBservers has dbname='$srvDB' but \$wgDBname='$ldDB'. " .
329 "Set \$wgDBname to the database used by this wiki project. " .
330 "There is rarely a need to set 'dbname' in \$wgDBservers. " .
331 "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " .
332 "use of Database::getDomainId(), and other features are not reliable when " .
333 "\$wgDBservers does not match the local wiki database/prefix."
335 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW );
336 exit;
340 * @param string $srvTP Server config table prefix
341 * @param string $ldTP Local DB domain database
342 * @return never
344 private function reportMismatchedPrefixes( string $srvTP, string $ldTP ) {
345 $e = new UnexpectedValueException(
346 "\$wgDBservers has tablePrefix='$srvTP' but \$wgDBprefix='$ldTP'. " .
347 "Set \$wgDBprefix to the table prefix used by this wiki project. " .
348 "There is rarely a need to set 'tablePrefix' in \$wgDBservers. " .
349 "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " .
350 "use of Database::getDomainId(), and other features are not reliable when " .
351 "\$wgDBservers does not match the local wiki database/prefix."
353 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW );
354 exit;
358 * Decide which LBFactory class to use.
360 * @internal For use by ServiceWiring
361 * @param array $config (e.g. $wgLBFactoryConf)
362 * @return string Class name
364 public function getLBFactoryClass( array $config ): string {
365 $compat = [
366 // For LocalSettings.php compat after removing underscores (since 1.23).
367 'LBFactory_Single' => Wikimedia\Rdbms\LBFactorySingle::class,
368 'LBFactory_Simple' => Wikimedia\Rdbms\LBFactorySimple::class,
369 'LBFactory_Multi' => Wikimedia\Rdbms\LBFactoryMulti::class,
370 // For LocalSettings.php compat after moving classes to namespaces (since 1.29).
371 'LBFactorySingle' => Wikimedia\Rdbms\LBFactorySingle::class,
372 'LBFactorySimple' => Wikimedia\Rdbms\LBFactorySimple::class,
373 'LBFactoryMulti' => Wikimedia\Rdbms\LBFactoryMulti::class
376 $class = $config['class'];
377 return $compat[$class] ?? $class;
380 public function setDomainAliases( ILBFactory $lbFactory ): void {
381 $domain = DatabaseDomain::newFromId( $lbFactory->getLocalDomainID() );
382 // For compatibility with hyphenated $wgDBname values on older wikis, handle callers
383 // that assume corresponding database domain IDs and wiki IDs have identical values
384 $rawLocalDomain = strlen( $domain->getTablePrefix() )
385 ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}"
386 : (string)$domain->getDatabase();
388 $lbFactory->setDomainAliases( [ $rawLocalDomain => $domain ] );
392 * Log a database deprecation warning
394 * @param string $msg Deprecation message
396 public static function logDeprecation( string $msg ): void {
397 if ( isset( self::$loggedDeprecations[$msg] ) ) {
398 return;
400 self::$loggedDeprecations[$msg] = true;
401 MWDebug::sendRawDeprecated( $msg, true, wfGetCaller() );