MediaWiki  master
MWLBFactory.php
Go to the documentation of this file.
1 <?php
24 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
34 use Wikimedia\RequestTimeout\CriticalSectionProvider;
35 
42 class MWLBFactory {
43 
45  private static $loggedDeprecations = [];
46 
51  'CommandLineMode',
52  MainConfigNames::DBcompress,
53  MainConfigNames::DBDefaultGroup,
54  MainConfigNames::DBmwschema,
55  MainConfigNames::DBname,
56  MainConfigNames::DBpassword,
57  MainConfigNames::DBport,
58  MainConfigNames::DBprefix,
59  MainConfigNames::DBserver,
60  MainConfigNames::DBservers,
61  MainConfigNames::DBssl,
62  MainConfigNames::DBtype,
63  MainConfigNames::DBuser,
64  MainConfigNames::DebugDumpSql,
65  MainConfigNames::DebugLogFile,
66  MainConfigNames::DebugToolbar,
67  MainConfigNames::ExternalServers,
68  MainConfigNames::SQLiteDataDir,
69  MainConfigNames::SQLMode,
70  ];
74  private $options;
78  private $readOnlyMode;
82  private $cpStash;
86  private $srvCache;
90  private $wanCache;
94  private $csProvider;
98  private $statsdDataFactory;
102  private $databaseFactory;
103 
114  public function __construct(
115  ServiceOptions $options,
116  ConfiguredReadOnlyMode $readOnlyMode,
117  BagOStuff $cpStash,
118  BagOStuff $srvCache,
119  WANObjectCache $wanCache,
120  CriticalSectionProvider $csProvider,
121  StatsdDataFactoryInterface $statsdDataFactory,
122  DatabaseFactory $databaseFactory
123  ) {
124  $this->options = $options;
125  $this->readOnlyMode = $readOnlyMode;
126  $this->cpStash = $cpStash;
127  $this->srvCache = $srvCache;
128  $this->wanCache = $wanCache;
129  $this->csProvider = $csProvider;
130  $this->statsdDataFactory = $statsdDataFactory;
131  $this->databaseFactory = $databaseFactory;
132  }
133 
139  public function applyDefaultConfig( array $lbConf ) {
140  $this->options->assertRequiredOptions( self::APPLY_DEFAULT_CONFIG_OPTIONS );
141 
142  $typesWithSchema = self::getDbTypesWithSchemas();
143 
144  $lbConf += [
145  'localDomain' => new DatabaseDomain(
146  $this->options->get( MainConfigNames::DBname ),
147  $this->options->get( MainConfigNames::DBmwschema ),
148  $this->options->get( MainConfigNames::DBprefix )
149  ),
150  'profiler' => static function ( $section ) {
151  return Profiler::instance()->scopedProfileIn( $section );
152  },
153  'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
154  'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
155  'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
156  'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
157  'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ),
158  'errorLogger' => [ MWExceptionHandler::class, 'logException' ],
159  'deprecationLogger' => [ static::class, 'logDeprecation' ],
160  'statsdDataFactory' => $this->statsdDataFactory,
161  'cliMode' => $this->options->get( 'CommandLineMode' ),
162  'readOnlyReason' => $this->readOnlyMode->getReason(),
163  'defaultGroup' => $this->options->get( MainConfigNames::DBDefaultGroup ),
164  'criticalSectionProvider' => $this->csProvider
165  ];
166 
167  $serversCheck = [];
168  // When making changes here, remember to also specify MediaWiki-specific options
169  // for Database classes in the relevant Installer subclass.
170  // Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams.
171  if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) {
172  if ( isset( $lbConf['servers'] ) ) {
173  // Server array is already explicitly configured
174  } elseif ( is_array( $this->options->get( MainConfigNames::DBservers ) ) ) {
175  $lbConf['servers'] = [];
176  foreach ( $this->options->get( MainConfigNames::DBservers ) as $i => $server ) {
177  $lbConf['servers'][$i] = self::initServerInfo( $server, $this->options );
178  }
179  } else {
180  $server = self::initServerInfo(
181  [
182  'host' => $this->options->get( MainConfigNames::DBserver ),
183  'user' => $this->options->get( MainConfigNames::DBuser ),
184  'password' => $this->options->get( MainConfigNames::DBpassword ),
185  'dbname' => $this->options->get( MainConfigNames::DBname ),
186  'type' => $this->options->get( MainConfigNames::DBtype ),
187  'load' => 1
188  ],
189  $this->options
190  );
191 
192  if ( $this->options->get( MainConfigNames::DBssl ) ) {
193  $server['ssl'] = true;
194  }
195  $server['flags'] |= $this->options->get( MainConfigNames::DBcompress ) ? DBO_COMPRESS : 0;
196 
197  $lbConf['servers'] = [ $server ];
198  }
199  if ( !isset( $lbConf['externalClusters'] ) ) {
200  $lbConf['externalClusters'] = $this->options->get( MainConfigNames::ExternalServers );
201  }
202 
203  $serversCheck = $lbConf['servers'];
204  } elseif ( $lbConf['class'] === Wikimedia\Rdbms\LBFactoryMulti::class ) {
205  if ( isset( $lbConf['serverTemplate'] ) ) {
206  if ( in_array( $lbConf['serverTemplate']['type'], $typesWithSchema, true ) ) {
207  $lbConf['serverTemplate']['schema'] = $this->options->get( MainConfigNames::DBmwschema );
208  }
209  $lbConf['serverTemplate']['sqlMode'] = $this->options->get( MainConfigNames::SQLMode );
210  $serversCheck = [ $lbConf['serverTemplate'] ];
211  }
212  }
213 
214  self::assertValidServerConfigs(
215  $serversCheck,
216  $this->options->get( MainConfigNames::DBname ),
217  $this->options->get( MainConfigNames::DBprefix )
218  );
219 
220  $lbConf['cpStash'] = $this->cpStash;
221  $lbConf['srvCache'] = $this->srvCache;
222  $lbConf['wanCache'] = $this->wanCache;
223  $lbConf['databaseFactory'] = $this->databaseFactory;
224 
225  return $lbConf;
226  }
227 
231  private function getDbTypesWithSchemas() {
232  return [ 'postgres' ];
233  }
234 
240  private function initServerInfo( array $server, ServiceOptions $options ) {
241  if ( $server['type'] === 'sqlite' ) {
242  $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null;
243  // T93097: hint for how file-based databases (e.g. sqlite) should go about locking.
244  // See https://www.sqlite.org/lang_transaction.html
245  // See https://www.sqlite.org/lockingv3.html#shared_lock
246  $isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
247  if ( MW_ENTRY_POINT === 'rest' && !$isHttpRead ) {
248  // Hack to support some re-entrant invocations using sqlite
249  // See: T259685, T91820
251  if ( $request->hasHeader( 'Promise-Non-Write-API-Action' ) ) {
252  $isHttpRead = true;
253  }
254  }
255  $server += [
256  'dbDirectory' => $options->get( MainConfigNames::SQLiteDataDir ),
257  'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE'
258  ];
259  } elseif ( $server['type'] === 'postgres' ) {
260  $server += [ 'port' => $options->get( MainConfigNames::DBport ) ];
261  }
262 
263  if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) {
264  $server += [ 'schema' => $options->get( MainConfigNames::DBmwschema ) ];
265  }
266 
267  $flags = $server['flags'] ?? DBO_DEFAULT;
268  if ( $options->get( MainConfigNames::DebugDumpSql )
269  || $options->get( MainConfigNames::DebugLogFile )
270  || $options->get( MainConfigNames::DebugToolbar )
271  ) {
272  $flags |= DBO_DEBUG;
273  }
274  $server['flags'] = $flags;
275 
276  $server += [
277  'tablePrefix' => $options->get( MainConfigNames::DBprefix ),
278  'sqlMode' => $options->get( MainConfigNames::SQLMode ),
279  ];
280 
281  return $server;
282  }
283 
289  private function assertValidServerConfigs( array $servers, $ldDB, $ldTP ) {
290  foreach ( $servers as $server ) {
291  $type = $server['type'] ?? null;
292  $srvDB = $server['dbname'] ?? null; // server DB
293  $srvTP = $server['tablePrefix'] ?? ''; // server table prefix
294 
295  if ( $type === 'mysql' ) {
296  // A DB name is not needed to connect to mysql; 'dbname' is useless.
297  // This field only defines the DB to use for unspecified DB domains.
298  if ( $srvDB !== null && $srvDB !== $ldDB ) {
299  self::reportMismatchedDBs( $srvDB, $ldDB );
300  }
301  } elseif ( $type === 'postgres' ) {
302  if ( $srvTP !== '' ) {
303  self::reportIfPrefixSet( $srvTP, $type );
304  }
305  }
306 
307  if ( $srvTP !== '' && $srvTP !== $ldTP ) {
308  self::reportMismatchedPrefixes( $srvTP, $ldTP );
309  }
310  }
311  }
312 
318  private function reportIfPrefixSet( $prefix, $dbType ) {
319  $e = new UnexpectedValueException(
320  "\$wgDBprefix is set to '$prefix' but the database type is '$dbType'. " .
321  "MediaWiki does not support using a table prefix with this RDBMS type."
322  );
324  exit;
325  }
326 
332  private function reportMismatchedDBs( $srvDB, $ldDB ) {
333  $e = new UnexpectedValueException(
334  "\$wgDBservers has dbname='$srvDB' but \$wgDBname='$ldDB'. " .
335  "Set \$wgDBname to the database used by this wiki project. " .
336  "There is rarely a need to set 'dbname' in \$wgDBservers. " .
337  "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " .
338  "use of Database::getDomainId(), and other features are not reliable when " .
339  "\$wgDBservers does not match the local wiki database/prefix."
340  );
342  exit;
343  }
344 
350  private function reportMismatchedPrefixes( $srvTP, $ldTP ) {
351  $e = new UnexpectedValueException(
352  "\$wgDBservers has tablePrefix='$srvTP' but \$wgDBprefix='$ldTP'. " .
353  "Set \$wgDBprefix to the table prefix used by this wiki project. " .
354  "There is rarely a need to set 'tablePrefix' in \$wgDBservers. " .
355  "Cross-wiki database access, use of WikiMap::getCurrentWikiDbDomain(), " .
356  "use of Database::getDomainId(), and other features are not reliable when " .
357  "\$wgDBservers does not match the local wiki database/prefix."
358  );
360  exit;
361  }
362 
370  public function getLBFactoryClass( array $config ) {
371  $compat = [
372  // For LocalSettings.php compat after removing underscores (since 1.23).
373  'LBFactory_Single' => Wikimedia\Rdbms\LBFactorySingle::class,
374  'LBFactory_Simple' => Wikimedia\Rdbms\LBFactorySimple::class,
375  'LBFactory_Multi' => Wikimedia\Rdbms\LBFactoryMulti::class,
376  // For LocalSettings.php compat after moving classes to namespaces (since 1.29).
377  'LBFactorySingle' => Wikimedia\Rdbms\LBFactorySingle::class,
378  'LBFactorySimple' => Wikimedia\Rdbms\LBFactorySimple::class,
379  'LBFactoryMulti' => Wikimedia\Rdbms\LBFactoryMulti::class
380  ];
381 
382  $class = $config['class'];
383  return $compat[$class] ?? $class;
384  }
385 
389  public function setDomainAliases( ILBFactory $lbFactory ) {
390  $domain = DatabaseDomain::newFromId( $lbFactory->getLocalDomainID() );
391  // For compatibility with hyphenated $wgDBname values on older wikis, handle callers
392  // that assume corresponding database domain IDs and wiki IDs have identical values
393  $rawLocalDomain = strlen( $domain->getTablePrefix() )
394  ? "{$domain->getDatabase()}-{$domain->getTablePrefix()}"
395  : (string)$domain->getDatabase();
396 
397  $lbFactory->setDomainAliases( [ $rawLocalDomain => $domain ] );
398  }
399 
423  public function applyGlobalState(
424  ILBFactory $lbFactory,
425  Config $config,
427  ): void {
428  // Use the global WebRequest singleton. The main reason for using this
429  // is to call WebRequest::getIP() which is non-trivial to reproduce statically
430  // because it needs $wgUsePrivateIPs, as well as ProxyLookup and HookRunner services.
431  // TODO: Create a static version of WebRequest::getIP that accepts these three
432  // as dependencies, and then call that here. The other uses of $req below can
433  // trivially use $_COOKIES, $_GET and $_SERVER instead.
434  $req = RequestContext::getMain()->getRequest();
435 
436  // Set user IP/agent information for agent session consistency purposes
437  $reqStart = (int)( $_SERVER['REQUEST_TIME_FLOAT'] ?? time() );
438  $cpPosInfo = LBFactory::getCPInfoFromCookieValue(
439  // The cookie has no prefix and is set by MediaWiki::preOutputCommit()
440  $req->getCookie( 'cpPosIndex', '' ),
441  // Mitigate broken client-side cookie expiration handling (T190082)
442  $reqStart - ChronologyProtector::POSITION_COOKIE_TTL
443  );
444  $lbFactory->setRequestInfo( [
445  'IPAddress' => $req->getIP(),
446  'UserAgent' => $req->getHeader( 'User-Agent' ),
447  'ChronologyProtection' => $req->getHeader( 'MediaWiki-Chronology-Protection' ),
448  'ChronologyPositionIndex' => $req->getInt( 'cpPosIndex', $cpPosInfo['index'] ),
449  'ChronologyClientId' => $cpPosInfo['clientId']
450  ?? $req->getHeader( 'MediaWiki-Chronology-Client-Id' )
451  ] );
452 
453  if ( $config->get( 'CommandLineMode' ) ) {
454  // Disable buffering and delaying of DeferredUpdates and stats
455  // for maintenance scripts and PHPUnit tests.
456  // Hook into period lag checks which often happen in long-running scripts
457  $lbFactory->setWaitForReplicationListener(
458  __METHOD__,
459  static function () use ( $stats, $config ) {
461  // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385)
462  MediaWiki::emitBufferedStatsdData( $stats, $config );
463  }
464  );
465  // Check for other windows to run them. A script may read or do a few writes
466  // to the primary DB but mostly be writing to something else, like a file store.
467  $lbFactory->getMainLB()->setTransactionListener(
468  __METHOD__,
469  static function ( $trigger ) use ( $stats, $config ) {
470  if ( $trigger === IDatabase::TRIGGER_COMMIT ) {
472  }
473  // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385)
474  MediaWiki::emitBufferedStatsdData( $stats, $config );
475  }
476  );
477 
478  }
479  }
480 
486  public static function logDeprecation( $msg ) {
487  if ( isset( self::$loggedDeprecations[$msg] ) ) {
488  return;
489  }
490  self::$loggedDeprecations[$msg] = true;
491  MWDebug::sendRawDeprecated( $msg, true, wfGetCaller() );
492  }
493 }
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
const MW_ENTRY_POINT
Definition: api.php:41
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
A read-only mode service which does not depend on LoadBalancer.
static tryOpportunisticExecute()
Consume and execute all pending updates unless an update is already in progress or the ILBFactory ser...
static sendRawDeprecated( $msg, $sendToLog=true, $callerFunc='')
Send a raw deprecation message to the log and the debug toolbar, without filtering of duplicate messa...
Definition: MWDebug.php:370
static output(Throwable $e, $mode, Throwable $eNew=null)
MediaWiki-specific class for generating database load balancers.
Definition: MWLBFactory.php:42
static logDeprecation( $msg)
Log a database deprecation warning.
const APPLY_DEFAULT_CONFIG_OPTIONS
Definition: MWLBFactory.php:50
__construct(ServiceOptions $options, ConfiguredReadOnlyMode $readOnlyMode, BagOStuff $cpStash, BagOStuff $srvCache, WANObjectCache $wanCache, CriticalSectionProvider $csProvider, StatsdDataFactoryInterface $statsdDataFactory, DatabaseFactory $databaseFactory)
setDomainAliases(ILBFactory $lbFactory)
applyDefaultConfig(array $lbConf)
applyGlobalState(ILBFactory $lbFactory, Config $config, IBufferingStatsdDataFactory $stats)
Apply global state from the current web request or other PHP process.
getLBFactoryClass(array $config)
Decide which LBFactory class to use.
A class for passing options to services.
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
static emitBufferedStatsdData(IBufferingStatsdDataFactory $stats, Config $config)
Send out any buffered statsd data according to sampling rules.
Definition: MediaWiki.php:1161
static instance()
Singleton.
Definition: Profiler.php:94
Group all the pieces relevant to the context of a request into one instance.
Multi-datacenter aware caching interface.
Provide a given client with protection against visible database lag.
Class to handle database/schema/prefix specifications for IDatabase.
Constructs Database objects.
Interface for configuration instances.
Definition: Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:40
Manager of ILoadBalancer objects, and indirectly of IDatabase connections.
Definition: ILBFactory.php:31
getLocalDomainID()
Get the local (and default) database domain ID of connection handles.
setRequestInfo(array $info)
Inject HTTP request header/cookie information during setup of this instance.
setDomainAliases(array $aliases)
Convert certain database domains to alternative ones.
getMainLB( $domain=false)
Get the tracked load balancer instance for the main cluster that handles the given domain.
setWaitForReplicationListener( $name, callable $callback=null)
Add a callback to be run in every call to waitForReplication() before waiting.
const DBO_COMPRESS
Definition: defines.php:19
const DBO_DEFAULT
Definition: defines.php:13
const DBO_DEBUG
Definition: defines.php:9