rdbms: apply minimum sanity timeout for all cases of LoadBalancer::doWait()
[lhc/web/wiklou.git] / includes / libs / rdbms / loadbalancer / LoadBalancer.php
index a75dc4d..f410882 100644 (file)
@@ -122,6 +122,8 @@ class LoadBalancer implements ILoadBalancer {
 
        /** @var int Default 'maxLag' when unspecified */
        const MAX_LAG_DEFAULT = 10;
+       /** @var int Default 'waitTimeout' when unspecified */
+       const MAX_WAIT_DEFAULT = 10;
        /** @var int Seconds to cache master server read-only status */
        const TTL_CACHE_READONLY = 5;
 
@@ -146,19 +148,14 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
-               $this->localDomain = isset( $params['localDomain'] )
+               $localDomain = isset( $params['localDomain'] )
                        ? DatabaseDomain::newFromId( $params['localDomain'] )
                        : DatabaseDomain::newUnspecified();
-               // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost
-               // always true, gracefully handle the case when they fail to account for escaping.
-               if ( $this->localDomain->getTablePrefix() != '' ) {
-                       $this->localDomainIdAlias =
-                               $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix();
-               } else {
-                       $this->localDomainIdAlias = $this->localDomain->getDatabase();
-               }
+               $this->setLocalDomain( $localDomain );
 
-               $this->mWaitTimeout = isset( $params['waitTimeout'] ) ? $params['waitTimeout'] : 10;
+               $this->mWaitTimeout = isset( $params['waitTimeout'] )
+                       ? $params['waitTimeout']
+                       : self::MAX_WAIT_DEFAULT;
 
                $this->mReadIndex = -1;
                $this->mConns = [
@@ -242,6 +239,17 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
+       /**
+        * Get the local (and default) database domain ID of connection handles
+        *
+        * @see DatabaseDomain
+        * @return string Database domain ID; this specifies DB name, schema, and table prefix
+        * @since 1.31
+        */
+       public function getLocalDomainID() {
+               return $this->localDomain->getId();
+       }
+
        /**
         * Get a LoadMonitor instance
         *
@@ -507,6 +515,8 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function waitForAll( $pos, $timeout = null ) {
+               $timeout = $timeout ?: $this->mWaitTimeout;
+
                $oldPos = $this->mWaitForPos;
                try {
                        $this->mWaitForPos = $pos;
@@ -515,7 +525,12 @@ class LoadBalancer implements ILoadBalancer {
                        $ok = true;
                        for ( $i = 1; $i < $serverCount; $i++ ) {
                                if ( $this->mLoads[$i] > 0 ) {
+                                       $start = microtime( true );
                                        $ok = $this->doWait( $i, true, $timeout ) && $ok;
+                                       $timeout -= ( microtime( true ) - $start );
+                                       if ( $timeout <= 0 ) {
+                                               break; // timeout reached
+                                       }
                                }
                        }
                } finally {
@@ -564,7 +579,7 @@ class LoadBalancer implements ILoadBalancer {
         * @return bool
         */
        protected function doWait( $index, $open = false, $timeout = null ) {
-               $close = false; // close the connection afterwards
+               $timeout = max( 1, $timeout ?: $this->mWaitTimeout );
 
                // Check if we already know that the DB has reached this point
                $server = $this->getServerName( $index );
@@ -575,25 +590,32 @@ class LoadBalancer implements ILoadBalancer {
                        $knownReachedPos instanceof DBMasterPos &&
                        $knownReachedPos->hasReached( $this->mWaitForPos )
                ) {
-                       $this->replLogger->debug( __METHOD__ .
+                       $this->replLogger->debug(
+                               __METHOD__ .
                                ': replica DB {dbserver} known to be caught up (pos >= $knownReachedPos).',
-                               [ 'dbserver' => $server ] );
+                               [ 'dbserver' => $server ]
+                       );
                        return true;
                }
 
                // Find a connection to wait on, creating one if needed and allowed
+               $close = false; // close the connection afterwards
                $conn = $this->getAnyOpenConnection( $index );
                if ( !$conn ) {
                        if ( !$open ) {
-                               $this->replLogger->debug( __METHOD__ . ': no connection open for {dbserver}',
-                                       [ 'dbserver' => $server ] );
+                               $this->replLogger->debug(
+                                       __METHOD__ . ': no connection open for {dbserver}',
+                                       [ 'dbserver' => $server ]
+                               );
 
                                return false;
                        } else {
                                $conn = $this->openConnection( $index, self::DOMAIN_ANY );
                                if ( !$conn ) {
-                                       $this->replLogger->warning( __METHOD__ . ': failed to connect to {dbserver}',
-                                               [ 'dbserver' => $server ] );
+                                       $this->replLogger->warning(
+                                               __METHOD__ . ': failed to connect to {dbserver}',
+                                               [ 'dbserver' => $server ]
+                                       );
 
                                        return false;
                                }
@@ -603,16 +625,32 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
-               $this->replLogger->info( __METHOD__ . ': Waiting for replica DB {dbserver} to catch up...',
-                       [ 'dbserver' => $server ] );
-               $timeout = $timeout ?: $this->mWaitTimeout;
+               $this->replLogger->info(
+                       __METHOD__ .
+                       ': Waiting for replica DB {dbserver} to catch up...',
+                       [ 'dbserver' => $server ]
+               );
+
                $result = $conn->masterPosWait( $this->mWaitForPos, $timeout );
 
-               if ( $result == -1 || is_null( $result ) ) {
-                       // Timed out waiting for replica DB, use master instead
+               if ( $result === null ) {
+                       $this->replLogger->warning(
+                               __METHOD__ . ': Errored out waiting on {host} pos {pos}',
+                               [
+                                       'host' => $server,
+                                       'pos' => $this->mWaitForPos,
+                                       'trace' => ( new RuntimeException() )->getTraceAsString()
+                               ]
+                       );
+                       $ok = false;
+               } elseif ( $result == -1 ) {
                        $this->replLogger->warning(
                                __METHOD__ . ': Timed out waiting on {host} pos {pos}',
-                               [ 'host' => $server, 'pos' => $this->mWaitForPos ]
+                               [
+                                       'host' => $server,
+                                       'pos' => $this->mWaitForPos,
+                                       'trace' => ( new RuntimeException() )->getTraceAsString()
+                               ]
                        );
                        $ok = false;
                } else {
@@ -810,7 +848,11 @@ class LoadBalancer implements ILoadBalancer {
                                $server = $this->mServers[$i];
                                $server['serverIndex'] = $i;
                                $server['autoCommitOnly'] = $autoCommit;
-                               $conn = $this->reallyOpenConnection( $server, false );
+                               if ( $this->localDomain->getDatabase() !== null ) {
+                                       // Use the local domain table prefix if the local domain is specified
+                                       $server['tablePrefix'] = $this->localDomain->getTablePrefix();
+                               }
+                               $conn = $this->reallyOpenConnection( $server, $this->localDomain );
                                $host = $this->getServerName( $i );
                                if ( $conn->isOpen() ) {
                                        $this->connLogger->debug( "Connected to database $i at '$host'." );
@@ -888,8 +930,6 @@ class LoadBalancer implements ILoadBalancer {
                        // Reuse a free connection from another domain
                        $conn = reset( $this->mConns[$connFreeKey][$i] );
                        $oldDomain = key( $this->mConns[$connFreeKey][$i] );
-                       // The empty string as a DB name means "don't care".
-                       // DatabaseMysqlBase::open() already handle this on connection.
                        if ( strlen( $dbName ) && !$conn->selectDB( $dbName ) ) {
                                $this->mLastError = "Error selecting database '$dbName' on server " .
                                        $conn->getServer() . " from client host {$this->host}";
@@ -898,7 +938,8 @@ class LoadBalancer implements ILoadBalancer {
                        } else {
                                $conn->tablePrefix( $prefix );
                                unset( $this->mConns[$connFreeKey][$i][$oldDomain] );
-                               $this->mConns[$connInUseKey][$i][$domain] = $conn;
+                               // Note that if $domain is an empty string, getDomainID() might not match it
+                               $this->mConns[$connInUseKey][$i][$conn->getDomainId()] = $conn;
                                $this->connLogger->debug( __METHOD__ .
                                        ": reusing free connection from $oldDomain for $domain" );
                        }
@@ -912,14 +953,15 @@ class LoadBalancer implements ILoadBalancer {
                        $server['foreignPoolRefCount'] = 0;
                        $server['foreign'] = true;
                        $server['autoCommitOnly'] = $autoCommit;
-                       $conn = $this->reallyOpenConnection( $server, $dbName );
+                       $conn = $this->reallyOpenConnection( $server, $domainInstance );
                        if ( !$conn->isOpen() ) {
                                $this->connLogger->warning( __METHOD__ . ": connection error for $i/$domain" );
                                $this->errorConnection = $conn;
                                $conn = false;
                        } else {
-                               $conn->tablePrefix( $prefix );
-                               $this->mConns[$connInUseKey][$i][$domain] = $conn;
+                               $conn->tablePrefix( $prefix ); // as specified
+                               // Note that if $domain is an empty string, getDomainID() might not match it
+                               $this->mConns[$connInUseKey][$i][$conn->getDomainID()] = $conn;
                                $this->connLogger->debug( __METHOD__ . ": opened new connection for $i/$domain" );
                        }
                }
@@ -949,23 +991,24 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
-        * Really opens a connection. Uncached.
+        * Open a new network connection to a server (uncached)
+        *
         * Returns a Database object whether or not the connection was successful.
-        * @access private
         *
         * @param array $server
-        * @param string|bool $dbNameOverride Use "" to not select any database
+        * @param DatabaseDomain $domainOverride Use an unspecified domain to not select any database
         * @return Database
         * @throws DBAccessError
         * @throws InvalidArgumentException
         */
-       protected function reallyOpenConnection( array $server, $dbNameOverride = false ) {
+       protected function reallyOpenConnection( array $server, DatabaseDomain $domainOverride ) {
                if ( $this->disabled ) {
                        throw new DBAccessError();
                }
 
-               if ( $dbNameOverride !== false ) {
-                       $server['dbname'] = $dbNameOverride;
+               if ( $domainOverride->getDatabase() !== null ) {
+                       $server['dbname'] = $domainOverride->getDatabase();
+                       $server['schema'] = $domainOverride->getSchema();
                }
 
                // Let the handle know what the cluster master is (e.g. "db1052")
@@ -1629,8 +1672,11 @@ class LoadBalancer implements ILoadBalancer {
                        $result = $conn->masterPosWait( $pos, $timeout );
                        if ( $result == -1 || is_null( $result ) ) {
                                $msg = __METHOD__ . ': Timed out waiting on {host} pos {pos}';
-                               $this->replLogger->warning( $msg,
-                                       [ 'host' => $conn->getServer(), 'pos' => $pos ] );
+                               $this->replLogger->warning( $msg, [
+                                       'host' => $conn->getServer(),
+                                       'pos' => $pos,
+                                       'trace' => ( new RuntimeException() )->getTraceAsString()
+                               ] );
                                $ok = false;
                        } else {
                                $this->replLogger->info( __METHOD__ . ': Done' );
@@ -1638,8 +1684,13 @@ class LoadBalancer implements ILoadBalancer {
                        }
                } else {
                        $ok = false; // something is misconfigured
-                       $this->replLogger->error( 'Could not get master pos for {host}',
-                               [ 'host' => $conn->getServer() ] );
+                       $this->replLogger->error(
+                               __METHOD__ . ': could not get master pos for {host}',
+                               [
+                                       'host' => $conn->getServer(),
+                                       'trace' => ( new RuntimeException() )->getTraceAsString()
+                               ]
+                       );
                }
 
                return $ok;
@@ -1680,17 +1731,35 @@ class LoadBalancer implements ILoadBalancer {
                                "Foreign domain connections are still in use ($domains)." );
                }
 
-               $this->localDomain = new DatabaseDomain(
+               $oldDomain = $this->localDomain->getId();
+               $this->setLocalDomain( new DatabaseDomain(
                        $this->localDomain->getDatabase(),
-                       null,
+                       $this->localDomain->getSchema(),
                        $prefix
-               );
+               ) );
 
-               $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
-                       $db->tablePrefix( $prefix );
+               $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix, $oldDomain ) {
+                       if ( !$db->getLBInfo( 'foreign' ) ) {
+                               $db->tablePrefix( $prefix );
+                       }
                } );
        }
 
+       /**
+        * @param DatabaseDomain $domain
+        */
+       private function setLocalDomain( DatabaseDomain $domain ) {
+               $this->localDomain = $domain;
+               // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost
+               // always true, gracefully handle the case when they fail to account for escaping.
+               if ( $this->localDomain->getTablePrefix() != '' ) {
+                       $this->localDomainIdAlias =
+                               $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix();
+               } else {
+                       $this->localDomainIdAlias = $this->localDomain->getDatabase();
+               }
+       }
+
        /**
         * Make PHP ignore user aborts/disconnects until the returned
         * value leaves scope. This returns null and does nothing in CLI mode.