Aaron Schulz has uploaded a new change for review. https://gerrit.wikimedia.org/r/310536
Change subject: Rename LBFactory => LBFactoryMW and make LBFactory in /libs ...................................................................... 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 --- M autoload.php M includes/ServiceWiring.php M includes/db/ChronologyProtector.php M includes/db/loadbalancer/LBFactory.php M includes/db/loadbalancer/LBFactoryFake.php M includes/db/loadbalancer/LBFactoryMulti.php M includes/db/loadbalancer/LBFactorySimple.php M includes/db/loadbalancer/LBFactorySingle.php A includes/libs/rdbms/lbfactory/LBFactory.php 9 files changed, 645 insertions(+), 549 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/36/310536/1 diff --git a/autoload.php b/autoload.php index 4b5d9e8..d2c3f27 100644 --- a/autoload.php +++ b/autoload.php @@ -654,6 +654,7 @@ 'KuConverter' => __DIR__ . '/languages/classes/LanguageKu.php', 'LBFactory' => __DIR__ . '/includes/db/loadbalancer/LBFactory.php', 'LBFactoryFake' => __DIR__ . '/includes/db/loadbalancer/LBFactoryFake.php', + 'LBFactoryMW' => __DIR__ . '/includes/libs/rdbms/lbfactory/LBFactory.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 8734bd6..4ab412e 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -45,7 +45,7 @@ '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/ChronologyProtector.php b/includes/db/ChronologyProtector.php index 4d03bc6..f37006c 100644 --- a/includes/db/ChronologyProtector.php +++ b/includes/db/ChronologyProtector.php @@ -96,17 +96,17 @@ } /** - * 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 @@ } /** - * 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 ) ) { @@ -265,10 +265,11 @@ if ( $result == $loop::CONDITION_REACHED ) { $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)"; + $this->logger->info( $msg ); } else { $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)"; + $this->logger->warning( $msg ); } - wfDebugLog( 'replication', $msg ); } else { $data = $this->store->get( $this->key ); } diff --git a/includes/db/loadbalancer/LBFactory.php b/includes/db/loadbalancer/LBFactory.php index 5115fbe..04c1764 100644 --- a/includes/db/loadbalancer/LBFactory.php +++ b/includes/db/loadbalancer/LBFactory.php @@ -21,7 +21,6 @@ * @ingroup Database */ -use Psr\Log\LoggerInterface; use MediaWiki\MediaWikiServices; use MediaWiki\Services\DestructibleService; use MediaWiki\Logger\LoggerFactory; @@ -30,35 +29,8 @@ * An interface for generating database load balancers * @ingroup Database */ -abstract class LBFactory implements DestructibleService { - /** @var ChronologyProtector */ - protected $chronProt; - /** @var TransactionProfiler */ - protected $trxProfiler; - /** @var LoggerInterface */ - protected $trxLogger; - /** @var LoggerInterface */ - protected $replLogger; - /** @var BagOStuff */ - protected $srvCache; - /** @var BagOStuff */ - protected $memCache; - /** @var WANObjectCache */ - protected $wanCache; - - /** @var mixed */ - protected $ticket; - /** @var string|bool String if a requested DBO_TRX transaction round is active */ - protected $trxRoundId = false; - /** @var string|bool Reason all LBs are read-only or false if not */ - protected $readOnlyReason = false; - /** @var callable[] */ - protected $replicationWaitCallbacks = []; - - const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all - const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs - const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs - +abstract class LBFactoryMW extends LBFactory implements DestructibleService { + /** @noinspection PhpMissingParentConstructorInspection */ /** * Construct a factory based on a configuration array (typically from $wgLBFactoryConf) * @param array $conf @@ -95,18 +67,10 @@ } /** - * 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 + * + * @deprecated since 1.28, Use MediaWikiServices::disableStorageBackend() */ public static function disableBackend() { MediaWikiServices::disableStorageBackend(); @@ -164,238 +128,6 @@ } /** - * 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() @@ -404,202 +136,6 @@ 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( @@ -619,67 +155,6 @@ } 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' ] - ]; - } - - /** - * @param LoadBalancer $lb - */ - protected function initLoadBalancer( LoadBalancer $lb ) { - if ( $this->trxRoundId !== false ) { - $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX - } } /** @@ -703,13 +178,5 @@ } 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/LBFactoryFake.php b/includes/db/loadbalancer/LBFactoryFake.php index 5cd1d4b..b97c19b 100644 --- a/includes/db/loadbalancer/LBFactoryFake.php +++ b/includes/db/loadbalancer/LBFactoryFake.php @@ -27,7 +27,7 @@ * Call LBFactory::disableBackend() to start using this, and * LBFactory::enableBackend() to return to normal behavior */ -class LBFactoryFake extends LBFactory { +class LBFactoryFake extends LBFactoryMW { public function newMainLB( $wiki = false ) { throw new DBAccessError; } diff --git a/includes/db/loadbalancer/LBFactoryMulti.php b/includes/db/loadbalancer/LBFactoryMulti.php index dd7737b..fc4a73d 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 d8590b7..9f6d847 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/db/loadbalancer/LBFactorySingle.php b/includes/db/loadbalancer/LBFactorySingle.php index de82a1f..77e41e3 100644 --- a/includes/db/loadbalancer/LBFactorySingle.php +++ b/includes/db/loadbalancer/LBFactorySingle.php @@ -24,7 +24,7 @@ /** * An LBFactory class that always returns a single database object. */ -class LBFactorySingle extends LBFactory { +class LBFactorySingle extends LBFactoryMW { /** @var LoadBalancerSingle */ private $lb; diff --git a/includes/libs/rdbms/lbfactory/LBFactory.php b/includes/libs/rdbms/lbfactory/LBFactory.php new file mode 100644 index 0000000..c98670d --- /dev/null +++ b/includes/libs/rdbms/lbfactory/LBFactory.php @@ -0,0 +1,627 @@ +<?php +/** + * Generator and manager of database load balancing objects + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Database + */ + +use Psr\Log\LoggerInterface; + +/** + * An interface for generating database load balancers + * @ingroup Database + */ +abstract class LBFactory { + /** @var ChronologyProtector */ + protected $chronProt; + /** @var TransactionProfiler */ + protected $trxProfiler; + /** @var LoggerInterface */ + protected $trxLogger; + /** @var LoggerInterface */ + protected $replLogger; + /** @var LoggerInterface */ + protected $connLogger; + /** @var LoggerInterface */ + protected $queryLogger; + /** @var LoggerInterface */ + protected $perfLogger; + /** @var callable Error logger */ + protected $errorLogger; + /** @var BagOStuff */ + protected $srvCache; + /** @var BagOStuff */ + protected $memCache; + /** @var WANObjectCache */ + protected $wanCache; + + /** @var string Local domain */ + protected $domain; + /** @var mixed */ + protected $ticket; + /** @var string|bool String if a requested DBO_TRX transaction round is active */ + protected $trxRoundId = false; + /** @var string|bool Reason all LBs are read-only or false if not */ + protected $readOnlyReason = false; + /** @var callable[] */ + protected $replicationWaitCallbacks = []; + + const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all + const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs + const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs + + private static $loggerFields = + [ 'trxLogger', 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ]; + + /** + * @TODO: document base params here + * @param array $conf + */ + public function __construct( array $conf ) { + $this->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->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->trxLogger->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->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 ) { + $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->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() { + $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 + ); + 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 + ]; + } + + /** + * @param ILoadBalancer $lb + */ + protected function initLoadBalancer( ILoadBalancer $lb ) { + if ( $this->trxRoundId !== false ) { + $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX + } + } + + /** + * Close all open database connections on all open load balancers. + * @since 1.28 + */ + public function closeAll() { + $this->forEachLBCallMethod( 'closeAll', [] ); + } +} -- To view, visit https://gerrit.wikimedia.org/r/310536 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I325f59b7467ab9c2137731d1ce69816f5a020f03 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Aaron Schulz <asch...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits