Merge "parserTests: Do not check for DjVu support"
[lhc/web/wiklou.git] / includes / libs / rdbms / lbfactory / LBFactory.php
index d75ba93..f5d57c4 100644 (file)
@@ -27,7 +27,7 @@ use Psr\Log\LoggerInterface;
  * An interface for generating database load balancers
  * @ingroup Database
  */
-abstract class LBFactory {
+abstract class LBFactory implements ILBFactory {
        /** @var ChronologyProtector */
        protected $chronProt;
        /** @var object|string Class name or object With profileIn/profileOut methods */
@@ -72,35 +72,9 @@ abstract class LBFactory {
        /** @var string Agent name for query profiling */
        protected $agent;
 
-       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 =
                [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
 
-       /**
-        * Construct a manager of ILoadBalancer objects
-        *
-        * Sub-classes will extend the required keys in $conf with additional parameters
-        *
-        * @param $conf $params Array with keys:
-        *  - localDomain: A DatabaseDomain or domain ID string.
-        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
-        *  - srvCache : BagOStuff object for server cache [optional]
-        *  - memCache : BagOStuff object for cluster memory cache [optional]
-        *  - wanCache : WANObjectCache object [optional]
-        *  - hostname : The name of the current server [optional]
-        *  - cliMode: Whether the execution context is a CLI script. [optional]
-        *  - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
-        *  - trxProfiler: TransactionProfiler instance. [optional]
-        *  - replLogger: PSR-3 logger instance. [optional]
-        *  - connLogger: PSR-3 logger instance. [optional]
-        *  - queryLogger: PSR-3 logger instance. [optional]
-        *  - perfLogger: PSR-3 logger instance. [optional]
-        *  - errorLogger : Callback that takes an Exception and logs it. [optional]
-        * @throws InvalidArgumentException
-        */
        public function __construct( array $conf ) {
                $this->localDomain = isset( $conf['localDomain'] )
                        ? DatabaseDomain::newFromId( $conf['localDomain'] )
@@ -143,81 +117,54 @@ abstract class LBFactory {
                $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 ILoadBalancer::disable()
-        */
        public function destroy() {
                $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
                $this->forEachLBCallMethod( 'disable' );
        }
 
+       public function shutdown(
+               $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
+       ) {
+               $chronProt = $this->getChronologyProtector();
+               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
+                       $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
+               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
+                       $this->shutdownChronologyProtector( $chronProt, null, 'async' );
+               }
+
+               $this->commitMasterChanges( __METHOD__ ); // sanity
+       }
+
        /**
-        * 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 Domain ID, or false for the current domain
-        * @return ILoadBalancer
+        * @see ILBFactory::newMainLB()
+        * @param bool $domain
+        * @return LoadBalancer
         */
        abstract public function newMainLB( $domain = false );
 
        /**
-        * Get a cached (tracked) load balancer object.
-        *
-        * @param bool|string $domain Domain ID, or false for the current domain
-        * @return ILoadBalancer
+        * @see ILBFactory::getMainLB()
+        * @param bool $domain
+        * @return mixed
         */
        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 Domain ID, or false for the current domain
-        * @return ILoadBalancer
+        * @see ILBFactory::newExternalLB()
+        * @param string $cluster
+        * @param bool $domain
+        * @return LoadBalancer
         */
-       abstract protected function newExternalLB( $cluster, $domain = false );
+       abstract public 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 Domain ID, or false for the current domain
-        * @return ILoadBalancer
+        * @see ILBFactory::getExternalLB()
+        * @param string $cluster
+        * @param bool $domain
+        * @return mixed
         */
        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
-       ) {
-               $chronProt = $this->getChronologyProtector();
-               if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
-                       $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
-               } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
-                       $this->shutdownChronologyProtector( $chronProt, null, 'async' );
-               }
-
-               $this->commitMasterChanges( __METHOD__ ); // sanity
-       }
-
        /**
         * Call a method of each tracked load balancer
         *
@@ -233,43 +180,15 @@ abstract class LBFactory {
                );
        }
 
-       /**
-        * 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(
@@ -282,13 +201,6 @@ abstract class LBFactory {
                $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(
@@ -296,6 +208,8 @@ abstract class LBFactory {
                                "$fname: transaction round '{$this->trxRoundId}' still running."
                        );
                }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $scope = $this->getScopedPHPBehaviorForCommit(); // try to ignore client aborts
                // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
                $this->forEachLBCallMethod( 'finalizeMasterChanges' );
                $this->trxRoundId = false;
@@ -320,11 +234,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * 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' );
@@ -358,11 +267,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * 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 ) {
@@ -372,11 +276,6 @@ abstract class LBFactory {
                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 ) {
@@ -386,12 +285,6 @@ abstract class LBFactory {
                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 ) {
@@ -400,30 +293,6 @@ abstract class LBFactory {
                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 domain, use the 'domain' 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:
-        *   - domain : wait on the load balancer DBs that handles the given domain ID
-        *   - 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 += [
                        'domain' => false,
@@ -493,15 +362,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * 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;
@@ -510,36 +370,22 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * 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." );
+                       $this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
+                               ( new RuntimeException() )->getTraceAsString() );
+
                        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." );
+                       $this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
+                               ( new RuntimeException() )->getTraceAsString() );
+
                        return;
                }
 
@@ -561,22 +407,10 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * @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->getChronologyProtector()->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->getChronologyProtector()->setEnabled( false );
        }
@@ -677,12 +511,6 @@ abstract class LBFactory {
                }
        }
 
-       /**
-        * Set a new table prefix for the existing local domain ID for testing
-        *
-        * @param string $prefix
-        * @since 1.28
-        */
        public function setDomainPrefix( $prefix ) {
                $this->localDomain = new DatabaseDomain(
                        $this->localDomain->getDatabase(),
@@ -695,32 +523,14 @@ abstract class LBFactory {
                } );
        }
 
-       /**
-        * Close all open database connections on all open load balancers.
-        * @since 1.28
-        */
        public function closeAll() {
                $this->forEachLBCallMethod( 'closeAll', [] );
        }
 
-       /**
-        * @param string $agent Agent name for query profiling
-        * @since 1.28
-        */
        public function setAgentName( $agent ) {
                $this->agent = $agent;
        }
 
-       /**
-        * 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 ( ILoadBalancer $lb ) use ( &$usedCluster ) {
@@ -734,17 +544,27 @@ abstract class LBFactory {
                return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
        }
 
-       /**
-        * @param array $info Map of fields, including:
-        *   - IPAddress : IP address
-        *   - UserAgent : User-Agent HTTP header
-        *   - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
-        * @since 1.28
-        */
        public function setRequestInfo( array $info ) {
                $this->requestInfo = $info + $this->requestInfo;
        }
 
+       /**
+        * Make PHP ignore user aborts/disconnects until the returned
+        * value leaves scope. This returns null and does nothing in CLI mode.
+        *
+        * @return ScopedCallback|null
+        */
+       final protected function getScopedPHPBehaviorForCommit() {
+               if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+                       $old = ignore_user_abort( true ); // avoid half-finished operations
+                       return new ScopedCallback( function () use ( $old ) {
+                               ignore_user_abort( $old );
+                       } );
+               }
+
+               return null;
+       }
+
        function __destruct() {
                $this->destroy();
        }