From: Aaron Schulz Date: Wed, 14 Sep 2016 11:34:17 +0000 (-0700) Subject: Rename LBFactory => LBFactoryMW and make LBFactory in /libs X-Git-Tag: 1.31.0-rc.0~5551^2 X-Git-Url: http://git.cyclocoop.org/fichier?a=commitdiff_plain;h=0e5cd18b74d83c50ebe7dd7ee9f09cf375aae9d7;p=lhc%2Fweb%2Fwiklou.git Rename LBFactory => LBFactoryMW and make LBFactory in /libs The former extends the later with MW-specific logic. Also removed a wf* method call from ChronologyProtector. Change-Id: I325f59b7467ab9c2137731d1ce69816f5a020f03 --- diff --git a/autoload.php b/autoload.php index d46d2cedcb..22cf9cdee6 100644 --- a/autoload.php +++ b/autoload.php @@ -653,7 +653,8 @@ $wgAutoloadLocalClasses = [ 'JsonContentHandler' => __DIR__ . '/includes/content/JsonContentHandler.php', 'KkConverter' => __DIR__ . '/languages/classes/LanguageKk.php', 'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php', - 'LBFactory' => __DIR__ . '/includes/db/loadbalancer/LBFactory.php', + 'LBFactory' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactory.php', + 'LBFactoryMW' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMW.php', 'LBFactoryMulti' => __DIR__ . '/includes/db/loadbalancer/LBFactoryMulti.php', 'LBFactorySimple' => __DIR__ . '/includes/db/loadbalancer/LBFactorySimple.php', 'LBFactorySingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php', diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 8734bd6836..4ab412eb46 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -45,7 +45,7 @@ return [ 'DBLoadBalancerFactory' => function( MediaWikiServices $services ) { $config = $services->getMainConfig()->get( 'LBFactoryConf' ); - $class = LBFactory::getLBFactoryClass( $config ); + $class = LBFactoryMW::getLBFactoryClass( $config ); if ( !isset( $config['readOnlyReason'] ) ) { // TODO: replace the global wfConfiguredReadOnlyReason() with a service. $config['readOnlyReason'] = wfConfiguredReadOnlyReason(); diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index caca7e2744..ee82bdf5df 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -129,7 +129,10 @@ class CloneDatabase { */ public static function changePrefix( $prefix ) { global $wgDBprefix; - wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) { + + $lbFactory = wfGetLBFactory(); + $lbFactory->setDomainPrefix( $prefix ); + $lbFactory->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) { $lb->setDomainPrefix( $prefix ); $lb->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) { $db->tablePrefix( $prefix ); diff --git a/includes/db/loadbalancer/LBFactory.php b/includes/db/loadbalancer/LBFactory.php deleted file mode 100644 index 6fd1550ff3..0000000000 --- a/includes/db/loadbalancer/LBFactory.php +++ /dev/null @@ -1,716 +0,0 @@ -readOnlyReason = $conf['readOnlyReason']; - } - // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804) - $sCache = ObjectCache::getLocalServerInstance(); - if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) { - $this->srvCache = $sCache; - } else { - $this->srvCache = new EmptyBagOStuff(); - } - $cCache = ObjectCache::getLocalClusterInstance(); - if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) { - $this->memCache = $cCache; - } else { - $this->memCache = new EmptyBagOStuff(); - } - $wCache = ObjectCache::getMainWANInstance(); - if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) { - $this->wanCache = $wCache; - } else { - $this->wanCache = WANObjectCache::newEmpty(); - } - $this->trxProfiler = Profiler::instance()->getTransactionProfiler(); - $this->trxLogger = LoggerFactory::getInstance( 'DBTransaction' ); - $this->replLogger = LoggerFactory::getInstance( 'DBReplication' ); - $this->chronProt = $this->newChronologyProtector(); - $this->ticket = mt_rand(); - } - - /** - * Disables all load balancers. All connections are closed, and any attempt to - * open a new connection will result in a DBAccessError. - * @see LoadBalancer::disable() - */ - public function destroy() { - $this->shutdown( self::SHUTDOWN_NO_CHRONPROT ); - $this->forEachLBCallMethod( 'disable' ); - } - - /** - * Disables all access to the load balancer, will cause all database access - * to throw a DBAccessError - */ - public static function disableBackend() { - MediaWikiServices::disableStorageBackend(); - } - - /** - * Get an LBFactory instance - * - * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead. - * - * @return LBFactory - */ - public static function singleton() { - return MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); - } - - /** - * Returns the LBFactory class to use and the load balancer configuration. - * - * @todo instead of this, use a ServiceContainer for managing the different implementations. - * - * @param array $config (e.g. $wgLBFactoryConf) - * @return string Class name - */ - public static function getLBFactoryClass( array $config ) { - // For configuration backward compatibility after removing - // underscores from class names in MediaWiki 1.23. - $bcClasses = [ - 'LBFactory_Simple' => 'LBFactorySimple', - 'LBFactory_Single' => 'LBFactorySingle', - 'LBFactory_Multi' => 'LBFactoryMulti', - ]; - - $class = $config['class']; - - if ( isset( $bcClasses[$class] ) ) { - $class = $bcClasses[$class]; - wfDeprecated( - '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details', - '1.23' - ); - } - - return $class; - } - - /** - * Shut down, close connections and destroy the cached instance. - * - * @deprecated since 1.27, use LBFactory::destroy() - */ - public static function destroyInstance() { - MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy(); - } - - /** - * Create a new load balancer object. The resulting object will be untracked, - * not chronology-protected, and the caller is responsible for cleaning it up. - * - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancer - */ - abstract public function newMainLB( $wiki = false ); - - /** - * Get a cached (tracked) load balancer object. - * - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancer - */ - abstract public function getMainLB( $wiki = false ); - - /** - * Create a new load balancer for external storage. The resulting object will be - * untracked, not chronology-protected, and the caller is responsible for - * cleaning it up. - * - * @param string $cluster External storage cluster, or false for core - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancer - */ - abstract protected function newExternalLB( $cluster, $wiki = false ); - - /** - * Get a cached (tracked) load balancer for external storage - * - * @param string $cluster External storage cluster, or false for core - * @param bool|string $wiki Wiki ID, or false for the current wiki - * @return LoadBalancer - */ - abstract public function getExternalLB( $cluster, $wiki = false ); - - /** - * Execute a function for each tracked load balancer - * The callback is called with the load balancer as the first parameter, - * and $params passed as the subsequent parameters. - * - * @param callable $callback - * @param array $params - */ - abstract public function forEachLB( $callback, array $params = [] ); - - /** - * Prepare all tracked load balancers for shutdown - * @param integer $mode One of the class SHUTDOWN_* constants - * @param callable|null $workCallback Work to mask ChronologyProtector writes - */ - public function shutdown( - $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null - ) { - if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) { - $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' ); - } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) { - $this->shutdownChronologyProtector( $this->chronProt, null, 'async' ); - } - - $this->commitMasterChanges( __METHOD__ ); // sanity - } - - /** - * Call a method of each tracked load balancer - * - * @param string $methodName - * @param array $args - */ - private function forEachLBCallMethod( $methodName, array $args = [] ) { - $this->forEachLB( - function ( LoadBalancer $loadBalancer, $methodName, array $args ) { - call_user_func_array( [ $loadBalancer, $methodName ], $args ); - }, - [ $methodName, $args ] - ); - } - - /** - * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot - * - * @param string $fname Caller name - * @since 1.28 - */ - public function flushReplicaSnapshots( $fname = __METHOD__ ) { - $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] ); - } - - /** - * Commit on all connections. Done for two reasons: - * 1. To commit changes to the masters. - * 2. To release the snapshot on all connections, master and replica DB. - * @param string $fname Caller name - * @param array $options Options map: - * - maxWriteDuration: abort if more than this much time was spent in write queries - */ - public function commitAll( $fname = __METHOD__, array $options = [] ) { - $this->commitMasterChanges( $fname, $options ); - $this->forEachLBCallMethod( 'commitAll', [ $fname ] ); - } - - /** - * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set) - * - * The DBO_TRX setting will be reverted to the default in each of these methods: - * - commitMasterChanges() - * - rollbackMasterChanges() - * - commitAll() - * - * This allows for custom transaction rounds from any outer transaction scope. - * - * @param string $fname - * @throws DBTransactionError - * @since 1.28 - */ - public function beginMasterChanges( $fname = __METHOD__ ) { - if ( $this->trxRoundId !== false ) { - throw new DBTransactionError( - null, - "$fname: transaction round '{$this->trxRoundId}' already started." - ); - } - $this->trxRoundId = $fname; - // Set DBO_TRX flags on all appropriate DBs - $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] ); - } - - /** - * Commit changes on all master connections - * @param string $fname Caller name - * @param array $options Options map: - * - maxWriteDuration: abort if more than this much time was spent in write queries - * @throws Exception - */ - public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) { - if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) { - throw new DBTransactionError( - null, - "$fname: transaction round '{$this->trxRoundId}' still running." - ); - } - // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure - $this->forEachLBCallMethod( 'finalizeMasterChanges' ); - $this->trxRoundId = false; - // Perform pre-commit checks, aborting on failure - $this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] ); - // Log the DBs and methods involved in multi-DB transactions - $this->logIfMultiDbTransaction(); - // Actually perform the commit on all master DB connections and revert DBO_TRX - $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] ); - // Run all post-commit callbacks - /** @var Exception $e */ - $e = null; // first callback exception - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$e ) { - $ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT ); - $e = $e ?: $ex; - } ); - // Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB - $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] ); - // Throw any last post-commit callback error - if ( $e instanceof Exception ) { - throw $e; - } - } - - /** - * Rollback changes on all master connections - * @param string $fname Caller name - * @since 1.23 - */ - public function rollbackMasterChanges( $fname = __METHOD__ ) { - $this->trxRoundId = false; - $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' ); - $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] ); - // Run all post-rollback callbacks - $this->forEachLB( function ( LoadBalancer $lb ) { - $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK ); - } ); - } - - /** - * Log query info if multi DB transactions are going to be committed now - */ - private function logIfMultiDbTransaction() { - $callersByDB = []; - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$callersByDB ) { - $masterName = $lb->getServerName( $lb->getWriterIndex() ); - $callers = $lb->pendingMasterChangeCallers(); - if ( $callers ) { - $callersByDB[$masterName] = $callers; - } - } ); - - if ( count( $callersByDB ) >= 2 ) { - $dbs = implode( ', ', array_keys( $callersByDB ) ); - $msg = "Multi-DB transaction [{$dbs}]:\n"; - foreach ( $callersByDB as $db => $callers ) { - $msg .= "$db: " . implode( '; ', $callers ) . "\n"; - } - $this->trxLogger->info( $msg ); - } - } - - /** - * Determine if any master connection has pending changes - * @return bool - * @since 1.23 - */ - public function hasMasterChanges() { - $ret = false; - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) { - $ret = $ret || $lb->hasMasterChanges(); - } ); - - return $ret; - } - - /** - * Detemine if any lagged replica DB connection was used - * @return bool - * @since 1.28 - */ - public function laggedReplicaUsed() { - $ret = false; - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) { - $ret = $ret || $lb->laggedReplicaUsed(); - } ); - - return $ret; - } - - /** - * @return bool - * @since 1.27 - * @deprecated Since 1.28; use laggedReplicaUsed() - */ - public function laggedSlaveUsed() { - return $this->laggedReplicaUsed(); - } - - /** - * Determine if any master connection has pending/written changes from this request - * @param float $age How many seconds ago is "recent" [defaults to LB lag wait timeout] - * @return bool - * @since 1.27 - */ - public function hasOrMadeRecentMasterChanges( $age = null ) { - $ret = false; - $this->forEachLB( function ( LoadBalancer $lb ) use ( $age, &$ret ) { - $ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age ); - } ); - return $ret; - } - - /** - * Waits for the replica DBs to catch up to the current master position - * - * Use this when updating very large numbers of rows, as in maintenance scripts, - * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs. - * - * By default this waits on all DB clusters actually used in this request. - * This makes sense when lag being waiting on is caused by the code that does this check. - * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters - * that were not changed since the last wait check. To forcefully wait on a specific cluster - * for a given wiki, use the 'wiki' parameter. To forcefully wait on an "external" cluster, - * use the "cluster" parameter. - * - * Never call this function after a large DB write that is *still* in a transaction. - * It only makes sense to call this after the possible lag inducing changes were committed. - * - * @param array $opts Optional fields that include: - * - wiki : wait on the load balancer DBs that handles the given wiki - * - cluster : wait on the given external load balancer DBs - * - timeout : Max wait time. Default: ~60 seconds - * - ifWritesSince: Only wait if writes were done since this UNIX timestamp - * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster - * @since 1.27 - */ - public function waitForReplication( array $opts = [] ) { - $opts += [ - 'wiki' => false, - 'cluster' => false, - 'timeout' => 60, - 'ifWritesSince' => null - ]; - - // Figure out which clusters need to be checked - /** @var LoadBalancer[] $lbs */ - $lbs = []; - if ( $opts['cluster'] !== false ) { - $lbs[] = $this->getExternalLB( $opts['cluster'] ); - } elseif ( $opts['wiki'] !== false ) { - $lbs[] = $this->getMainLB( $opts['wiki'] ); - } else { - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) { - $lbs[] = $lb; - } ); - if ( !$lbs ) { - return; // nothing actually used - } - } - - // Get all the master positions of applicable DBs right now. - // This can be faster since waiting on one cluster reduces the - // time needed to wait on the next clusters. - $masterPositions = array_fill( 0, count( $lbs ), false ); - foreach ( $lbs as $i => $lb ) { - if ( $lb->getServerCount() <= 1 ) { - // Bug 27975 - Don't try to wait for replica DBs if there are none - // Prevents permission error when getting master position - continue; - } elseif ( $opts['ifWritesSince'] - && $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince'] - ) { - continue; // no writes since the last wait - } - $masterPositions[$i] = $lb->getMasterPos(); - } - - // Run any listener callbacks *after* getting the DB positions. The more - // time spent in the callbacks, the less time is spent in waitForAll(). - foreach ( $this->replicationWaitCallbacks as $callback ) { - $callback(); - } - - $failed = []; - foreach ( $lbs as $i => $lb ) { - if ( $masterPositions[$i] ) { - // The DBMS may not support getMasterPos() or the whole - // load balancer might be fake (e.g. $wgAllDBsAreLocalhost). - if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) { - $failed[] = $lb->getServerName( $lb->getWriterIndex() ); - } - } - } - - if ( $failed ) { - throw new DBReplicationWaitError( - "Could not wait for replica DBs to catch up to " . - implode( ', ', $failed ) - ); - } - } - - /** - * Add a callback to be run in every call to waitForReplication() before waiting - * - * Callbacks must clear any transactions that they start - * - * @param string $name Callback name - * @param callable|null $callback Use null to unset a callback - * @since 1.28 - */ - public function setWaitForReplicationListener( $name, callable $callback = null ) { - if ( $callback ) { - $this->replicationWaitCallbacks[$name] = $callback; - } else { - unset( $this->replicationWaitCallbacks[$name] ); - } - } - - /** - * Get a token asserting that no transaction writes are active - * - * @param string $fname Caller name (e.g. __METHOD__) - * @return mixed A value to pass to commitAndWaitForReplication() - * @since 1.28 - */ - public function getEmptyTransactionTicket( $fname ) { - if ( $this->hasMasterChanges() ) { - $this->trxLogger->error( __METHOD__ . ": $fname does not have outer scope." ); - return null; - } - - return $this->ticket; - } - - /** - * Convenience method for safely running commitMasterChanges()/waitForReplication() - * - * This will commit and wait unless $ticket indicates it is unsafe to do so - * - * @param string $fname Caller name (e.g. __METHOD__) - * @param mixed $ticket Result of getEmptyTransactionTicket() - * @param array $opts Options to waitForReplication() - * @throws DBReplicationWaitError - * @since 1.28 - */ - public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) { - if ( $ticket !== $this->ticket ) { - $logger = LoggerFactory::getInstance( 'DBPerformance' ); - $logger->error( __METHOD__ . ": cannot commit; $fname does not have outer scope." ); - return; - } - - // The transaction owner and any caller with the empty transaction ticket can commit - // so that getEmptyTransactionTicket() callers don't risk seeing DBTransactionError. - if ( $this->trxRoundId !== false && $fname !== $this->trxRoundId ) { - $this->trxLogger->info( "$fname: committing on behalf of {$this->trxRoundId}." ); - $fnameEffective = $this->trxRoundId; - } else { - $fnameEffective = $fname; - } - - $this->commitMasterChanges( $fnameEffective ); - $this->waitForReplication( $opts ); - // If a nested caller committed on behalf of $fname, start another empty $fname - // transaction, leaving the caller with the same empty transaction state as before. - if ( $fnameEffective !== $fname ) { - $this->beginMasterChanges( $fnameEffective ); - } - } - - /** - * @param string $dbName DB master name (e.g. "db1052") - * @return float|bool UNIX timestamp when client last touched the DB or false if not recent - * @since 1.28 - */ - public function getChronologyProtectorTouched( $dbName ) { - return $this->chronProt->getTouched( $dbName ); - } - - /** - * Disable the ChronologyProtector for all load balancers - * - * This can be called at the start of special API entry points - * - * @since 1.27 - */ - public function disableChronologyProtection() { - $this->chronProt->setEnabled( false ); - } - - /** - * @return ChronologyProtector - */ - protected function newChronologyProtector() { - $request = RequestContext::getMain()->getRequest(); - $chronProt = new ChronologyProtector( - ObjectCache::getMainStashInstance(), - [ - 'ip' => $request->getIP(), - 'agent' => $request->getHeader( 'User-Agent' ), - ], - $request->getFloat( 'cpPosTime', $request->getCookie( 'cpPosTime', '' ) ) - ); - $chronProt->setLogger( $this->replLogger ); - if ( PHP_SAPI === 'cli' ) { - $chronProt->setEnabled( false ); - } elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) { - // Request opted out of using position wait logic. This is useful for requests - // done by the job queue or background ETL that do not have a meaningful session. - $chronProt->setWaitEnabled( false ); - } - - return $chronProt; - } - - /** - * Get and record all of the staged DB positions into persistent memory storage - * - * @param ChronologyProtector $cp - * @param callable|null $workCallback Work to do instead of waiting on syncing positions - * @param string $mode One of (sync, async); whether to wait on remote datacenters - */ - protected function shutdownChronologyProtector( - ChronologyProtector $cp, $workCallback, $mode - ) { - // Record all the master positions needed - $this->forEachLB( function ( LoadBalancer $lb ) use ( $cp ) { - $cp->shutdownLB( $lb ); - } ); - // Write them to the persistent stash. Try to do something useful by running $work - // while ChronologyProtector waits for the stash write to replicate to all DCs. - $unsavedPositions = $cp->shutdown( $workCallback, $mode ); - if ( $unsavedPositions && $workCallback ) { - // Invoke callback in case it did not cache the result yet - $workCallback(); // work now to block for less time in waitForAll() - } - // If the positions failed to write to the stash, at least wait on local datacenter - // replica DBs to catch up before responding. Even if there are several DCs, this increases - // the chance that the user will see their own changes immediately afterwards. As long - // as the sticky DC cookie applies (same domain), this is not even an issue. - $this->forEachLB( function ( LoadBalancer $lb ) use ( $unsavedPositions ) { - $masterName = $lb->getServerName( $lb->getWriterIndex() ); - if ( isset( $unsavedPositions[$masterName] ) ) { - $lb->waitForAll( $unsavedPositions[$masterName] ); - } - } ); - } - - /** - * Base parameters to LoadBalancer::__construct() - * @return array - */ - final protected function baseLoadBalancerParams() { - return [ - 'localDomain' => wfWikiID(), - 'readOnlyReason' => $this->readOnlyReason, - 'srvCache' => $this->srvCache, - 'memCache' => $this->memCache, - 'wanCache' => $this->wanCache, - 'trxProfiler' => $this->trxProfiler, - 'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ), - 'connLogger' => LoggerFactory::getInstance( 'DBConnection' ), - 'replLogger' => LoggerFactory::getInstance( 'DBReplication' ), - 'errorLogger' => [ MWExceptionHandler::class, 'logException' ], - 'hostname' => wfHostname() - ]; - } - - /** - * @param LoadBalancer $lb - */ - protected function initLoadBalancer( LoadBalancer $lb ) { - if ( $this->trxRoundId !== false ) { - $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX - } - } - - /** - * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed - * - * Note that unlike cookies, this works accross domains - * - * @param string $url - * @param float $time UNIX timestamp just before shutdown() was called - * @return string - * @since 1.28 - */ - public function appendPreShutdownTimeAsQuery( $url, $time ) { - $usedCluster = 0; - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$usedCluster ) { - $usedCluster |= ( $lb->getServerCount() > 1 ); - } ); - - if ( !$usedCluster ) { - return $url; // no master/replica clusters touched - } - - return wfAppendQuery( $url, [ 'cpPosTime' => $time ] ); - } - - /** - * Close all open database connections on all open load balancers. - * @since 1.28 - */ - public function closeAll() { - $this->forEachLBCallMethod( 'closeAll', [] ); - } -} diff --git a/includes/db/loadbalancer/LBFactoryMW.php b/includes/db/loadbalancer/LBFactoryMW.php new file mode 100644 index 0000000000..87fd81bb6c --- /dev/null +++ b/includes/db/loadbalancer/LBFactoryMW.php @@ -0,0 +1,149 @@ + wfWikiID(), + 'hostname' => wfHostname(), + 'trxProfiler' => Profiler::instance()->getTransactionProfiler(), + 'replLogger' => LoggerFactory::getInstance( 'DBReplication' ), + 'queryLogger' => LoggerFactory::getInstance( 'wfLogDBError' ), + 'connLogger' => LoggerFactory::getInstance( 'wfLogDBError' ), + 'perfLogger' => LoggerFactory::getInstance( 'DBPerformance' ), + 'errorLogger' => [ MWExceptionHandler::class, 'logException' ] + ]; + // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804) + $sCache = ObjectCache::getLocalServerInstance(); + if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) { + $defaults['srvCache'] = $sCache; + } + $cCache = ObjectCache::getLocalClusterInstance(); + if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) { + $defaults['memCache'] = $cCache; + } + $wCache = ObjectCache::getMainWANInstance(); + if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) { + $defaults['wanCache'] = $wCache; + } + + parent::__construct( $conf + $defaults ); + } + + /** + * Returns the LBFactory class to use and the load balancer configuration. + * + * @todo instead of this, use a ServiceContainer for managing the different implementations. + * + * @param array $config (e.g. $wgLBFactoryConf) + * @return string Class name + */ + public static function getLBFactoryClass( array $config ) { + // For configuration backward compatibility after removing + // underscores from class names in MediaWiki 1.23. + $bcClasses = [ + 'LBFactory_Simple' => 'LBFactorySimple', + 'LBFactory_Single' => 'LBFactorySingle', + 'LBFactory_Multi' => 'LBFactoryMulti' + ]; + + $class = $config['class']; + + if ( isset( $bcClasses[$class] ) ) { + $class = $bcClasses[$class]; + wfDeprecated( + '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details', + '1.23' + ); + } + + return $class; + } + + /** + * @return bool + * @since 1.27 + * @deprecated Since 1.28; use laggedReplicaUsed() + */ + public function laggedSlaveUsed() { + return $this->laggedReplicaUsed(); + } + + protected function newChronologyProtector() { + $request = RequestContext::getMain()->getRequest(); + $chronProt = new ChronologyProtector( + ObjectCache::getMainStashInstance(), + [ + 'ip' => $request->getIP(), + 'agent' => $request->getHeader( 'User-Agent' ), + ], + $request->getFloat( 'cpPosTime', $request->getCookie( 'cpPosTime', '' ) ) + ); + if ( PHP_SAPI === 'cli' ) { + $chronProt->setEnabled( false ); + } elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) { + // Request opted out of using position wait logic. This is useful for requests + // done by the job queue or background ETL that do not have a meaningful session. + $chronProt->setWaitEnabled( false ); + } + + return $chronProt; + } + + /** + * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed + * + * Note that unlike cookies, this works accross domains + * + * @param string $url + * @param float $time UNIX timestamp just before shutdown() was called + * @return string + * @since 1.28 + */ + public function appendPreShutdownTimeAsQuery( $url, $time ) { + $usedCluster = 0; + $this->forEachLB( function ( LoadBalancer $lb ) use ( &$usedCluster ) { + $usedCluster |= ( $lb->getServerCount() > 1 ); + } ); + + if ( !$usedCluster ) { + return $url; // no master/replica clusters touched + } + + return wfAppendQuery( $url, [ 'cpPosTime' => $time ] ); + } +} diff --git a/includes/db/loadbalancer/LBFactoryMulti.php b/includes/db/loadbalancer/LBFactoryMulti.php index bbee3b8783..95bc8f418b 100644 --- a/includes/db/loadbalancer/LBFactoryMulti.php +++ b/includes/db/loadbalancer/LBFactoryMulti.php @@ -83,7 +83,7 @@ * * @ingroup Database */ -class LBFactoryMulti extends LBFactory { +class LBFactoryMulti extends LBFactoryMW { /** @var array A map of database names to section names */ private $sectionsByDB; diff --git a/includes/db/loadbalancer/LBFactorySimple.php b/includes/db/loadbalancer/LBFactorySimple.php index 908453caa5..9ccb997d6d 100644 --- a/includes/db/loadbalancer/LBFactorySimple.php +++ b/includes/db/loadbalancer/LBFactorySimple.php @@ -24,7 +24,7 @@ /** * A simple single-master LBFactory that gets its configuration from the b/c globals */ -class LBFactorySimple extends LBFactory { +class LBFactorySimple extends LBFactoryMW { /** @var LoadBalancer */ private $mainLB; /** @var LoadBalancer[] */ diff --git a/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php b/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php index 09b820ba2b..b102f0fc04 100644 --- a/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php +++ b/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php @@ -96,17 +96,17 @@ class ChronologyProtector implements LoggerAwareInterface{ } /** - * Initialise a LoadBalancer to give it appropriate chronology protection. + * Initialise a ILoadBalancer to give it appropriate chronology protection. * * If the stash has a previous master position recorded, this will try to * make sure that the next query to a replica DB of that master will see changes up * to that position by delaying execution. The delay may timeout and allow stale * data if no non-lagged replica DBs are available. * - * @param LoadBalancer $lb + * @param ILoadBalancer $lb * @return void */ - public function initLB( LoadBalancer $lb ) { + public function initLB( ILoadBalancer $lb ) { if ( !$this->enabled || $lb->getServerCount() <= 1 ) { return; // non-replicated setup or disabled } @@ -122,13 +122,13 @@ class ChronologyProtector implements LoggerAwareInterface{ } /** - * Notify the ChronologyProtector that the LoadBalancer is about to shut + * Notify the ChronologyProtector that the ILoadBalancer is about to shut * down. Saves replication positions. * - * @param LoadBalancer $lb + * @param ILoadBalancer $lb * @return void */ - public function shutdownLB( LoadBalancer $lb ) { + public function shutdownLB( ILoadBalancer $lb ) { if ( !$this->enabled ) { return; // not enabled } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) { diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php new file mode 100644 index 0000000000..feae4bd05b --- /dev/null +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -0,0 +1,644 @@ +domain = isset( $conf['domain'] ) ? $conf['domain'] : ''; + if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) { + $this->readOnlyReason = $conf['readOnlyReason']; + } + + $this->srvCache = isset( $conf['srvCache'] ) ? $conf['srvCache'] : new EmptyBagOStuff(); + $this->memCache = isset( $conf['memCache'] ) ? $conf['memCache'] : new EmptyBagOStuff(); + $this->wanCache = isset( $conf['wanCache'] ) + ? $conf['wanCache'] + : WANObjectCache::newEmpty(); + + foreach ( self::$loggerFields as $key ) { + $this->$key = isset( $conf[$key] ) ? $conf[$key] : new \Psr\Log\NullLogger(); + } + $this->errorLogger = isset( $conf['errorLogger'] ) + ? $conf['errorLogger'] + : function ( Exception $e ) { + trigger_error( E_WARNING, $e->getMessage() ); + }; + $this->hostname = isset( $conf['hostname'] ) + ? $conf['hostname'] + : gethostname(); + + $this->chronProt = isset( $conf['chronProt'] ) + ? $conf['chronProt'] + : $this->newChronologyProtector(); + $this->trxProfiler = isset( $conf['trxProfiler'] ) + ? $conf['trxProfiler'] + : new TransactionProfiler(); + + $this->ticket = mt_rand(); + } + + /** + * Disables all load balancers. All connections are closed, and any attempt to + * open a new connection will result in a DBAccessError. + * @see LoadBalancer::disable() + */ + public function destroy() { + $this->shutdown( self::SHUTDOWN_NO_CHRONPROT ); + $this->forEachLBCallMethod( 'disable' ); + } + + /** + * Create a new load balancer object. The resulting object will be untracked, + * not chronology-protected, and the caller is responsible for cleaning it up. + * + * @param bool|string $domain Wiki ID, or false for the current wiki + * @return ILoadBalancer + */ + abstract public function newMainLB( $domain = false ); + + /** + * Get a cached (tracked) load balancer object. + * + * @param bool|string $domain Wiki ID, or false for the current wiki + * @return ILoadBalancer + */ + abstract public function getMainLB( $domain = false ); + + /** + * Create a new load balancer for external storage. The resulting object will be + * untracked, not chronology-protected, and the caller is responsible for + * cleaning it up. + * + * @param string $cluster External storage cluster, or false for core + * @param bool|string $domain Wiki ID, or false for the current wiki + * @return ILoadBalancer + */ + abstract protected function newExternalLB( $cluster, $domain = false ); + + /** + * Get a cached (tracked) load balancer for external storage + * + * @param string $cluster External storage cluster, or false for core + * @param bool|string $domain Wiki ID, or false for the current wiki + * @return ILoadBalancer + */ + abstract public function getExternalLB( $cluster, $domain = false ); + + /** + * Execute a function for each tracked load balancer + * The callback is called with the load balancer as the first parameter, + * and $params passed as the subsequent parameters. + * + * @param callable $callback + * @param array $params + */ + abstract public function forEachLB( $callback, array $params = [] ); + + /** + * Prepare all tracked load balancers for shutdown + * @param integer $mode One of the class SHUTDOWN_* constants + * @param callable|null $workCallback Work to mask ChronologyProtector writes + */ + public function shutdown( + $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null + ) { + if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) { + $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' ); + } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) { + $this->shutdownChronologyProtector( $this->chronProt, null, 'async' ); + } + + $this->commitMasterChanges( __METHOD__ ); // sanity + } + + /** + * Call a method of each tracked load balancer + * + * @param string $methodName + * @param array $args + */ + protected function forEachLBCallMethod( $methodName, array $args = [] ) { + $this->forEachLB( + function ( ILoadBalancer $loadBalancer, $methodName, array $args ) { + call_user_func_array( [ $loadBalancer, $methodName ], $args ); + }, + [ $methodName, $args ] + ); + } + + /** + * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot + * + * @param string $fname Caller name + * @since 1.28 + */ + public function flushReplicaSnapshots( $fname = __METHOD__ ) { + $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] ); + } + + /** + * Commit on all connections. Done for two reasons: + * 1. To commit changes to the masters. + * 2. To release the snapshot on all connections, master and replica DB. + * @param string $fname Caller name + * @param array $options Options map: + * - maxWriteDuration: abort if more than this much time was spent in write queries + */ + public function commitAll( $fname = __METHOD__, array $options = [] ) { + $this->commitMasterChanges( $fname, $options ); + $this->forEachLBCallMethod( 'commitAll', [ $fname ] ); + } + + /** + * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set) + * + * The DBO_TRX setting will be reverted to the default in each of these methods: + * - commitMasterChanges() + * - rollbackMasterChanges() + * - commitAll() + * + * This allows for custom transaction rounds from any outer transaction scope. + * + * @param string $fname + * @throws DBTransactionError + * @since 1.28 + */ + public function beginMasterChanges( $fname = __METHOD__ ) { + if ( $this->trxRoundId !== false ) { + throw new DBTransactionError( + null, + "$fname: transaction round '{$this->trxRoundId}' already started." + ); + } + $this->trxRoundId = $fname; + // Set DBO_TRX flags on all appropriate DBs + $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] ); + } + + /** + * Commit changes on all master connections + * @param string $fname Caller name + * @param array $options Options map: + * - maxWriteDuration: abort if more than this much time was spent in write queries + * @throws Exception + */ + public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) { + if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) { + throw new DBTransactionError( + null, + "$fname: transaction round '{$this->trxRoundId}' still running." + ); + } + // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure + $this->forEachLBCallMethod( 'finalizeMasterChanges' ); + $this->trxRoundId = false; + // Perform pre-commit checks, aborting on failure + $this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] ); + // Log the DBs and methods involved in multi-DB transactions + $this->logIfMultiDbTransaction(); + // Actually perform the commit on all master DB connections and revert DBO_TRX + $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] ); + // Run all post-commit callbacks + /** @var Exception $e */ + $e = null; // first callback exception + $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) { + $ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT ); + $e = $e ?: $ex; + } ); + // Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB + $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] ); + // Throw any last post-commit callback error + if ( $e instanceof Exception ) { + throw $e; + } + } + + /** + * Rollback changes on all master connections + * @param string $fname Caller name + * @since 1.23 + */ + public function rollbackMasterChanges( $fname = __METHOD__ ) { + $this->trxRoundId = false; + $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' ); + $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] ); + // Run all post-rollback callbacks + $this->forEachLB( function ( ILoadBalancer $lb ) { + $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK ); + } ); + } + + /** + * Log query info if multi DB transactions are going to be committed now + */ + private function logIfMultiDbTransaction() { + $callersByDB = []; + $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$callersByDB ) { + $masterName = $lb->getServerName( $lb->getWriterIndex() ); + $callers = $lb->pendingMasterChangeCallers(); + if ( $callers ) { + $callersByDB[$masterName] = $callers; + } + } ); + + if ( count( $callersByDB ) >= 2 ) { + $dbs = implode( ', ', array_keys( $callersByDB ) ); + $msg = "Multi-DB transaction [{$dbs}]:\n"; + foreach ( $callersByDB as $db => $callers ) { + $msg .= "$db: " . implode( '; ', $callers ) . "\n"; + } + $this->queryLogger->info( $msg ); + } + } + + /** + * Determine if any master connection has pending changes + * @return bool + * @since 1.23 + */ + public function hasMasterChanges() { + $ret = false; + $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) { + $ret = $ret || $lb->hasMasterChanges(); + } ); + + return $ret; + } + + /** + * Detemine if any lagged replica DB connection was used + * @return bool + * @since 1.28 + */ + public function laggedReplicaUsed() { + $ret = false; + $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) { + $ret = $ret || $lb->laggedReplicaUsed(); + } ); + + return $ret; + } + + /** + * Determine if any master connection has pending/written changes from this request + * @param float $age How many seconds ago is "recent" [defaults to LB lag wait timeout] + * @return bool + * @since 1.27 + */ + public function hasOrMadeRecentMasterChanges( $age = null ) { + $ret = false; + $this->forEachLB( function ( ILoadBalancer $lb ) use ( $age, &$ret ) { + $ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age ); + } ); + return $ret; + } + + /** + * Waits for the replica DBs to catch up to the current master position + * + * Use this when updating very large numbers of rows, as in maintenance scripts, + * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs. + * + * By default this waits on all DB clusters actually used in this request. + * This makes sense when lag being waiting on is caused by the code that does this check. + * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters + * that were not changed since the last wait check. To forcefully wait on a specific cluster + * for a given wiki, use the 'wiki' parameter. To forcefully wait on an "external" cluster, + * use the "cluster" parameter. + * + * Never call this function after a large DB write that is *still* in a transaction. + * It only makes sense to call this after the possible lag inducing changes were committed. + * + * @param array $opts Optional fields that include: + * - wiki : wait on the load balancer DBs that handles the given wiki + * - cluster : wait on the given external load balancer DBs + * - timeout : Max wait time. Default: ~60 seconds + * - ifWritesSince: Only wait if writes were done since this UNIX timestamp + * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster + * @since 1.27 + */ + public function waitForReplication( array $opts = [] ) { + $opts += [ + 'wiki' => false, + 'cluster' => false, + 'timeout' => 60, + 'ifWritesSince' => null + ]; + + // Figure out which clusters need to be checked + /** @var ILoadBalancer[] $lbs */ + $lbs = []; + if ( $opts['cluster'] !== false ) { + $lbs[] = $this->getExternalLB( $opts['cluster'] ); + } elseif ( $opts['wiki'] !== false ) { + $lbs[] = $this->getMainLB( $opts['wiki'] ); + } else { + $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) { + $lbs[] = $lb; + } ); + if ( !$lbs ) { + return; // nothing actually used + } + } + + // Get all the master positions of applicable DBs right now. + // This can be faster since waiting on one cluster reduces the + // time needed to wait on the next clusters. + $masterPositions = array_fill( 0, count( $lbs ), false ); + foreach ( $lbs as $i => $lb ) { + if ( $lb->getServerCount() <= 1 ) { + // Bug 27975 - Don't try to wait for replica DBs if there are none + // Prevents permission error when getting master position + continue; + } elseif ( $opts['ifWritesSince'] + && $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince'] + ) { + continue; // no writes since the last wait + } + $masterPositions[$i] = $lb->getMasterPos(); + } + + // Run any listener callbacks *after* getting the DB positions. The more + // time spent in the callbacks, the less time is spent in waitForAll(). + foreach ( $this->replicationWaitCallbacks as $callback ) { + $callback(); + } + + $failed = []; + foreach ( $lbs as $i => $lb ) { + if ( $masterPositions[$i] ) { + // The DBMS may not support getMasterPos() or the whole + // load balancer might be fake (e.g. $wgAllDBsAreLocalhost). + if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) { + $failed[] = $lb->getServerName( $lb->getWriterIndex() ); + } + } + } + + if ( $failed ) { + throw new DBReplicationWaitError( + "Could not wait for replica DBs to catch up to " . + implode( ', ', $failed ) + ); + } + } + + /** + * Add a callback to be run in every call to waitForReplication() before waiting + * + * Callbacks must clear any transactions that they start + * + * @param string $name Callback name + * @param callable|null $callback Use null to unset a callback + * @since 1.28 + */ + public function setWaitForReplicationListener( $name, callable $callback = null ) { + if ( $callback ) { + $this->replicationWaitCallbacks[$name] = $callback; + } else { + unset( $this->replicationWaitCallbacks[$name] ); + } + } + + /** + * Get a token asserting that no transaction writes are active + * + * @param string $fname Caller name (e.g. __METHOD__) + * @return mixed A value to pass to commitAndWaitForReplication() + * @since 1.28 + */ + public function getEmptyTransactionTicket( $fname ) { + if ( $this->hasMasterChanges() ) { + $this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope." ); + return null; + } + + return $this->ticket; + } + + /** + * Convenience method for safely running commitMasterChanges()/waitForReplication() + * + * This will commit and wait unless $ticket indicates it is unsafe to do so + * + * @param string $fname Caller name (e.g. __METHOD__) + * @param mixed $ticket Result of getEmptyTransactionTicket() + * @param array $opts Options to waitForReplication() + * @throws DBReplicationWaitError + * @since 1.28 + */ + public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) { + if ( $ticket !== $this->ticket ) { + $this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope." ); + return; + } + + // The transaction owner and any caller with the empty transaction ticket can commit + // so that getEmptyTransactionTicket() callers don't risk seeing DBTransactionError. + if ( $this->trxRoundId !== false && $fname !== $this->trxRoundId ) { + $this->queryLogger->info( "$fname: committing on behalf of {$this->trxRoundId}." ); + $fnameEffective = $this->trxRoundId; + } else { + $fnameEffective = $fname; + } + + $this->commitMasterChanges( $fnameEffective ); + $this->waitForReplication( $opts ); + // If a nested caller committed on behalf of $fname, start another empty $fname + // transaction, leaving the caller with the same empty transaction state as before. + if ( $fnameEffective !== $fname ) { + $this->beginMasterChanges( $fnameEffective ); + } + } + + /** + * @param string $dbName DB master name (e.g. "db1052") + * @return float|bool UNIX timestamp when client last touched the DB or false if not recent + * @since 1.28 + */ + public function getChronologyProtectorTouched( $dbName ) { + return $this->chronProt->getTouched( $dbName ); + } + + /** + * Disable the ChronologyProtector for all load balancers + * + * This can be called at the start of special API entry points + * + * @since 1.27 + */ + public function disableChronologyProtection() { + $this->chronProt->setEnabled( false ); + } + + /** + * @return ChronologyProtector + */ + protected function newChronologyProtector() { + $chronProt = new ChronologyProtector( + $this->memCache, + [ + 'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '', + 'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '' + ], + isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null + ); + $chronProt->setLogger( $this->replLogger ); + if ( PHP_SAPI === 'cli' ) { + $chronProt->setEnabled( false ); + } + + return $chronProt; + } + + /** + * Get and record all of the staged DB positions into persistent memory storage + * + * @param ChronologyProtector $cp + * @param callable|null $workCallback Work to do instead of waiting on syncing positions + * @param string $mode One of (sync, async); whether to wait on remote datacenters + */ + protected function shutdownChronologyProtector( + ChronologyProtector $cp, $workCallback, $mode + ) { + // Record all the master positions needed + $this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) { + $cp->shutdownLB( $lb ); + } ); + // Write them to the persistent stash. Try to do something useful by running $work + // while ChronologyProtector waits for the stash write to replicate to all DCs. + $unsavedPositions = $cp->shutdown( $workCallback, $mode ); + if ( $unsavedPositions && $workCallback ) { + // Invoke callback in case it did not cache the result yet + $workCallback(); // work now to block for less time in waitForAll() + } + // If the positions failed to write to the stash, at least wait on local datacenter + // replica DBs to catch up before responding. Even if there are several DCs, this increases + // the chance that the user will see their own changes immediately afterwards. As long + // as the sticky DC cookie applies (same domain), this is not even an issue. + $this->forEachLB( function ( ILoadBalancer $lb ) use ( $unsavedPositions ) { + $masterName = $lb->getServerName( $lb->getWriterIndex() ); + if ( isset( $unsavedPositions[$masterName] ) ) { + $lb->waitForAll( $unsavedPositions[$masterName] ); + } + } ); + } + + /** + * Base parameters to LoadBalancer::__construct() + * @return array + */ + final protected function baseLoadBalancerParams() { + return [ + 'localDomain' => $this->domain, + 'readOnlyReason' => $this->readOnlyReason, + 'srvCache' => $this->srvCache, + 'wanCache' => $this->wanCache, + 'trxProfiler' => $this->trxProfiler, + 'queryLogger' => $this->queryLogger, + 'connLogger' => $this->connLogger, + 'replLogger' => $this->replLogger, + 'errorLogger' => $this->errorLogger, + 'hostname' => $this->hostname + ]; + } + + /** + * @param ILoadBalancer $lb + */ + protected function initLoadBalancer( ILoadBalancer $lb ) { + if ( $this->trxRoundId !== false ) { + $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX + } + } + + /** + * Define a new local domain (for testing) + * + * Caller should make sure no local connection are open to the old local domain + * + * @param string $domain + * @since 1.28 + */ + public function setDomainPrefix( $domain ) { + $this->domain = $domain; + } + + /** + * Close all open database connections on all open load balancers. + * @since 1.28 + */ + public function closeAll() { + $this->forEachLBCallMethod( 'closeAll', [] ); + } +} diff --git a/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php b/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php index a5fc85d107..1a40b8f8ea 100644 --- a/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php +++ b/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php @@ -28,11 +28,11 @@ class LoadMonitorNull implements LoadMonitor { public function setLogger( LoggerInterface $logger ) { } - public function scaleLoads( &$loads, $group = false, $wiki = false ) { + public function scaleLoads( &$loads, $group = false, $domain = false ) { } - public function getLagTimes( $serverIndexes, $wiki ) { + public function getLagTimes( $serverIndexes, $domain ) { return array_fill_keys( $serverIndexes, 0 ); } diff --git a/maintenance/Maintenance.php b/maintenance/Maintenance.php index 2216de1c17..1fbca1417c 100644 --- a/maintenance/Maintenance.php +++ b/maintenance/Maintenance.php @@ -1091,7 +1091,7 @@ abstract class Maintenance { $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser; $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword; } - LBFactory::destroyInstance(); + MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy(); } // Per-script profiling; useful for debugging diff --git a/tests/phpunit/includes/db/LBFactoryTest.php b/tests/phpunit/includes/db/LBFactoryTest.php index 364a6c2934..5affa9cd92 100644 --- a/tests/phpunit/includes/db/LBFactoryTest.php +++ b/tests/phpunit/includes/db/LBFactoryTest.php @@ -43,7 +43,7 @@ class LBFactoryTest extends MediaWikiTestCase { ]; $this->hideDeprecated( '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details' ); - $result = LBFactory::getLBFactoryClass( $config ); + $result = LBFactoryMW::getLBFactoryClass( $config ); $this->assertEquals( $expected, $result ); }