diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e780b135c9..bf8a8abf99 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -13,6 +13,7 @@ Yii Framework 2 Change Log - Bug #17960: Fix unsigned primary key type mapping for SQLite (bizley) - Enh #17758: `Query::withQuery()` can be used for CTE (sartor) - Bug #17974: Fix ActiveRelationTrait compatibility with PHP 7.4 (Ximich) +- Bug #17991: Improve `yii\db\Connection` master and slave failover, no connection attempt was made when all servers are marked as unavailable (cebe) - Enh #17993: Added `yii\i18n\Formatter::$currencyDecimalSeparator` to allow setting custom symbols for currency decimal in IntlNumberFormatter (XPOHOC269) - Bug #18000: PK value of Oracle ActiveRecord is missing after save (mankwok) - Enh #18006: Allow SameSite cookie pre PHP 7.3 (scottix) diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 1170bf98a7..9a4ddfeacb 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -332,6 +332,8 @@ class Connection extends Component * the health status of the DB servers specified in [[masters]] and [[slaves]]. * This is used only when read/write splitting is enabled or [[masters]] is not empty. * Set boolean `false` to disabled server status caching. + * @see openFromPoolSequentially() for details about the failover behavior. + * @see serverRetryInterval */ public $serverStatusCache = 'cache'; /** @@ -1099,12 +1101,16 @@ class Connection extends Component /** * Opens the connection to a server in the pool. - * This method implements the load balancing among the given list of the servers. + * + * This method implements load balancing and failover among the given list of the servers. * Connections will be tried in random order. + * For details about the failover behavior, see [[openFromPoolSequentially]]. + * * @param array $pool the list of connection configurations in the server pool * @param array $sharedConfig the configuration common to those given in `$pool`. * @return Connection the opened DB connection, or `null` if no server is available * @throws InvalidConfigException if a configuration does not specify "dsn" + * @see openFromPoolSequentially */ protected function openFromPool(array $pool, array $sharedConfig) { @@ -1114,13 +1120,26 @@ class Connection extends Component /** * Opens the connection to a server in the pool. - * This method implements the load balancing among the given list of the servers. - * Connections will be tried in sequential order. + * + * This method implements failover among the given list of servers. + * Connections will be tried in sequential order. The first successful connection will return. + * + * If [[serverStatusCache]] is configured, this method will cache information about + * unreachable servers and does not try to connect to these for the time configured in [[serverRetryInterval]]. + * This helps to keep the application stable when some servers are unavailable. Avoiding + * connection attempts to unavailable servers saves time when the connection attempts fail due to timeout. + * + * If none of the servers are available the status cache is ignored and connection attempts are made to all + * servers (Since version 2.0.35). This is to avoid downtime when all servers are unavailable for a short time. + * After a successful connection attempt the server is marked as avaiable again. + * * @param array $pool the list of connection configurations in the server pool * @param array $sharedConfig the configuration common to those given in `$pool`. * @return Connection the opened DB connection, or `null` if no server is available * @throws InvalidConfigException if a configuration does not specify "dsn" * @since 2.0.11 + * @see openFromPool + * @see serverStatusCache */ protected function openFromPoolSequentially(array $pool, array $sharedConfig) { @@ -1134,8 +1153,8 @@ class Connection extends Component $cache = is_string($this->serverStatusCache) ? Yii::$app->get($this->serverStatusCache, false) : $this->serverStatusCache; - foreach ($pool as $config) { - $config = array_merge($sharedConfig, $config); + foreach ($pool as $i => $config) { + $pool[$i] = $config = array_merge($sharedConfig, $config); if (empty($config['dsn'])) { throw new InvalidConfigException('The "dsn" option must be specified.'); } @@ -1158,6 +1177,30 @@ class Connection extends Component // mark this server as dead and only retry it after the specified interval $cache->set($key, 1, $this->serverRetryInterval); } + // exclude server from retry below + unset($pool[$i]); + } + } + + if ($cache instanceof CacheInterface) { + // if server status cache is enabled and no server is available + // ignore the cache and try to connect anyway + // $pool now only contains servers we did not already try in the loop above + foreach ($pool as $config) { + + /* @var $db Connection */ + $db = Yii::createObject($config); + try { + $db->open(); + } catch (\Exception $e) { + Yii::warning("Connection ({$config['dsn']}) failed: " . $e->getMessage(), __METHOD__); + continue; + } + + // mark this server as available again after successful connection + $cache->delete([__METHOD__, $config['dsn']]); + + return $db; } }