Merge "Add alternative DB_* class constants to ILoadBalancer"
[lhc/web/wiklou.git] / includes / libs / rdbms / lbfactory / LBFactory.php
index feae4bd..fd89f33 100644 (file)
@@ -30,6 +30,8 @@ use Psr\Log\LoggerInterface;
 abstract class LBFactory {
        /** @var ChronologyProtector */
        protected $chronProt;
+       /** @var object|string Class name or object With profileIn/profileOut methods */
+       protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
        /** @var LoggerInterface */
@@ -49,10 +51,13 @@ abstract class LBFactory {
        /** @var WANObjectCache */
        protected $wanCache;
 
-       /** @var string Local domain */
-       protected $domain;
+       /** @var DatabaseDomain Local domain */
+       protected $localDomain;
        /** @var string Local hostname of the app server */
        protected $hostname;
+       /** @var array Web request information about the client */
+       protected $requestInfo;
+
        /** @var mixed */
        protected $ticket;
        /** @var string|bool String if a requested DBO_TRX transaction round is active */
@@ -62,6 +67,11 @@ abstract class LBFactory {
        /** @var callable[] */
        protected $replicationWaitCallbacks = [];
 
+       /** @var bool Whether this PHP instance is for a CLI script */
+       protected $cliMode;
+       /** @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
@@ -70,11 +80,32 @@ abstract class LBFactory {
                [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
 
        /**
-        * @TODO: document base params here
-        * @param array $conf
+        * 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->domain = isset( $conf['domain'] ) ? $conf['domain'] : '';
+               $this->localDomain = isset( $conf['localDomain'] )
+                       ? DatabaseDomain::newFromId( $conf['localDomain'] )
+                       : DatabaseDomain::newUnspecified();
+
                if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
                        $this->readOnlyReason = $conf['readOnlyReason'];
                }
@@ -91,26 +122,31 @@ abstract class LBFactory {
                $this->errorLogger = isset( $conf['errorLogger'] )
                        ? $conf['errorLogger']
                        : function ( Exception $e ) {
-                               trigger_error( E_WARNING, $e->getMessage() );
+                               trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
                        };
-               $this->hostname = isset( $conf['hostname'] )
-                       ? $conf['hostname']
-                       : gethostname();
 
-               $this->chronProt = isset( $conf['chronProt'] )
-                       ? $conf['chronProt']
-                       : $this->newChronologyProtector();
+               $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
                $this->trxProfiler = isset( $conf['trxProfiler'] )
                        ? $conf['trxProfiler']
                        : new TransactionProfiler();
 
+               $this->requestInfo = [
+                       'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
+                       'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
+                       'ChronologyProtection' => 'true'
+               ];
+
+               $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
+               $this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
+               $this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
+
                $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()
+        * @see ILoadBalancer::disable()
         */
        public function destroy() {
                $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
@@ -121,7 +157,12 @@ abstract class LBFactory {
         * 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
+        * This method is for only advanced usage and callers should almost always use
+        * getMainLB() instead. This method can be useful when a table is used as a key/value
+        * store. In that cases, one might want to query it in autocommit mode (DBO_TRX off)
+        * but still use DBO_TRX transaction rounds on other tables.
+        *
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract public function newMainLB( $domain = false );
@@ -129,7 +170,7 @@ abstract class LBFactory {
        /**
         * Get a cached (tracked) load balancer object.
         *
-        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract public function getMainLB( $domain = false );
@@ -139,17 +180,22 @@ abstract class LBFactory {
         * untracked, not chronology-protected, and the caller is responsible for
         * cleaning it up.
         *
+        * This method is for only advanced usage and callers should almost always use
+        * getExternalLB() instead. This method can be useful when a table is used as a
+        * key/value store. In that cases, one might want to query it in autocommit mode
+        * (DBO_TRX off) but still use DBO_TRX transaction rounds on other tables.
+        *
         * @param string $cluster External storage cluster, or false for core
-        * @param bool|string $domain Wiki ID, or false for the current wiki
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
-       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 Wiki ID, or false for the current wiki
+        * @param bool|string $domain Domain ID, or false for the current domain
         * @return ILoadBalancer
         */
        abstract public function getExternalLB( $cluster, $domain = false );
@@ -172,10 +218,11 @@ abstract class LBFactory {
        public function shutdown(
                $mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
        ) {
+               $chronProt = $this->getChronologyProtector();
                if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, $workCallback, 'sync' );
+                       $this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
                } elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
-                       $this->shutdownChronologyProtector( $this->chronProt, null, 'async' );
+                       $this->shutdownChronologyProtector( $chronProt, null, 'async' );
                }
 
                $this->commitMasterChanges( __METHOD__ ); // sanity
@@ -373,14 +420,14 @@ abstract class LBFactory {
         * 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,
+        * 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:
-        *   - wiki : wait on the load balancer DBs that handles the given wiki
+        *   - 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
@@ -389,19 +436,23 @@ abstract class LBFactory {
         */
        public function waitForReplication( array $opts = [] ) {
                $opts += [
-                       'wiki' => false,
+                       'domain' => false,
                        'cluster' => false,
                        'timeout' => 60,
                        'ifWritesSince' => null
                ];
 
+               if ( $opts['domain'] === false && isset( $opts['wiki'] ) ) {
+                       $opts['domain'] = $opts['wiki']; // b/c
+               }
+
                // 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'] );
+               } elseif ( $opts['domain'] !== false ) {
+                       $lbs[] = $this->getMainLB( $opts['domain'] );
                } else {
                        $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) {
                                $lbs[] = $lb;
@@ -437,8 +488,7 @@ abstract class LBFactory {
                $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).
+                               // The DBMS may not support getMasterPos()
                                if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
                                        $failed[] = $lb->getServerName( $lb->getWriterIndex() );
                                }
@@ -479,7 +529,9 @@ abstract class LBFactory {
         */
        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;
                }
 
@@ -499,7 +551,9 @@ abstract class LBFactory {
         */
        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;
                }
 
@@ -527,7 +581,7 @@ abstract class LBFactory {
         * @since 1.28
         */
        public function getChronologyProtectorTouched( $dbName ) {
-               return $this->chronProt->getTouched( $dbName );
+               return $this->getChronologyProtector()->getTouched( $dbName );
        }
 
        /**
@@ -538,27 +592,39 @@ abstract class LBFactory {
         * @since 1.27
         */
        public function disableChronologyProtection() {
-               $this->chronProt->setEnabled( false );
+               $this->getChronologyProtector()->setEnabled( false );
        }
 
        /**
         * @return ChronologyProtector
         */
-       protected function newChronologyProtector() {
-               $chronProt = new ChronologyProtector(
+       protected function getChronologyProtector() {
+               if ( $this->chronProt ) {
+                       return $this->chronProt;
+               }
+
+               $this->chronProt = new ChronologyProtector(
                        $this->memCache,
                        [
-                               'ip' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
-                               'agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : ''
+                               'ip' => $this->requestInfo['IPAddress'],
+                               'agent' => $this->requestInfo['UserAgent'],
                        ],
                        isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
                );
-               $chronProt->setLogger( $this->replLogger );
-               if ( PHP_SAPI === 'cli' ) {
-                       $chronProt->setEnabled( false );
+               $this->chronProt->setLogger( $this->replLogger );
+
+               if ( $this->cliMode ) {
+                       $this->chronProt->setEnabled( false );
+               } elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
+                       // Request opted out of using position wait logic. This is useful for requests
+                       // done by the job queue or background ETL that do not have a meaningful session.
+                       $this->chronProt->setWaitEnabled( false );
                }
 
-               return $chronProt;
+               $this->replLogger->debug( __METHOD__ . ': using request info ' .
+                       json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
+
+               return $this->chronProt;
        }
 
        /**
@@ -600,16 +666,19 @@ abstract class LBFactory {
         */
        final protected function baseLoadBalancerParams() {
                return [
-                       'localDomain' => $this->domain,
+                       'localDomain' => $this->localDomain,
                        'readOnlyReason' => $this->readOnlyReason,
                        'srvCache' => $this->srvCache,
                        'wanCache' => $this->wanCache,
+                       'profiler' => $this->profiler,
                        'trxProfiler' => $this->trxProfiler,
                        'queryLogger' => $this->queryLogger,
                        'connLogger' => $this->connLogger,
                        'replLogger' => $this->replLogger,
                        'errorLogger' => $this->errorLogger,
-                       'hostname' => $this->hostname
+                       'hostname' => $this->hostname,
+                       'cliMode' => $this->cliMode,
+                       'agent' => $this->agent
                ];
        }
 
@@ -623,15 +692,21 @@ abstract class LBFactory {
        }
 
        /**
-        * Define a new local domain (for testing)
+        * Set a new table prefix for the existing local domain ID for testing
         *
-        * Caller should make sure no local connection are open to the old local domain
-        *
-        * @param string $domain
+        * @param string $prefix
         * @since 1.28
         */
-       public function setDomainPrefix( $domain ) {
-               $this->domain = $domain;
+       public function setDomainPrefix( $prefix ) {
+               $this->localDomain = new DatabaseDomain(
+                       $this->localDomain->getDatabase(),
+                       null,
+                       $prefix
+               );
+
+               $this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
+                       $lb->setDomainPrefix( $prefix );
+               } );
        }
 
        /**
@@ -641,4 +716,50 @@ abstract class LBFactory {
        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 ) {
+                       $usedCluster |= ( $lb->getServerCount() > 1 );
+               } );
+
+               if ( !$usedCluster ) {
+                       return $url; // no master/replica clusters touched
+               }
+
+               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;
+       }
+
+       function __destruct() {
+               $this->destroy();
+       }
 }