Support masking the WRITE_SYNC latency from ChronologyProtector
[lhc/web/wiklou.git] / includes / db / loadbalancer / LBFactory.php
index dfa4c29..3120889 100644 (file)
@@ -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', [] );
        }
-
 }