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 24use MediaWiki\Config\ServiceOptions; 25use MediaWiki\Logger\LoggerFactory; 26use Wikimedia\Rdbms\DatabaseDomain; 27use Wikimedia\Rdbms\ILBFactory; 28 29/** 30 * MediaWiki-specific class for generating database load balancers 31 * @ingroup Database 32 */ 33abstract class MWLBFactory { 34 35 /** @var array Cache of already-logged deprecation messages */ 36 private static $loggedDeprecations = []; 37 38 /** 39 * @var array 40 * @since 1.34 41 */ 42 public const APPLY_DEFAULT_CONFIG_OPTIONS = [ 43 'DBcompress', 44 'DBDefaultGroup', 45 'DBmwschema', 46 'DBname', 47 'DBpassword', 48 'DBport', 49 'DBprefix', 50 'DBserver', 51 'DBservers', 52 'DBssl', 53 'DBtype', 54 'DBuser', 55 'DebugDumpSql', 56 'DebugLogFile', 57 'DebugToolbar', 58 'ExternalServers', 59 'SQLiteDataDir', 60 'SQLMode', 61 ]; 62 63 /** 64 * @param array $lbConf Config for LBFactory::__construct() 65 * @param ServiceOptions $options 66 * @param ConfiguredReadOnlyMode $readOnlyMode 67 * @param BagOStuff $srvCache 68 * @param BagOStuff $mainStash 69 * @param WANObjectCache $wanCache 70 * @return array 71 * @internal For use with service wiring 72 */ 73 public static function applyDefaultConfig( 74 array $lbConf, 75 ServiceOptions $options, 76 ConfiguredReadOnlyMode $readOnlyMode, 77 BagOStuff $srvCache, 78 BagOStuff $mainStash, 79 WANObjectCache $wanCache 80 ) { 81 $options->assertRequiredOptions( self::APPLY_DEFAULT_CONFIG_OPTIONS ); 82 83 global $wgCommandLineMode; 84 85 $typesWithSchema = self::getDbTypesWithSchemas(); 86 87 $lbConf += [ 88 'localDomain' => new DatabaseDomain( 89 $options->get( 'DBname' ), 90 $options->get( 'DBmwschema' ), 91 $options->get( 'DBprefix' ) 92 ), 93 'profiler' => function ( $section ) { 94 return Profiler::instance()->scopedProfileIn( $section ); 95 }, 96 'trxProfiler' => Profiler::instance()->getTransactionProfiler(), 97 'replLogger' => LoggerFactory::getInstance( 'DBReplication' ), 98 'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ), 99 'connLogger' => LoggerFactory::getInstance( 'DBConnection' ), 100 'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ), 101 'errorLogger' => [ MWExceptionHandler::class, 'logException' ], 102 'deprecationLogger' => [ static::class, 'logDeprecation' ], 103 'cliMode' => $wgCommandLineMode, 104 'hostname' => wfHostname(), 105 'readOnlyReason' => $readOnlyMode->getReason(), 106 'defaultGroup' => $options->get( 'DBDefaultGroup' ), 107 ]; 108 109 $serversCheck = []; 110 // When making changes here, remember to also specify MediaWiki-specific options 111 // for Database classes in the relevant Installer subclass. 112 // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams. 113 if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) { 114 if ( isset( $lbConf['servers'] ) ) { 115 // Server array is already explicitly configured 116 } elseif ( is_array( $options->get( 'DBservers' ) ) ) { 117 $lbConf['servers'] = []; 118 foreach ( $options->get( 'DBservers' ) as $i => $server ) { 119 $lbConf['servers'][$i] = self::initServerInfo( $server, $options ); 120 } 121 } else { 122 $server = self::initServerInfo( 123 [ 124 'host' => $options->get( 'DBserver' ), 125 'user' => $options->get( 'DBuser' ), 126 'password' => $options->get( 'DBpassword' ), 127 'dbname' => $options->get( 'DBname' ), 128 'type' => $options->get( 'DBtype' ), 129 'load' => 1 130 ], 131 $options 132 ); 133 134 $server['flags'] |= $options->get( 'DBssl' ) ? DBO_SSL : 0; 135 $server['flags'] |= $options->get( 'DBcompress' ) ? DBO_COMPRESS : 0; 136 137 $lbConf['servers'] = [ $server ]; 138 } 139 if ( !isset( $lbConf['externalClusters'] ) ) { 140 $lbConf['externalClusters'] = $options->get( 'ExternalServers' ); 141 } 142 143 $serversCheck = $lbConf['servers']; 144 } elseif ( $lbConf['class'] === Wikimedia\Rdbms\LBFactoryMulti::class ) { 145 if ( isset( $lbConf['serverTemplate'] ) ) { 146 if ( in_array( $lbConf['serverTemplate']['type'], $typesWithSchema, true ) ) { 147 $lbConf['serverTemplate']['schema'] = $options->get( 'DBmwschema' ); 148 } 149 $lbConf['serverTemplate']['sqlMode'] = $options->get( 'SQLMode' ); 150 $serversCheck = [ $lbConf['serverTemplate'] ]; 151 } 152 } 153 154 self::assertValidServerConfigs( 155 $serversCheck, 156 $options->get( 'DBname' ), 157 $options->get( 'DBprefix' ) 158 ); 159 160 $lbConf['srvCache'] = $srvCache; 161 $lbConf['memStash'] = $mainStash; 162 $lbConf['wanCache'] = $wanCache; 163 164 return $lbConf; 165 } 166 167 /** 168 * @return array 169 */ 170 private static function getDbTypesWithSchemas() { 171 return [ 'postgres' ]; 172 } 173 174 /** 175 * @param array $server 176 * @param ServiceOptions $options 177 * @return array 178 */ 179 private static function initServerInfo( array $server, ServiceOptions $options ) { 180 if ( $server['type'] === 'sqlite' ) { 181 $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null; 182 // T93097: hint for how file-based databases (e.g. sqlite) should go about locking. 183 // See https://www.sqlite.org/lang_transaction.html 184 // See https://www.sqlite.org/lockingv3.html#shared_lock 185 $isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] ); 186 if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) { 187 // Hack to support some re-entrant invocations using sqlite 188 // See: T259685, T91820 189 $request = \MediaWiki\Rest\EntryPoint::getMainRequest(); 190 if ( $request->hasHeader( 'Promise-Non-Write-API-Action' ) ) { 191 $isHttpRead = true; 192 } 193 } 194 $server += [ 195 'dbDirectory' => $options->get( 'SQLiteDataDir' ), 196 'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE' 197 ]; 198 } elseif ( $server['type'] === 'postgres' ) { 199 $server += [ 200 'port' => $options->get( 'DBport' ), 201 // Work around the reserved word usage in MediaWiki schema 202 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ] 203 ]; 204 } 205 206 if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) { 207 $server += [ 'schema' => $options->get( 'DBmwschema' ) ]; 208 } 209 210 $flags = $server['flags'] ?? DBO_DEFAULT; 211 if ( $options->get( 'DebugDumpSql' ) 212 || $options->get( 'DebugLogFile' ) 213 || $options->get( 'DebugToolbar' ) 214 ) { 215 $flags |= DBO_DEBUG; 216 } 217 $server['flags'] = $flags; 218 219 $server += [ 220 'tablePrefix' => $options->get( 'DBprefix' ), 221 'sqlMode' => $options->get( 'SQLMode' ), 222 ]; 223 224 return $server; 225 } 226 227 /** 228 * @param array $servers 229 * @param string $ldDB Local domain database name 230 * @param string $ldTP Local domain prefix 231 */ 232 private static function assertValidServerConfigs( array $servers, $ldDB, $ldTP ) { 233 foreach ( $servers as $server ) { 234 $type = $server['type'] ?? null; 235 $srvDB = $server['dbname'] ?? null; // server DB 236 $srvTP = $server['tablePrefix'] ?? ''; // server table prefix 237 238 if ( $type === 'mysql' ) { 239 // A DB name is not needed to connect to mysql; 'dbname' is useless. 240 // This field only defines the DB to use for unspecified DB domains. 241 if ( $srvDB !== null && $srvDB !== $ldDB ) { 242 self::reportMismatchedDBs( $srvDB, $ldDB ); 243 } 244 } elseif ( $type === 'postgres' ) { 245 if ( $srvTP !== '' ) { 246 self::reportIfPrefixSet( $srvTP, $type ); 247 } 248 } 249 250 if ( $srvTP !== '' && $srvTP !== $ldTP ) { 251 self::reportMismatchedPrefixes( $srvTP, $ldTP ); 252 } 253 } 254 } 255 256 /** 257 * @param string $prefix Table prefix 258 * @param string $dbType Database type 259 */ 260 private static function reportIfPrefixSet( $prefix, $dbType ) { 261 $e = new UnexpectedValueException( 262 "\$wgDBprefix is set to '$prefix' but the database type is '$dbType'. " . 263 "MediaWiki does not support using a table prefix with this RDBMS type." 264 ); 265 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY ); 266 exit; 267 } 268 269 /** 270 * @param string $srvDB Server config database 271 * @param string $ldDB Local DB domain database 272 */ 273 private static function reportMismatchedDBs( $srvDB, $ldDB ) { 274 $e = new UnexpectedValueException( 275 "\$wgDBservers has dbname='$srvDB' but \$wgDBname='$ldDB'. " . 276 "Set \$wgDBname to the database used by this wiki project. " . 277 "There is rarely a need to set 'dbname' in \$wgDBservers. " . 278 "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " . 279 "use of Database::getDomainId(), and other features are not reliable when " . 280 "\$wgDBservers does not match the local wiki database/prefix." 281 ); 282 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY ); 283 exit; 284 } 285 286 /** 287 * @param string $srvTP Server config table prefix 288 * @param string $ldTP Local DB domain database 289 */ 290 private static function reportMismatchedPrefixes( $srvTP, $ldTP ) { 291 $e = new UnexpectedValueException( 292 "\$wgDBservers has tablePrefix='$srvTP' but \$wgDBprefix='$ldTP'. " . 293 "Set \$wgDBprefix to the table prefix used by this wiki project. " . 294 "There is rarely a need to set 'tablePrefix' in \$wgDBservers. " . 295 "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " . 296 "use of Database::getDomainId(), and other features are not reliable when " . 297 "\$wgDBservers does not match the local wiki database/prefix." 298 ); 299 MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY ); 300 exit; 301 } 302 303 /** 304 * Decide which LBFactory class to use. 305 * 306 * @internal For use by ServiceWiring 307 * @param array $config (e.g. $wgLBFactoryConf) 308 * @return string Class name 309 */ 310 public static function getLBFactoryClass( array $config ) { 311 $compat = [ 312 // For LocalSettings.php compat after removing underscores (since 1.23). 313 'LBFactory_Single' => Wikimedia\Rdbms\LBFactorySingle::class, 314 'LBFactory_Simple' => Wikimedia\Rdbms\LBFactorySimple::class, 315 'LBFactory_Multi' => Wikimedia\Rdbms\LBFactoryMulti::class, 316 // For LocalSettings.php compat after moving classes to namespaces (since 1.29). 317 'LBFactorySingle' => Wikimedia\Rdbms\LBFactorySingle::class, 318 'LBFactorySimple' => Wikimedia\Rdbms\LBFactorySimple::class, 319 'LBFactoryMulti' => Wikimedia\Rdbms\LBFactoryMulti::class 320 ]; 321 322 $class = $config['class']; 323 return $compat[$class] ?? $class; 324 } 325 326 /** 327 * @param ILBFactory $lbFactory 328 */ 329 public static function setDomainAliases( ILBFactory $lbFactory ) { 330 $domain = DatabaseDomain::newFromId( $lbFactory->getLocalDomainID() ); 331 // For compatibility with hyphenated $wgDBname values on older wikis, handle callers 332 // that assume corresponding database domain IDs and wiki IDs have identical values 333 $rawLocalDomain = strlen( $domain->getTablePrefix() ) 334 ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}" 335 : (string)$domain->getDatabase(); 336 337 $lbFactory->setDomainAliases( [ $rawLocalDomain => $domain ] ); 338 } 339 340 /** 341 * Log a database deprecation warning 342 * @param string $msg Deprecation message 343 * @internal For use with service wiring 344 */ 345 public static function logDeprecation( $msg ) { 346 if ( isset( self::$loggedDeprecations[$msg] ) ) { 347 return; 348 } 349 self::$loggedDeprecations[$msg] = true; 350 MWDebug::sendRawDeprecated( $msg, true, wfGetCaller() ); 351 } 352} 353