Merge "Remember checkbox state on Special:Block if checkbox disabled"
[lhc/web/wiklou.git] / includes / libs / rdbms / loadbalancer / LoadBalancer.php
index b640dc0..1ef1d09 100644 (file)
@@ -73,8 +73,6 @@ class LoadBalancer implements ILoadBalancer {
 
        /** @var array[] Map of (server index => server config array) */
        private $servers;
-       /** @var float[] Map of (server index => weight) */
-       private $genericLoads;
        /** @var array[] Map of (group => server index => weight) */
        private $groupLoads;
        /** @var bool Whether to disregard replica DB lag as a factor in replica DB selection */
@@ -87,7 +85,7 @@ class LoadBalancer implements ILoadBalancer {
        private $localDomainIdAlias;
        /** @var int Amount of replication lag, in seconds, that is considered "high" */
        private $maxLag;
-       /** @var string|bool The query group list to be used by default */
+       /** @var string|null Default query group to use with getConnection() */
        private $defaultGroup;
 
        /** @var string Current server name */
@@ -106,17 +104,13 @@ class LoadBalancer implements ILoadBalancer {
 
        /** @var Database Connection handle that caused a problem */
        private $errorConnection;
-       /** @var int The generic (not query grouped) replica server index */
-       private $genericReadIndex = -1;
        /** @var int[] The group replica server indexes keyed by group */
        private $readIndexByGroup = [];
        /** @var bool|DBMasterPos Replication sync position or false if not set */
        private $waitForPos;
        /** @var bool Whether the generic reader fell back to a lagged replica DB */
        private $laggedReplicaMode = false;
-       /** @var bool Whether the generic reader fell back to a lagged replica DB */
-       private $allReplicasDownMode = false;
-       /** @var string The last DB domain selection or connection error */
+       /** @var string The last DB selection or connection error */
        private $lastError = 'Unknown error';
        /** @var string|bool Reason this instance is read-only or false if not */
        private $readOnlyReason = false;
@@ -127,9 +121,9 @@ class LoadBalancer implements ILoadBalancer {
        /** @var bool Whether any connection has been attempted yet */
        private $connectionAttempted = false;
 
-       /** @var int|null An integer ID of the managing LBFactory instance or null */
+       /** @var int|null Integer ID of the managing LBFactory instance or null if none */
        private $ownerId;
-       /** @var string|bool String if a requested DBO_TRX transaction round is active */
+       /** @var string|bool Explicit DBO_TRX transaction round active or false if none */
        private $trxRoundId = false;
        /** @var string Stage of the current transaction round in the transaction round life-cycle */
        private $trxRoundStage = self::ROUND_CURSORY;
@@ -141,7 +135,7 @@ class LoadBalancer implements ILoadBalancer {
        const MAX_LAG_DEFAULT = 6;
        /** @var int Default 'waitTimeout' when unspecified */
        const MAX_WAIT_DEFAULT = 10;
-       /** @var int Seconds to cache master server read-only status */
+       /** @var int Seconds to cache master DB server read-only status */
        const TTL_CACHE_READONLY = 5;
 
        const KEY_LOCAL = 'local';
@@ -172,7 +166,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $listKey = -1;
                $this->servers = [];
-               $this->genericLoads = [];
+               $this->groupLoads = [ self::GROUP_GENERIC => [] ];
                foreach ( $params['servers'] as $i => $server ) {
                        if ( ++$listKey !== $i ) {
                                throw new UnexpectedValueException( 'List expected for "servers" parameter' );
@@ -183,12 +177,10 @@ class LoadBalancer implements ILoadBalancer {
                                $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;
-                               }
+                       $serverGroupLoads = [ self::GROUP_GENERIC => $server['load'] ];
+                       $serverGroupLoads += ( $server['groupLoads'] ?? [] );
+                       foreach ( $serverGroupLoads as $group => $ratio ) {
+                               $this->groupLoads[$group][$i] = $ratio;
                        }
                }
 
@@ -244,7 +236,9 @@ class LoadBalancer implements ILoadBalancer {
                        }
                }
 
-               $this->defaultGroup = $params['defaultGroup'] ?? self::GROUP_GENERIC;
+               $group = $params['defaultGroup'] ?? self::GROUP_GENERIC;
+               $this->defaultGroup = isset( $this->groupLoads[$group] ) ? $group : self::GROUP_GENERIC;
+
                $this->ownerId = $params['ownerId'] ?? null;
        }
 
@@ -275,48 +269,58 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
-        * @param string[]|string|bool $groups Query group list or false for the default
+        * Resolve $groups into a list of query groups defining as having database servers
+        *
+        * @param string[]|string|bool $groups Query group(s) in preference order, [], or false
         * @param int $i Specific server index or DB_MASTER/DB_REPLICA
-        * @return string[]|bool[] Query group list
+        * @return string[] Non-empty group list in preference order with the default group appended
         */
        private function resolveGroups( $groups, $i ) {
-               if ( $groups === false ) {
-                       $resolvedGroups = [ $this->defaultGroup ];
-               } elseif ( is_string( $groups ) ) {
-                       $resolvedGroups = [ $groups ];
-               } elseif ( is_array( $groups ) ) {
-                       $resolvedGroups = $groups ?: [ $this->defaultGroup ];
-               } else {
-                       throw new InvalidArgumentException( "Invalid query groups provided" );
+               // If a specific replica server was specified, then $groups makes no sense
+               if ( $i > 0 && $groups !== [] && $groups !== false ) {
+                       $list = implode( ', ', (array)$groups );
+                       throw new LogicException( "Query group(s) ($list) given with server index (#$i)" );
                }
 
-               if ( $groups && $i > 0 ) {
-                       $groupList = implode( ', ', $groups );
-                       throw new LogicException( "Got query groups ($groupList) with a server index (#$i)" );
+               if ( $groups === [] || $groups === false || $groups === $this->defaultGroup ) {
+                       $resolvedGroups = [ $this->defaultGroup ]; // common case
+               } elseif ( is_string( $groups ) && isset( $this->groupLoads[$groups] ) ) {
+                       $resolvedGroups = [ $groups, $this->defaultGroup ];
+               } elseif ( is_array( $groups ) ) {
+                       $resolvedGroups = array_keys( array_flip( $groups ) + [ self::GROUP_GENERIC => 1 ] );
+               } else {
+                       $resolvedGroups = [ $this->defaultGroup ];
                }
 
                return $resolvedGroups;
        }
 
        /**
-        * @param int $flags
-        * @return bool
+        * @param int $flags Bitfield of class CONN_* constants
+        * @param int $i Specific server index or DB_MASTER/DB_REPLICA
+        * @return int Sanitized bitfield
         */
-       private function sanitizeConnectionFlags( $flags ) {
-               if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) === self::CONN_TRX_AUTOCOMMIT ) {
-                       // Assuming all servers are of the same type (or similar), which is overwhelmingly
-                       // the case, use the master server information to get the attributes. The information
-                       // for $i cannot be used since it might be DB_REPLICA, which might require connection
-                       // attempts in order to be resolved into a real server index.
+       private function sanitizeConnectionFlags( $flags, $i ) {
+               // Whether an outside caller is explicitly requesting the master database server
+               if ( $i === self::DB_MASTER || $i === $this->getWriterIndex() ) {
+                       $flags |= self::CONN_INTENT_WRITABLE;
+               }
+
+               if ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT ) {
+                       // Callers use CONN_TRX_AUTOCOMMIT to bypass REPEATABLE-READ staleness without
+                       // resorting to row locks (e.g. FOR UPDATE) or to make small out-of-band commits
+                       // during larger transactions. This is useful for avoiding lock contention.
+
+                       // Master DB server attributes (should match those of the replica DB servers)
                        $attributes = $this->getServerAttributes( $this->getWriterIndex() );
                        if ( $attributes[Database::ATTR_DB_LEVEL_LOCKING] ) {
-                               // Callers sometimes want to (a) escape REPEATABLE-READ stateness without locking
-                               // rows (e.g. FOR UPDATE) or (b) make small commits during a larger transactions
-                               // to reduce lock contention. None of these apply for sqlite and using separate
-                               // connections just causes self-deadlocks.
+                               // The RDBMS does not support concurrent writes (e.g. SQLite), so attempts
+                               // to use separate connections would just cause self-deadlocks. Note that
+                               // REPEATABLE-READ staleness is not an issue since DB-level locking means
+                               // that transactions are Strict Serializable anyway.
                                $flags &= ~self::CONN_TRX_AUTOCOMMIT;
-                               $this->connLogger->info( __METHOD__ .
-                                       ': ignoring CONN_TRX_AUTOCOMMIT to avoid deadlocks.' );
+                               $type = $this->getServerType( $this->getWriterIndex() );
+                               $this->connLogger->info( __METHOD__ . ": CONN_TRX_AUTOCOMMIT disallowed ($type)" );
                        }
                }
 
@@ -341,7 +345,7 @@ class LoadBalancer implements ILoadBalancer {
                }
        }
 
-               /**
+       /**
         * Get a LoadMonitor instance
         *
         * @return ILoadMonitor
@@ -426,7 +430,7 @@ class LoadBalancer implements ILoadBalancer {
         * Get the server index to use for a specified server index and query group list
         *
         * @param int $i Specific server index or DB_MASTER/DB_REPLICA
-        * @param string[]|bool[] $groups Resolved query group list (non-empty)
+        * @param string[] $groups Non-empty query group list in preference order
         * @param string|bool $domain
         * @return int A specific server index (replica DBs are checked for connectivity)
         */
@@ -434,7 +438,6 @@ class LoadBalancer implements ILoadBalancer {
                if ( $i === self::DB_MASTER ) {
                        $i = $this->getWriterIndex();
                } elseif ( $i === self::DB_REPLICA ) {
-                       // Find an available server in any of the query groups (in order)
                        foreach ( $groups as $group ) {
                                $groupIndex = $this->getReaderIndex( $group, $domain );
                                if ( $groupIndex !== false ) {
@@ -447,21 +450,10 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                if ( $i === self::DB_REPLICA ) {
-                       // No specific server was yet found
                        $this->lastError = 'Unknown error'; // set here in case of worse failure
-                       // Either make one last connection attempt or give up
-                       $i = in_array( $this->defaultGroup, $groups, true )
-                               // Connection attempt already included the default query group; give up
-                               ? false
-                               // Connection attempt was for other query groups; try the default one
-                               : $this->getReaderIndex( $this->defaultGroup, $domain );
-
-                       if ( $i === false ) {
-                               // Still coundn't find a working non-zero read load server
-                               $this->lastError = 'No working replica DB server: ' . $this->lastError;
-                               $this->reportConnectionError();
-                               return null; // unreachable due to exception
-                       }
+                       $this->lastError = 'No working replica DB server: ' . $this->lastError;
+                       $this->reportConnectionError();
+                       return null; // unreachable due to exception
                }
 
                return $i;
@@ -473,25 +465,21 @@ class LoadBalancer implements ILoadBalancer {
                        return $this->getWriterIndex();
                }
 
+               $group = is_string( $group ) ? $group : self::GROUP_GENERIC;
+
                $index = $this->getExistingReaderIndex( $group );
                if ( $index >= 0 ) {
                        // A reader index was already selected and "waitForPos" was handled
                        return $index;
                }
 
-               if ( $group !== self::GROUP_GENERIC ) {
-                       // Use the server weight array for this load group
-                       if ( isset( $this->groupLoads[$group] ) ) {
-                               $loads = $this->groupLoads[$group];
-                       } else {
-                               // No loads for this group, return false and the caller can use some other group
-                               $this->connLogger->info( __METHOD__ . ": no loads for group $group" );
-
-                               return false;
-                       }
+               // Use the server weight array for this load group
+               if ( isset( $this->groupLoads[$group] ) ) {
+                       $loads = $this->groupLoads[$group];
                } else {
-                       // Use the generic load group
-                       $loads = $this->genericLoads;
+                       $this->connLogger->info( __METHOD__ . ": no loads for group $group" );
+
+                       return false;
                }
 
                // Scale the configured load ratios according to each server's load and state
@@ -528,41 +516,30 @@ class LoadBalancer implements ILoadBalancer {
        /**
         * Get the server index chosen by the load balancer for use with the given query group
         *
-        * @param string|bool $group Query group; use false for the generic group
+        * @param string $group Query group; use false for the generic group
         * @return int Server index or -1 if none was chosen
         */
        protected function getExistingReaderIndex( $group ) {
-               if ( $group === self::GROUP_GENERIC ) {
-                       $index = $this->genericReadIndex;
-               } else {
-                       $index = $this->readIndexByGroup[$group] ?? -1;
-               }
-
-               return $index;
+               return $this->readIndexByGroup[$group] ?? -1;
        }
 
        /**
         * Set the server index chosen by the load balancer for use with the given query group
         *
-        * @param string|bool $group Query group; use false for the generic group
+        * @param string $group Query group; use false for the generic group
         * @param int $index The index of a specific server
         */
        private function setExistingReaderIndex( $group, $index ) {
                if ( $index < 0 ) {
                        throw new UnexpectedValueException( "Cannot set a negative read server index" );
                }
-
-               if ( $group === self::GROUP_GENERIC ) {
-                       $this->genericReadIndex = $index;
-               } else {
-                       $this->readIndexByGroup[$group] = $index;
-               }
+               $this->readIndexByGroup[$group] = $index;
        }
 
        /**
         * @param array $loads List of server weights
         * @param string|bool $domain
-        * @return array (reader index, lagged replica mode) or false on failure
+        * @return array (reader index, lagged replica mode) or (false, false) on failure
         */
        private function pickReaderIndex( array $loads, $domain = false ) {
                if ( $loads === [] ) {
@@ -615,7 +592,9 @@ class LoadBalancer implements ILoadBalancer {
                        $serverName = $this->getServerName( $i );
                        $this->connLogger->debug( __METHOD__ . ": Using reader #$i: $serverName..." );
 
-                       $conn = $this->getConnection( $i, [], $domain, self::CONN_SILENCE_ERRORS );
+                       // Get a connection to this server without triggering other server connections
+                       $flags = self::CONN_SILENCE_ERRORS;
+                       $conn = $this->getServerConnection( $i, $domain, $flags );
                        if ( !$conn ) {
                                $this->connLogger->warning( __METHOD__ . ": Failed connecting to $i/$domain" );
                                unset( $currentLoads[$i] ); // avoid this server next iteration
@@ -646,7 +625,8 @@ class LoadBalancer implements ILoadBalancer {
                try {
                        $this->waitForPos = $pos;
                        // If a generic reader connection was already established, then wait now
-                       if ( $this->genericReadIndex > 0 && !$this->doWait( $this->genericReadIndex ) ) {
+                       $i = $this->getExistingReaderIndex( self::GROUP_GENERIC );
+                       if ( $i > 0 && !$this->doWait( $i ) ) {
                                $this->laggedReplicaMode = true;
                        }
                        // Otherwise, wait until a connection is established in getReaderIndex()
@@ -661,10 +641,10 @@ class LoadBalancer implements ILoadBalancer {
                try {
                        $this->waitForPos = $pos;
 
-                       $i = $this->genericReadIndex;
+                       $i = $this->getExistingReaderIndex( self::GROUP_GENERIC );
                        if ( $i <= 0 ) {
                                // Pick a generic replica DB if there isn't one yet
-                               $readLoads = $this->genericLoads;
+                               $readLoads = $this->groupLoads[self::GROUP_GENERIC];
                                unset( $readLoads[$this->getWriterIndex()] ); // replica DBs only
                                $readLoads = array_filter( $readLoads ); // with non-zero load
                                $i = ArrayUtils::pickRandom( $readLoads );
@@ -693,7 +673,7 @@ class LoadBalancer implements ILoadBalancer {
 
                        $ok = true;
                        for ( $i = 1; $i < $serverCount; $i++ ) {
-                               if ( $this->genericLoads[$i] > 0 ) {
+                               if ( $this->groupLoads[self::GROUP_GENERIC][$i] > 0 ) {
                                        $start = microtime( true );
                                        $ok = $this->doWait( $i, true, $timeout ) && $ok;
                                        $timeout -= intval( microtime( true ) - $start );
@@ -725,6 +705,8 @@ class LoadBalancer implements ILoadBalancer {
 
        public function getAnyOpenConnection( $i, $flags = 0 ) {
                $i = ( $i === self::DB_MASTER ) ? $this->getWriterIndex() : $i;
+               // Connection handles required to be in auto-commit mode use a separate connection
+               // pool since the main pool is effected by implicit and explicit transaction rounds
                $autocommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
 
                $conn = false;
@@ -782,7 +764,7 @@ class LoadBalancer implements ILoadBalancer {
 
        /**
         * Wait for a given replica DB to catch up to the master pos stored in "waitForPos"
-        * @param int $index Server index
+        * @param int $index Specific 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"
         * @return bool
@@ -809,7 +791,8 @@ class LoadBalancer implements ILoadBalancer {
 
                // Find a connection to wait on, creating one if needed and allowed
                $close = false; // close the connection afterwards
-               $conn = $this->getAnyOpenConnection( $index );
+               $flags = self::CONN_SILENCE_ERRORS;
+               $conn = $this->getAnyOpenConnection( $index, $flags );
                if ( !$conn ) {
                        if ( !$open ) {
                                $this->replLogger->debug(
@@ -819,8 +802,8 @@ class LoadBalancer implements ILoadBalancer {
 
                                return false;
                        }
-                       // Open a temporary new connection in order to wait for replication
-                       $conn = $this->getConnection( $index, [], self::DOMAIN_ANY, self::CONN_SILENCE_ERRORS );
+                       // Get a connection to this server without triggering other server connections
+                       $conn = $this->getServerConnection( $index, self::DOMAIN_ANY, $flags );
                        if ( !$conn ) {
                                $this->replLogger->warning(
                                        __METHOD__ . ': failed to connect to {dbserver}',
@@ -877,20 +860,42 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getConnection( $i, $groups = [], $domain = false, $flags = 0 ) {
-               $groups = $this->resolveGroups( $groups, $i );
                $domain = $this->resolveDomainID( $domain );
-               $flags = $this->sanitizeConnectionFlags( $flags );
-               $masterOnly = ( $i === self::DB_MASTER || $i === $this->getWriterIndex() );
+               $groups = $this->resolveGroups( $groups, $i );
+               $flags = $this->sanitizeConnectionFlags( $flags, $i );
+               // If given DB_MASTER/DB_REPLICA, resolve it to a specific server index. Resolving
+               // DB_REPLICA might trigger getServerConnection() calls due to the getReaderIndex()
+               // connectivity checks or LoadMonitor::scaleLoads() server state cache regeneration.
+               // The use of getServerConnection() instead of getConnection() avoids infinite loops.
+               $serverIndex = $this->getConnectionIndex( $i, $groups, $domain );
+               // Get an open connection to that server (might trigger a new connection)
+               $conn = $this->getServerConnection( $serverIndex, $domain, $flags );
+               // Set master DB handles as read-only if there is high replication lag
+               if ( $serverIndex === $this->getWriterIndex() && $this->getLaggedReplicaMode( $domain ) ) {
+                       $reason = ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 )
+                               ? 'The database is read-only until replication lag decreases.'
+                               : 'The database is read-only until replica database servers becomes reachable.';
+                       $conn->setLBInfo( 'readOnlyReason', $reason );
+               }
+
+               return $conn;
+       }
 
+       /**
+        * @param int $i Specific server index
+        * @param string $domain Resolved DB domain
+        * @param int $flags Bitfield of class CONN_* constants
+        * @return IDatabase|bool
+        * @throws InvalidArgumentException When the server index is invalid
+        */
+       public function getServerConnection( $i, $domain, $flags = 0 ) {
                // Number of connections made before getting the server index and handle
                $priorConnectionsMade = $this->connectionCounter;
-               // Choose a server if $i is DB_MASTER/DB_REPLICA (might trigger new connections)
-               $serverIndex = $this->getConnectionIndex( $i, $groups, $domain );
-               // Get an open connection to that server (might trigger a new connection)
+               // Get an open connection to this server (might trigger a new connection)
                $conn = $this->localDomain->equals( $domain )
-                       ? $this->getLocalConnection( $serverIndex, $flags )
-                       : $this->getForeignConnection( $serverIndex, $domain, $flags );
-               // Throw an error or bail out if the connection attempt failed
+                       ? $this->getLocalConnection( $i, $flags )
+                       : $this->getForeignConnection( $i, $domain, $flags );
+               // Throw an error or otherwise bail out if the connection attempt failed
                if ( !( $conn instanceof IDatabase ) ) {
                        if ( ( $flags & self::CONN_SILENCE_ERRORS ) != self::CONN_SILENCE_ERRORS ) {
                                $this->reportConnectionError();
@@ -901,25 +906,36 @@ class LoadBalancer implements ILoadBalancer {
 
                // Profile any new connections caused by this method
                if ( $this->connectionCounter > $priorConnectionsMade ) {
-                       $host = $conn->getServer();
-                       $dbname = $conn->getDBname();
-                       $this->trxProfiler->recordConnection( $host, $dbname, $masterOnly );
+                       $this->trxProfiler->recordConnection(
+                               $conn->getServer(),
+                               $conn->getDBname(),
+                               ( ( $flags & self::CONN_INTENT_WRITABLE ) == self::CONN_INTENT_WRITABLE )
+                       );
                }
 
                if ( !$conn->isOpen() ) {
-                       // Connection was made but later unrecoverably lost for some reason.
-                       // Do not return a handle that will just throw exceptions on use,
-                       // but let the calling code (e.g. getReaderIndex) try another server.
                        $this->errorConnection = $conn;
+                       // Connection was made but later unrecoverably lost for some reason.
+                       // Do not return a handle that will just throw exceptions on use, but
+                       // let the calling code, e.g. getReaderIndex(), try another server.
                        return false;
                }
 
+               // Make sure that flags like CONN_TRX_AUTOCOMMIT are respected by this handle
                $this->enforceConnectionFlags( $conn, $flags );
-               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.
-                       $conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $domain, $conn ) );
+               // Set master DB handles as read-only if the load balancer is configured as read-only
+               // or the master database server is running in server-side read-only mode. Note that
+               // replica DB handles are always read-only via Database::assertIsWritableMaster().
+               // Read-only mode due to replication lag is *avoided* here to avoid recursion.
+               if ( $conn->getLBInfo( 'serverIndex' ) === $this->getWriterIndex() ) {
+                       if ( $this->readOnlyReason !== false ) {
+                               $conn->setLBInfo( 'readOnlyReason', $this->readOnlyReason );
+                       } elseif ( $this->masterRunningReadOnly( $domain, $conn ) ) {
+                               $conn->setLBInfo(
+                                       'readOnlyReason',
+                                       'The master database server is running in read-only mode.'
+                               );
+                       }
                }
 
                return $conn;
@@ -1019,7 +1035,7 @@ class LoadBalancer implements ILoadBalancer {
 
        /**
         * @param int $i
-        * @param bool $domain
+        * @param string|bool $domain
         * @param int $flags
         * @return Database|bool Live database handle or false on failure
         * @deprecated Since 1.34 Use getConnection() instead
@@ -1039,6 +1055,8 @@ class LoadBalancer implements ILoadBalancer {
         * @param int $i Server index
         * @param int $flags Class CONN_* constant bitfield
         * @return Database
+        * @throws InvalidArgumentException When the server index is invalid
+        * @throws UnexpectedValueException When the DB domain of the connection is corrupted
         */
        private function getLocalConnection( $i, $flags = 0 ) {
                // Connection handles required to be in auto-commit mode use a separate connection
@@ -1101,6 +1119,8 @@ class LoadBalancer implements ILoadBalancer {
         * @param int $flags Class CONN_* constant bitfield
         * @return Database|bool Returns false on connection error
         * @throws DBError When database selection fails
+        * @throws InvalidArgumentException When the server index is invalid
+        * @throws UnexpectedValueException When the DB domain of the connection is corrupted
         */
        private function getForeignConnection( $i, $domain, $flags = 0 ) {
                $domainInstance = DatabaseDomain::newFromId( $domain );
@@ -1369,7 +1389,7 @@ class LoadBalancer implements ILoadBalancer {
         * @deprecated Since 1.34
         */
        public function isNonZeroLoad( $i ) {
-               return array_key_exists( $i, $this->servers ) && $this->genericLoads[$i] != 0;
+               return ( isset( $this->servers[$i] ) && $this->groupLoads[self::GROUP_GENERIC][$i] > 0 );
        }
 
        public function getServerCount() {
@@ -1405,22 +1425,56 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getMasterPos() {
-               # If this entire request was served from a replica DB without opening a connection to the
-               # master (however unlikely that may be), then we can fetch the position from the replica DB.
+               $index = $this->getWriterIndex();
+
+               $conn = $this->getAnyOpenConnection( $index );
+               if ( $conn ) {
+                       return $conn->getMasterPos();
+               }
+
+               $conn = $this->getConnection( $index, self::CONN_SILENCE_ERRORS );
+               if ( !$conn ) {
+                       $this->reportConnectionError();
+                       return null; // unreachable due to exception
+               }
+
+               try {
+                       $pos = $conn->getMasterPos();
+               } finally {
+                       $this->closeConnection( $conn );
+               }
+
+               return $pos;
+       }
+
+       public function getReplicaResumePos() {
+               // Get the position of any existing master server connection
                $masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
-               if ( !$masterConn ) {
-                       $serverCount = $this->getServerCount();
-                       for ( $i = 1; $i < $serverCount; $i++ ) {
-                               $conn = $this->getAnyOpenConnection( $i );
-                               if ( $conn ) {
-                                       return $conn->getReplicaPos();
-                               }
-                       }
-               } else {
+               if ( $masterConn ) {
                        return $masterConn->getMasterPos();
                }
 
-               return false;
+               // Get the highest position of any existing replica server connection
+               $highestPos = false;
+               $serverCount = $this->getServerCount();
+               for ( $i = 1; $i < $serverCount; $i++ ) {
+                       if ( !empty( $this->servers[$i]['is static'] ) ) {
+                               continue; // server does not use replication
+                       }
+
+                       $conn = $this->getAnyOpenConnection( $i );
+                       $pos = $conn ? $conn->getReplicaPos() : false;
+                       if ( !$pos ) {
+                               continue; // no open connection or could not get position
+                       }
+
+                       $highestPos = $highestPos ?: $pos;
+                       if ( $pos->hasReached( $highestPos ) ) {
+                               $highestPos = $pos;
+                       }
+               }
+
+               return $highestPos;
        }
 
        public function disable() {
@@ -1780,7 +1834,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                if ( $conn->getFlag( $conn::DBO_TRX ) ) {
-                       $conn->setLBInfo( 'trxRoundId', false );
+                       $conn->setLBInfo( 'trxRoundId', null ); // remove the round ID
                }
 
                if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
@@ -1847,20 +1901,16 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function getLaggedReplicaMode( $domain = false ) {
-               if (
-                       // Avoid recursion if there is only one DB
-                       $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
-                       !$this->laggedReplicaMode
-               ) {
+               if ( $this->laggedReplicaMode ) {
+                       return true; // stay in lagged replica mode
+               }
+
+               if ( $this->hasStreamingReplicaServers() ) {
                        try {
-                               // Calling this method will set "laggedReplicaMode" as needed
-                               $this->getReaderIndex( false, $domain );
+                               // Set "laggedReplicaMode"
+                               $this->getReaderIndex( self::GROUP_GENERIC, $domain );
                        } catch ( DBConnectionError $e ) {
-                               // Avoid expensive re-connect attempts and failures
-                               $this->allReplicasDownMode = true;
+                               // Sanity: avoid expensive re-connect attempts and failures
                                $this->laggedReplicaMode = true;
                        }
                }
@@ -1884,16 +1934,12 @@ class LoadBalancer implements ILoadBalancer {
        public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) {
                if ( $this->readOnlyReason !== false ) {
                        return $this->readOnlyReason;
-               } elseif ( $this->getLaggedReplicaMode( $domain ) ) {
-                       if ( $this->allReplicasDownMode ) {
-                               return 'The database has been automatically locked ' .
-                                       'until the replica database servers become available';
-                       } else {
-                               return 'The database has been automatically locked ' .
-                                       'while the replica database servers catch up to the master.';
-                       }
                } elseif ( $this->masterRunningReadOnly( $domain, $conn ) ) {
-                       return 'The database master is running in read-only mode.';
+                       return 'The master database server is running in read-only mode.';
+               } elseif ( $this->getLaggedReplicaMode( $domain ) ) {
+                       return ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 )
+                               ? 'The database is read-only until replication lag decreases.'
+                               : 'The database is read-only until a replica database server becomes reachable.';
                }
 
                return false;
@@ -1914,7 +1960,8 @@ class LoadBalancer implements ILoadBalancer {
                        function () use ( $domain, $conn ) {
                                $old = $this->trxProfiler->setSilenced( true );
                                try {
-                                       $dbw = $conn ?: $this->getConnection( self::DB_MASTER, [], $domain );
+                                       $index = $this->getWriterIndex();
+                                       $dbw = $conn ?: $this->getServerConnection( $index, $domain );
                                        $readOnly = (int)$dbw->serverIsReadOnly();
                                        if ( !$conn ) {
                                                $this->reuseConnection( $dbw );
@@ -1923,6 +1970,7 @@ class LoadBalancer implements ILoadBalancer {
                                        $readOnly = 0;
                                }
                                $this->trxProfiler->setSilenced( $old );
+
                                return $readOnly;
                        },
                        [ 'pcTTL' => $cache::TTL_PROC_LONG, 'busyValue' => 0 ]
@@ -2006,7 +2054,7 @@ class LoadBalancer implements ILoadBalancer {
                if ( $this->hasReplicaServers() ) {
                        $lagTimes = $this->getLagTimes( $domain );
                        foreach ( $lagTimes as $i => $lag ) {
-                               if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
+                               if ( $this->groupLoads[self::GROUP_GENERIC][$i] > 0 && $lag > $maxLag ) {
                                        $maxLag = $lag;
                                        $host = $this->getServerInfoStrict( $i, 'host' );
                                        $maxIndex = $i;
@@ -2070,12 +2118,12 @@ class LoadBalancer implements ILoadBalancer {
                if ( !$pos ) {
                        // Get the current master position, opening a connection if needed
                        $index = $this->getWriterIndex();
-                       $masterConn = $this->getAnyOpenConnection( $index );
+                       $flags = self::CONN_SILENCE_ERRORS;
+                       $masterConn = $this->getAnyOpenConnection( $index, $flags );
                        if ( $masterConn ) {
                                $pos = $masterConn->getMasterPos();
                        } else {
-                               $flags = self::CONN_SILENCE_ERRORS;
-                               $masterConn = $this->getConnection( $index, [], self::DOMAIN_ANY, $flags );
+                               $masterConn = $this->getServerConnection( $index, self::DOMAIN_ANY, $flags );
                                if ( !$masterConn ) {
                                        throw new DBReplicationWaitError(
                                                null,