X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fdb%2Floadbalancer%2FLBFactory.php;h=3120889756cbe6ff42227cf85904115a8cd9a5ac;hb=a3dacac90f19f1d0cb613919ecc1b2947428a19b;hp=dfa4c292fa1becf667ff930fe25570f794576f5a;hpb=5ce43a98613303fac6bad6e6c7663c478f726cd2;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/db/loadbalancer/LBFactory.php b/includes/db/loadbalancer/LBFactory.php index dfa4c292fa..3120889756 100644 --- a/includes/db/loadbalancer/LBFactory.php +++ b/includes/db/loadbalancer/LBFactory.php @@ -44,10 +44,16 @@ abstract class LBFactory implements DestructibleService { /** @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 = 1; // don't save ChronologyProtector positions (for async code) + 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 /** * Construct a factory based on a configuration array (typically from $wgLBFactoryConf) @@ -83,7 +89,7 @@ abstract class LBFactory implements DestructibleService { * @see LoadBalancer::disable() */ public function destroy() { - $this->shutdown(); + $this->shutdown( self::SHUTDOWN_NO_CHRONPROT ); $this->forEachLBCallMethod( 'disable' ); } @@ -181,7 +187,7 @@ abstract class LBFactory implements DestructibleService { * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ - abstract public function &getExternalLB( $cluster, $wiki = false ); + abstract public function getExternalLB( $cluster, $wiki = false ); /** * Execute a function for each tracked load balancer @@ -195,12 +201,18 @@ abstract class LBFactory implements DestructibleService { /** * Prepare all tracked load balancers for shutdown - * @param integer $flags Supports SHUTDOWN_* flags + * @param integer $mode One of the class SHUTDOWN_* constants + * @param callable|null $workCallback Work to mask ChronologyProtector writes */ - public function shutdown( $flags = 0 ) { - if ( !( $flags & self::SHUTDOWN_NO_CHRONPROT ) ) { - $this->shutdownChronologyProtector( $this->chronProt ); + 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 } @@ -220,25 +232,19 @@ abstract class LBFactory implements DestructibleService { } /** - * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set) + * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot * - * 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 + * @param string $fname Caller name * @since 1.28 */ - public function beginMasterChanges( $fname = __METHOD__ ) { - $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] ); + 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 slave. + * 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 @@ -248,6 +254,32 @@ abstract class LBFactory implements DestructibleService { $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 @@ -256,19 +288,26 @@ abstract class LBFactory implements DestructibleService { * @throws Exception */ public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) { - // Perform all pre-commit callbacks, aborting on failure - $this->forEachLBCallMethod( 'runMasterPreCommitCallbacks' ); - // Perform all pre-commit checks, aborting on failure + 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 + // 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->runMasterPostCommitCallbacks(); + $ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT ); $e = $e ?: $ex; } ); // Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB @@ -285,7 +324,13 @@ abstract class LBFactory implements DestructibleService { * @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 ); + } ); } /** @@ -326,37 +371,47 @@ abstract class LBFactory implements DestructibleService { } /** - * Detemine if any lagged slave connection was used - * @since 1.27 + * Detemine if any lagged replica DB connection was used * @return bool + * @since 1.28 */ - public function laggedSlaveUsed() { + public function laggedReplicaUsed() { $ret = false; $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) { - $ret = $ret || $lb->laggedSlaveUsed(); + $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() { + public function hasOrMadeRecentMasterChanges( $age = null ) { $ret = false; - $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) { - $ret = $ret || $lb->hasOrMadeRecentMasterChanges(); + $this->forEachLB( function ( LoadBalancer $lb ) use ( $age, &$ret ) { + $ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age ); } ); return $ret; } /** - * Waits for the slave DBs to catch up to the current master position + * 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 slaves. + * 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. @@ -384,6 +439,10 @@ abstract class LBFactory implements DestructibleService { 'ifWritesSince' => null ]; + foreach ( $this->replicationWaitCallbacks as $callback ) { + $callback(); + } + // Figure out which clusters need to be checked /** @var LoadBalancer[] $lbs */ $lbs = []; @@ -406,7 +465,7 @@ abstract class LBFactory implements DestructibleService { $masterPositions = array_fill( 0, count( $lbs ), false ); foreach ( $lbs as $i => $lb ) { if ( $lb->getServerCount() <= 1 ) { - // Bug 27975 - Don't try to wait for slaves if there are none + // 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'] @@ -430,12 +489,29 @@ abstract class LBFactory implements DestructibleService { if ( $failed ) { throw new DBReplicationWaitError( - "Could not wait for slaves to catch up to " . + "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 * @@ -458,7 +534,7 @@ abstract class LBFactory implements DestructibleService { * 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 getOuterTransactionScopeTicket() + * @param mixed $ticket Result of getEmptyTransactionTicket() * @param array $opts Options to waitForReplication() * @throws DBReplicationWaitError * @since 1.28 @@ -470,8 +546,31 @@ abstract class LBFactory implements DestructibleService { return; } - $this->commitMasterChanges( $fname ); + // 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 ); } /** @@ -494,8 +593,9 @@ abstract class LBFactory implements DestructibleService { ObjectCache::getMainStashInstance(), [ 'ip' => $request->getIP(), - 'agent' => $request->getHeader( 'User-Agent' ) - ] + 'agent' => $request->getHeader( 'User-Agent' ), + ], + $request->getFloat( 'cpPosTime', null ) ); if ( PHP_SAPI === 'cli' ) { $chronProt->setEnabled( false ); @@ -509,17 +609,28 @@ abstract class LBFactory implements DestructibleService { } /** + * 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 ) { - // Get all the master positions needed + 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 stash - $unsavedPositions = $cp->shutdown(); + // 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 - // slaves to catch up before responding. Even if there are several DCs, this increases + // 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 ) { @@ -530,6 +641,38 @@ abstract class LBFactory implements DestructibleService { } ); } + /** + * @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 @@ -537,5 +680,4 @@ abstract class LBFactory implements DestructibleService { public function closeAll() { $this->forEachLBCallMethod( 'closeAll', [] ); } - }