Merge "rdbms: add replica server counting methods to ILoadBalancer"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 21 Jun 2019 20:23:21 +0000 (20:23 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 21 Jun 2019 20:23:21 +0000 (20:23 +0000)
1  2 
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php

@@@ -62,6 -62,8 +62,8 @@@ use InvalidArgumentException
   *      Note that lag is still possible depending on how wsrep-sync-wait is set server-side.
   *   - Read-only archive clones: set 'is static' in the server configuration maps. This will
   *      treat all such DBs as having 0 lag.
+  *   - Externally updated dataset clones: set 'is static' in the server configuration maps.
+  *      This will treat all such DBs as having 0 lag.
   *   - SQL load balancing proxy: any proxy should handle lag checks on its own, so the 'max lag'
   *      parameter should probably be set to INF in the server configuration maps. This will make
   *      the load balancer ignore whatever it detects as the lag of the logical replica is (which
@@@ -148,7 -150,7 +150,7 @@@ interface ILoadBalancer 
        public function redefineLocalDomain( $domain );
  
        /**
-        * Get the index of the reader connection, which may be a replica DB
+        * Get the server index of the reader connection for a given group
         *
         * This takes into account load ratios and lag times. It should return a consistent
         * index during the life time of the load balancer. This initially checks replica DBs
        public function getMaintenanceConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
  
        /**
+        * Get the server index of the master server
+        *
         * @return int
         */
        public function getWriterIndex();
        public function isNonZeroLoad( $i );
  
        /**
-        * Get the number of defined servers (not the number of open connections)
+        * Get the number of servers defined in configuration
         *
         * @return int
         */
        public function getServerCount();
  
+       /**
+        * Whether there are any replica servers configured
+        *
+        * This counts both servers using streaming replication from the master server and
+        * servers that just have a clone of the static dataset found on the master server
+        *
+        * @return int
+        * @since 1.34
+        */
+       public function hasReplicaServers();
+       /**
+        * Whether any replica servers use streaming replication from the master server
+        *
+        * Generally this is one less than getServerCount(), though it might otherwise
+        * return a lower number if some of the servers are configured with "is static".
+        * That flag is used when both the server has no active replication setup and the
+        * dataset is either read-only or occasionally updated out-of-band. For example,
+        * a script might import a new geographic information dataset each week by writing
+        * it to each server and later directing the application to use the new version.
+        *
+        * It is possible for some replicas to be configured with "is static" but not
+        * others, though it generally should either be set for all or none of the replicas.
+        *
+        * If this returns zero, this means that there is generally no reason to execute
+        * replication wait logic for session consistency and lag reduction.
+        *
+        * @return int
+        * @since 1.34
+        */
+       public function hasStreamingReplicaServers();
        /**
         * Get the host name or IP address of the server with the specified index
         *
        public function forEachOpenReplicaConnection( $callback, array $params = [] );
  
        /**
-        * Get the hostname and lag time of the most-lagged replica DB
+        * Get the hostname and lag time of the most-lagged replica server
         *
         * This is useful for maintenance scripts that need to throttle their updates.
         * May attempt to open connections to replica DBs on the default DB. If there is
         */
        public function getLagTimes( $domain = false );
  
 -      /**
 -       * Get the lag in seconds for a given connection, or zero if this load
 -       * balancer does not have replication enabled.
 -       *
 -       * This should be used in preference to Database::getLag() in cases where
 -       * replication may not be in use, since there is no way to determine if
 -       * replication is in use at the connection level without running
 -       * potentially restricted queries such as SHOW SLAVE STATUS. Using this
 -       * function instead of Database::getLag() avoids a fatal error in this
 -       * case on many installations.
 -       *
 -       * @param IDatabase $conn
 -       * @return int|bool Returns false on error
 -       */
 -      public function safeGetLag( IDatabase $conn );
 -
        /**
         * Wait for a replica DB to reach a specified master position
         *
@@@ -166,15 -166,29 +166,29 @@@ class LoadBalancer implements ILoadBala
        const ROUND_ERROR = 'error';
  
        public function __construct( array $params ) {
-               if ( !isset( $params['servers'] ) ) {
-                       throw new InvalidArgumentException( __CLASS__ . ': missing "servers" parameter' );
+               if ( !isset( $params['servers'] ) || !count( $params['servers'] ) ) {
+                       throw new InvalidArgumentException( 'Missing or empty "servers" parameter' );
                }
-               $this->servers = $params['servers'];
-               foreach ( $this->servers as $i => $server ) {
+               $listKey = -1;
+               $this->servers = [];
+               $this->genericLoads = [];
+               foreach ( $params['servers'] as $i => $server ) {
+                       if ( ++$listKey !== $i ) {
+                               throw new UnexpectedValueException( 'List expected for "servers" parameter' );
+                       }
                        if ( $i == 0 ) {
-                               $this->servers[$i]['master'] = true;
+                               $server['master'] = true;
                        } else {
-                               $this->servers[$i]['replica'] = true;
+                               $server['replica'] = true;
+                       }
+                       $this->servers[$i] = $server;
+                       $this->genericLoads[$i] = $server['load'];
+                       if ( isset( $server['groupLoads'] ) ) {
+                               foreach ( $server['groupLoads'] as $group => $ratio ) {
+                                       $this->groupLoads[$group][$i] = $ratio;
+                               }
                        }
                }
  
  
                $this->waitTimeout = $params['waitTimeout'] ?? self::MAX_WAIT_DEFAULT;
  
-               $this->conns = [
-                       // Connection were transaction rounds may be applied
-                       self::KEY_LOCAL => [],
-                       self::KEY_FOREIGN_INUSE => [],
-                       self::KEY_FOREIGN_FREE => [],
-                       // Auto-committing counterpart connections that ignore transaction rounds
-                       self::KEY_LOCAL_NOROUND => [],
-                       self::KEY_FOREIGN_INUSE_NOROUND => [],
-                       self::KEY_FOREIGN_FREE_NOROUND => []
-               ];
-               $this->genericLoads = [];
+               $this->conns = self::newConnsArray();
                $this->waitForPos = false;
                $this->allowLagged = false;
  
                $this->loadMonitorConfig = $params['loadMonitor'] ?? [ 'class' => 'LoadMonitorNull' ];
                $this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
  
-               foreach ( $params['servers'] as $i => $server ) {
-                       $this->genericLoads[$i] = $server['load'];
-                       if ( isset( $server['groupLoads'] ) ) {
-                               foreach ( $server['groupLoads'] as $group => $ratio ) {
-                                       if ( !isset( $this->groupLoads[$group] ) ) {
-                                               $this->groupLoads[$group] = [];
-                                       }
-                                       $this->groupLoads[$group][$i] = $ratio;
-                               }
-                       }
-               }
                $this->srvCache = $params['srvCache'] ?? new EmptyBagOStuff();
                $this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
                $this->profiler = $params['profiler'] ?? null;
                $this->ownerId = $params['ownerId'] ?? null;
        }
  
+       private static function newConnsArray() {
+               return [
+                       // Connection were transaction rounds may be applied
+                       self::KEY_LOCAL => [],
+                       self::KEY_FOREIGN_INUSE => [],
+                       self::KEY_FOREIGN_FREE => [],
+                       // Auto-committing counterpart connections that ignore transaction rounds
+                       self::KEY_LOCAL_NOROUND => [],
+                       self::KEY_FOREIGN_INUSE_NOROUND => [],
+                       self::KEY_FOREIGN_FREE_NOROUND => []
+               ];
+       }
        public function getLocalDomainID() {
                return $this->localDomain->getId();
        }
  
                # Unset excessively lagged servers
                foreach ( $lags as $i => $lag ) {
-                       if ( $i != 0 ) {
+                       if ( $i !== $this->getWriterIndex() ) {
                                # How much lag this server nominally is allowed to have
                                $maxServerLag = $this->servers[$i]['max lag'] ?? $this->maxLag; // default
                                # Constrain that futher by $maxLag argument
        }
  
        public function getReaderIndex( $group = false, $domain = false ) {
-               if ( count( $this->servers ) == 1 ) {
+               if ( $this->getServerCount() == 1 ) {
                        // Skip the load balancing if there's only one server
                        return $this->getWriterIndex();
                }
  
                // If data seen by queries is expected to reflect the transactions committed as of
                // or after a given replication position then wait for the DB to apply those changes
-               if ( $this->waitForPos && $i != $this->getWriterIndex() && !$this->doWait( $i ) ) {
+               if ( $this->waitForPos && $i !== $this->getWriterIndex() && !$this->doWait( $i ) ) {
                        // Data will be outdated compared to what was expected
                        $laggedReplicaMode = true;
                }
                                        // Any server with less lag than it's 'max lag' param is preferable
                                        $i = $this->getRandomNonLagged( $currentLoads, $domain );
                                }
-                               if ( $i === false && count( $currentLoads ) != 0 ) {
+                               if ( $i === false && count( $currentLoads ) ) {
                                        // All replica DBs lagged. Switch to read-only mode
                                        $this->replLogger->error(
                                                __METHOD__ . ": all replica DBs lagged. Switch to read-only mode" );
                $oldPos = $this->waitForPos;
                try {
                        $this->waitForPos = $pos;
-                       $serverCount = count( $this->servers );
+                       $serverCount = $this->getServerCount();
  
                        $ok = true;
                        for ( $i = 1; $i < $serverCount; $i++ ) {
        }
  
        /**
-        * Wait for a given replica DB to catch up to the master pos stored in $this
+        * Wait for a given replica DB to catch up to the master pos stored in "waitForPos"
         * @param int $index Server index
         * @param bool $open Check the server even if a new connection has to be made
-        * @param int|null $timeout Max seconds to wait; default is "waitTimeout" given to __construct()
+        * @param int|null $timeout Max seconds to wait; default is "waitTimeout"
         * @return bool
         */
        protected function doWait( $index, $open = false, $timeout = null ) {
  
                $domain = $this->resolveDomainID( $domain );
                $flags = $this->sanitizeConnectionFlags( $flags );
-               $masterOnly = ( $i == self::DB_MASTER || $i == $this->getWriterIndex() );
+               $masterOnly = ( $i === self::DB_MASTER || $i === $this->getWriterIndex() );
  
                // Number of connections made before getting the server index and handle
                $priorConnectionsMade = $this->connsOpened;
                }
  
                $this->enforceConnectionFlags( $conn, $flags );
-               if ( $serverIndex == $this->getWriterIndex() ) {
+               if ( $serverIndex === $this->getWriterIndex() ) {
                        // If the load balancer is read-only, perhaps due to replication lag, then master
                        // DB handles will reflect that. Note that Database::assertIsWritableMaster() takes
                        // care of replica DB handles whereas getReadOnlyReason() would cause infinite loops.
                return count( $this->servers );
        }
  
+       public function hasReplicaServers() {
+               return ( $this->getServerCount() > 1 );
+       }
+       public function hasStreamingReplicaServers() {
+               foreach ( $this->servers as $i => $server ) {
+                       if ( $i !== $this->getWriterIndex() && empty( $server['is static'] ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
        public function getServerName( $i ) {
                $name = $this->servers[$i]['hostName'] ?? $this->servers[$i]['host'] ?? '';
  
                # master (however unlikely that may be), then we can fetch the position from the replica DB.
                $masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
                if ( !$masterConn ) {
-                       $serverCount = count( $this->servers );
+                       $serverCount = $this->getServerCount();
                        for ( $i = 1; $i < $serverCount; $i++ ) {
                                $conn = $this->getAnyOpenConnection( $i );
                                if ( $conn ) {
                        $conn->close();
                } );
  
-               $this->conns = [
-                       self::KEY_LOCAL => [],
-                       self::KEY_FOREIGN_INUSE => [],
-                       self::KEY_FOREIGN_FREE => [],
-                       self::KEY_LOCAL_NOROUND => [],
-                       self::KEY_FOREIGN_INUSE_NOROUND => [],
-                       self::KEY_FOREIGN_FREE_NOROUND => []
-               ];
+               $this->conns = self::newConnsArray();
                $this->connsOpened = 0;
        }
  
        public function getLaggedReplicaMode( $domain = false ) {
                if (
                        // Avoid recursion if there is only one DB
-                       $this->getServerCount() > 1 &&
+                       $this->hasStreamingReplicaServers() &&
                        // Avoid recursion if the (non-zero load) master DB was picked for generic reads
                        $this->genericReadIndex !== $this->getWriterIndex() &&
                        // Stay in lagged replica mode during the load balancer instance lifetime
        }
  
        public function getMaxLag( $domain = false ) {
-               $maxLag = -1;
                $host = '';
+               $maxLag = -1;
                $maxIndex = 0;
  
-               if ( $this->getServerCount() <= 1 ) {
-                       return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
-               }
-               $lagTimes = $this->getLagTimes( $domain );
-               foreach ( $lagTimes as $i => $lag ) {
-                       if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
-                               $maxLag = $lag;
-                               $host = $this->servers[$i]['host'];
-                               $maxIndex = $i;
+               if ( $this->hasReplicaServers() ) {
+                       $lagTimes = $this->getLagTimes( $domain );
+                       foreach ( $lagTimes as $i => $lag ) {
+                               if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
+                                       $maxLag = $lag;
+                                       $host = $this->servers[$i]['host'];
+                                       $maxIndex = $i;
+                               }
                        }
                }
  
        }
  
        public function getLagTimes( $domain = false ) {
-               if ( $this->getServerCount() <= 1 ) {
+               if ( !$this->hasReplicaServers() ) {
                        return [ $this->getWriterIndex() => 0 ]; // no replication = no lag
                }
  
                return $this->getLoadMonitor()->getLagTimes( $indexesWithLag, $domain ) + $knownLagTimes;
        }
  
 +      /**
 +       * Get the lag in seconds for a given connection, or zero if this load
 +       * balancer does not have replication enabled.
 +       *
 +       * This should be used in preference to Database::getLag() in cases where
 +       * replication may not be in use, since there is no way to determine if
 +       * replication is in use at the connection level without running
 +       * potentially restricted queries such as SHOW SLAVE STATUS. Using this
 +       * function instead of Database::getLag() avoids a fatal error in this
 +       * case on many installations.
 +       *
 +       * @param IDatabase $conn
 +       * @return int|bool Returns false on error
 +       * @deprecated Since 1.34 Use IDatabase::getLag() instead
 +       */
        public function safeGetLag( IDatabase $conn ) {
-               if ( $this->getServerCount() <= 1 ) {
-                       return 0;
-               } else {
-                       return $conn->getLag();
+               if ( $conn->getLBInfo( 'is static' ) ) {
+                       return 0; // static dataset
+               } elseif ( $conn->getLBInfo( 'serverIndex' ) == $this->getWriterIndex() ) {
+                       return 0; // this is the master
                }
+               return $conn->getLag();
        }
  
        public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
                        if ( $masterConn ) {
                                $pos = $masterConn->getMasterPos();
                        } else {
 -                              $masterConn = $this->getConnection( $index, [], self::DOMAIN_ANY, self::CONN_SILENCE_ERRORS );
 +                              $flags = self::CONN_SILENCE_ERRORS;
 +                              $masterConn = $this->getConnection( $index, [], self::DOMAIN_ANY, $flags );
                                if ( !$masterConn ) {
                                        throw new DBReplicationWaitError(
                                                null,