From 9caebe5f164ac0ee195b122b663fe4e5993aedc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renan=20Gon=C3=A7alves?= Date: Mon, 7 Dec 2020 15:08:24 +0100 Subject: [PATCH 1/3] Support Redis Sentinel mode when using phpredis/phpredis extension The version 5.2.0 of the [Redis PHP extension](http://pecl.php.net/package/redis), released back in March 2020, added support for Redis Sentinel mode with the help of the `RedisSentinel` class. Usage of the `Symfony/Cache RedisAdapter` can continue to be the same, thus relying on the `$options['redis_persistent']` option to both enable and define the master name. --- src/Symfony/Component/Cache/CHANGELOG.md | 5 +++ .../Adapter/PredisAdapterSentinelTest.php | 44 +++++++++++++++++++ .../Adapter/RedisAdapterSentinelTest.php | 4 +- .../Component/Cache/Traits/RedisTrait.php | 31 +++++++++---- 4 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 73965c2058903..2b6c4b177cee9 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.3.0 +----- + +* added support for connecting to Redis Sentinel clusters when using the Redis PHP extension + 5.2.0 ----- diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php new file mode 100644 index 0000000000000..bc01737fd3d01 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\RedisAdapter; + +/** + * @group integration + */ +class PredisAdapterSentinelTest extends AbstractRedisAdapterTest +{ + public static function setUpBeforeClass(): void + { + if (!class_exists(\Predis\Client::class)) { + self::markTestSkipped('The Predis\Client class is required.'); + } + if (!$hosts = getenv('REDIS_SENTINEL_HOSTS')) { + self::markTestSkipped('REDIS_SENTINEL_HOSTS env var is not defined.'); + } + if (!$service = getenv('REDIS_SENTINEL_SERVICE')) { + self::markTestSkipped('REDIS_SENTINEL_SERVICE env var is not defined.'); + } + + self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['redis_sentinel' => $service, 'class' => \Predis\Client::class]); + } + + public function testInvalidDSNHasBothClusterAndSentinel() + { + $this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); + $this->expectExceptionMessage('Cannot use both "redis_cluster" and "redis_sentinel" at the same time:'); + $dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&redis_cluster=1&redis_sentinel=mymaster'; + RedisAdapter::createConnection($dsn); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php index 82b9f08b65474..84ea7969ad962 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php @@ -21,8 +21,8 @@ class RedisAdapterSentinelTest extends AbstractRedisAdapterTest { public static function setUpBeforeClass(): void { - if (!class_exists('Predis\Client')) { - self::markTestSkipped('The Predis\Client class is required.'); + if (!class_exists(\RedisSentinel::class)) { + self::markTestSkipped('The RedisSentinel class is required.'); } if (!$hosts = getenv('REDIS_SENTINEL_HOSTS')) { self::markTestSkipped('REDIS_SENTINEL_HOSTS env var is not defined.'); diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 317879aa494e2..ef70bf786e78e 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -159,13 +159,17 @@ public static function createConnection($dsn, array $options = []) throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".', $dsn)); } - if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class)) { - throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package: "%s".', $dsn)); + $params += $query + $options + self::$defaultConnectionOptions; + + if (isset($params['redis_sentinel']) && (!class_exists(\Predis\Client::class) || !class_exists(\RedisSentinel::class))) { + throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package or the "redis" extension with minimum version v5.2.0: "%s".', $dsn)); } - $params += $query + $options + self::$defaultConnectionOptions; + if ($params['redis_cluster'] && isset($params['redis_sentinel'])) { + throw new InvalidArgumentException(sprintf('Cannot use both "redis_cluster" and "redis_sentinel" at the same time: "%s".', $dsn)); + } - if (null === $params['class'] && !isset($params['redis_sentinel']) && \extension_loaded('redis')) { + if (null === $params['class'] && \extension_loaded('redis')) { $class = $params['redis_cluster'] ? \RedisCluster::class : (1 < \count($hosts) ? \RedisArray::class : \Redis::class); } else { $class = null === $params['class'] ? \Predis\Client::class : $params['class']; @@ -176,8 +180,22 @@ public static function createConnection($dsn, array $options = []) $redis = new $class(); $initializer = static function ($redis) use ($connect, $params, $dsn, $auth, $hosts) { + $address = $hosts[0]['host'] ?? $hosts[0]['path']; + $port = $hosts[0]['port'] ?? null; + + if (isset($params['redis_sentinel'])) { + $sentinel = new \RedisSentinel($address, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); + $master = $sentinel->getMasterAddrByName($params['redis_sentinel']); + if (false === $master) { + throw new InvalidArgumentException(sprintf('Failed to retrieve master information from master name "%s" and address "%s:%d".', $params['redis_sentinel'], $address, $port)); + } + + $address = $master[0]; + $port = $master[1]; + } + try { - @$redis->{$connect}($hosts[0]['host'] ?? $hosts[0]['path'], $hosts[0]['port'] ?? null, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); + @$redis->{$connect}($address, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); $isConnected = $redis->isConnected(); @@ -254,9 +272,6 @@ public static function createConnection($dsn, array $options = []) } elseif (is_a($class, \Predis\ClientInterface::class, true)) { if ($params['redis_cluster']) { $params['cluster'] = 'redis'; - if (isset($params['redis_sentinel'])) { - throw new InvalidArgumentException(sprintf('Cannot use both "redis_cluster" and "redis_sentinel" at the same time: "%s".', $dsn)); - } } elseif (isset($params['redis_sentinel'])) { $params['replication'] = 'sentinel'; $params['service'] = $params['redis_sentinel']; From ba6bb87402aaec6555c25cc7a79266195f72baf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renan=20Gon=C3=A7alves?= Date: Tue, 8 Dec 2020 10:41:27 +0100 Subject: [PATCH 2/3] fixup! Support Redis Sentinel mode when using phpredis/phpredis extension --- .../Tests/Adapter/PredisAdapterSentinelTest.php | 9 --------- .../Component/Cache/Traits/RedisTrait.php | 17 +++++++---------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php index bc01737fd3d01..e6de9b3ee6bbe 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterSentinelTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Symfony\Component\Cache\Adapter\AbstractAdapter; -use Symfony\Component\Cache\Adapter\RedisAdapter; /** * @group integration @@ -33,12 +32,4 @@ public static function setUpBeforeClass(): void self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['redis_sentinel' => $service, 'class' => \Predis\Client::class]); } - - public function testInvalidDSNHasBothClusterAndSentinel() - { - $this->expectException('Symfony\Component\Cache\Exception\InvalidArgumentException'); - $this->expectExceptionMessage('Cannot use both "redis_cluster" and "redis_sentinel" at the same time:'); - $dsn = 'redis:?host[redis1]&host[redis2]&host[redis3]&redis_cluster=1&redis_sentinel=mymaster'; - RedisAdapter::createConnection($dsn); - } } diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index ef70bf786e78e..f0a61292be3ee 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -162,7 +162,7 @@ public static function createConnection($dsn, array $options = []) $params += $query + $options + self::$defaultConnectionOptions; if (isset($params['redis_sentinel']) && (!class_exists(\Predis\Client::class) || !class_exists(\RedisSentinel::class))) { - throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package or the "redis" extension with minimum version v5.2.0: "%s".', $dsn)); + throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package or the "redis" extension v5.2 or higher: "%s".', $dsn)); } if ($params['redis_cluster'] && isset($params['redis_sentinel'])) { @@ -180,22 +180,19 @@ public static function createConnection($dsn, array $options = []) $redis = new $class(); $initializer = static function ($redis) use ($connect, $params, $dsn, $auth, $hosts) { - $address = $hosts[0]['host'] ?? $hosts[0]['path']; + $host = $hosts[0]['host'] ?? $hosts[0]['path']; $port = $hosts[0]['port'] ?? null; if (isset($params['redis_sentinel'])) { - $sentinel = new \RedisSentinel($address, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); - $master = $sentinel->getMasterAddrByName($params['redis_sentinel']); - if (false === $master) { - throw new InvalidArgumentException(sprintf('Failed to retrieve master information from master name "%s" and address "%s:%d".', $params['redis_sentinel'], $address, $port)); - } + $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); - $address = $master[0]; - $port = $master[1]; + if (!([$host, $port] = $sentinel->getMasterAddrByName($params['redis_sentinel']))) { + throw new InvalidArgumentException(sprintf('Failed to retrieve master information from master name "%s" and address "%s:%d".', $params['redis_sentinel'], $host, $port)); + } } try { - @$redis->{$connect}($address, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); + @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); $isConnected = $redis->isConnected(); From c3086f77d481c179e552033b43f6ae46aec09b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renan=20Gon=C3=A7alves?= Date: Tue, 8 Dec 2020 11:32:59 +0100 Subject: [PATCH 3/3] fixup! Support Redis Sentinel mode when using phpredis/phpredis extension --- src/Symfony/Component/Cache/Traits/RedisTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index f0a61292be3ee..6e6b6d0e4f6c6 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -186,7 +186,7 @@ public static function createConnection($dsn, array $options = []) if (isset($params['redis_sentinel'])) { $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']); - if (!([$host, $port] = $sentinel->getMasterAddrByName($params['redis_sentinel']))) { + if (![$host, $port] = $sentinel->getMasterAddrByName($params['redis_sentinel'])) { throw new InvalidArgumentException(sprintf('Failed to retrieve master information from master name "%s" and address "%s:%d".', $params['redis_sentinel'], $host, $port)); } }