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
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
;
39 * MediaWiki-specific class for generating database load balancers
41 * @internal For use by core ServiceWiring only.
46 /** Cache of already-logged deprecation messages */
47 private static array $loggedDeprecations = [];
49 public const CORE_VIRTUAL_DOMAINS
= [
50 'virtual-botpasswords',
52 'virtual-interwiki-interlanguage',
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;
89 private array $virtualDomains;
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,
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()
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;
136 $profilerCallback = static function ( $section ) {
137 return Profiler
::instance()->scopedProfileIn( $section );
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
,
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
);
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
),
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(
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
);
225 private function getDbTypesWithSchemas(): array {
226 return [ 'postgres' ];
230 * @param array $server
231 * @param ServiceOptions $options
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' ) ) {
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
)
268 $server['flags'] = $flags;
271 'tablePrefix' => $options->get( MainConfigNames
::DBprefix
),
272 'sqlMode' => $options->get( MainConfigNames
::SQLMode
),
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
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
);
322 * @param string $srvDB Server config database
323 * @param string $ldDB Local DB domain database
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
);
340 * @param string $srvTP Server config table prefix
341 * @param string $ldTP Local DB domain database
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
);
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 {
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] ) ) {
400 self
::$loggedDeprecations[$msg] = true;
401 MWDebug
::sendRawDeprecated( $msg, true, wfGetCaller() );