diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 29b9b8ec62409..5c2839d74f818 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -81,6 +81,17 @@ jobs: REDIS_MASTER_HOST: redis REDIS_MASTER_SET: redis_sentinel REDIS_SENTINEL_QUORUM: 1 + redis-primary: + image: redis:latest + hostname: redis-primary + ports: + - 16381:6379 + + redis-replica: + image: redis:latest + ports: + - 16382:6379 + command: redis-server --slaveof redis-primary 6379 memcached: image: memcached:1.6.5 ports: @@ -239,6 +250,7 @@ jobs: REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' REDIS_SENTINEL_HOSTS: 'unreachable-host:26379 localhost:26379 localhost:26379' REDIS_SENTINEL_SERVICE: redis_sentinel + REDIS_REPLICATION_HOSTS: 'localhost:16381 localhost:16382' MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages MESSENGER_SQS_DSN: "sqs://localhost:4566/messages?sslmode=disable&poll_timeout=0.01" diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php new file mode 100644 index 0000000000000..552727740c18b --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisReplicationAdapterTest.php @@ -0,0 +1,29 @@ + + * + * 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\RedisAdapter; + +/** + * @group integration + */ +class PredisRedisReplicationAdapterTest extends AbstractRedisAdapterTestCase +{ + public static function setUpBeforeClass(): void + { + if (!$hosts = getenv('REDIS_REPLICATION_HOSTS')) { + self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); + } + + self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][alias]=master', ['class' => \Predis\Client::class, 'prefix' => 'prefix_']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php new file mode 100644 index 0000000000000..4add9d5f18c4a --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php @@ -0,0 +1,24 @@ + + * + * 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; + +/** + * @group integration + */ +class PredisReplicationAdapterTest extends AbstractRedisAdapterTestCase +{ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::$redis = new \Predis\Client(array_combine(['host', 'port'], explode(':', getenv('REDIS_HOST')) + [1 => 6379]), ['prefix' => 'prefix_']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php new file mode 100644 index 0000000000000..4d8651ce4ceb6 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareReplicationAdapterTest.php @@ -0,0 +1,37 @@ + + * + * 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 Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; + +/** + * @group integration + */ +class PredisTagAwareReplicationAdapterTest extends PredisReplicationAdapterTest +{ + use TagAwareTestTrait; + + protected function setUp(): void + { + parent::setUp(); + $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; + } + + public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface + { + $this->assertInstanceOf(\Predis\Client::class, self::$redis); + $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); + + return $adapter; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php new file mode 100644 index 0000000000000..e41745057f141 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisReplicationAdapterTest.php @@ -0,0 +1,65 @@ + + * + * 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 Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\RedisClusterProxy; + +/** + * @group integration + */ +class RedisReplicationAdapterTest extends AbstractRedisAdapterTestCase +{ + public static function setUpBeforeClass(): void + { + if (!$hosts = getenv('REDIS_REPLICATION_HOSTS')) { + self::markTestSkipped('REDIS_REPLICATION_HOSTS env var is not defined.'); + } + + self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).'][alias]=master', ['lazy' => true]); + self::$redis->setOption(\Redis::OPT_PREFIX, 'prefix_'); + } + + public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface + { + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + + $this->assertInstanceOf(RedisClusterProxy::class, self::$redis); + $adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); + + return $adapter; + } + + /** + * @dataProvider provideFailedCreateConnection + */ + public function testFailedCreateConnection(string $dsn) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Redis connection '); + RedisAdapter::createConnection($dsn); + } + + public static function provideFailedCreateConnection(): array + { + return [ + ['redis://localhost:1234'], + ['redis://foo@localhost?role=master'], + ['redis://localhost/123?role=master'], + ]; + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index fc8f5cec60472..2ebaed16f1804 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -17,6 +17,7 @@ use Predis\Connection\Aggregate\ReplicationInterface; use Predis\Connection\Cluster\ClusterInterface as Predis2ClusterInterface; use Predis\Connection\Cluster\RedisCluster as Predis2RedisCluster; +use Predis\Connection\Replication\ReplicationInterface as Predis2ReplicationInterface; use Predis\Response\ErrorInterface; use Predis\Response\Status; use Relay\Relay; @@ -473,9 +474,16 @@ protected function doClear(string $namespace): bool $cleared = true; $hosts = $this->getHosts(); $host = reset($hosts); - if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { - // Predis supports info command only on the master in replication environments - $hosts = [$host->getClientFor('master')]; + if ($host instanceof \Predis\Client) { + $connection = $host->getConnection(); + + if ($connection instanceof ReplicationInterface) { + $hosts = [$host->getClientFor('master')]; + } elseif ($connection instanceof Predis2ReplicationInterface) { + $connection->switchToMaster(); + + $hosts = [$host]; + } } foreach ($hosts as $host) {