8000 [Cache] Add \Relay\Cluster support to RedisAdapter · symfony/symfony@ddb8ecd · GitHub
[go: up one dir, main page]

Skip to content

Commit ddb8ecd

Browse files
committed
[Cache] Add \Relay\Cluster support to RedisAdapter
1 parent 0a10839 commit ddb8ecd

File tree

8 files changed

+1429
-14
lines changed

8 files changed

+1429
-14
lines changed

src/Symfony/Component/Cache/Adapter/RedisAdapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class RedisAdapter extends AbstractAdapter
1818
{
1919
use RedisTrait;
2020

21-
public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
21+
public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay|\Relay\Cluster $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
2222
{
2323
$this->init($redis, $namespace, $defaultLifetime, $marshaller);
2424
}

src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function __construct(
6969
throw new InvalidArgumentException(\sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
7070
}
7171

72-
$isRelay = $redis instanceof Relay;
72+
$isRelay = $redis instanceof Relay || $redis instanceof \Relay\Cluster;
7373
if ($isRelay || \defined('Redis::OPT_COMPRESSION') && \in_array($redis::class, [\Redis::class, \RedisArray::class, \RedisCluster::class], true)) {
7474
$compression = $redis->getOption($isRelay ? Relay::OPT_COMPRESSION : \Redis::OPT_COMPRESSION);
7575

@@ -225,7 +225,7 @@ protected function doInvalidate(array $tagIds): bool
225225
$results = $this->pipeline(function () use ($tagIds, $lua) {
226226
if ($this->redis instanceof \Predis\ClientInterface) {
227227
$prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
228-
} elseif (\is_array($prefix = $this->redis->getOption($this->redis instanceof Relay ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
228+
} elseif (\is_array($prefix = $this->redis->getOption(($this->redis instanceof Relay || $this->redis instanceof \Relay\Cluster) ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
229229
$prefix = current($prefix);
230230
}
231231

src/Symfony/Component/Cache/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
* Added support for `\Relay\Cluster` in `RedisAdapter`
7+
48
7.2
59
---
610

src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Cache\CacheItemPoolInterface;
1515
use Relay\Relay;
16+
use Relay\Cluster as RelayCluster;
1617
use Symfony\Component\Cache\Adapter\RedisAdapter;
1718

1819
abstract class AbstractRedisAdapterTestCase extends AdapterTestCase
@@ -23,7 +24,7 @@ abstract class AbstractRedisAdapterTestCase extends AdapterTestCase
2324
'testDefaultLifeTime' => 10000 ; 'Testing expiration slows down the test suite',
2425
];
2526

26-
protected static \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
27+
protected static \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
2728

2829
public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
2930
{
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Tests\Adapter;
13+
14+
use Relay\Relay;
15+
use Relay\Cluster as RelayCluster;
16+
use Psr\Cache\CacheItemPoolInterface;
17+
use Symfony\Component\Cache\Adapter\AbstractAdapter;
18+
use Symfony\Component\Cache\Adapter\RedisAdapter;
19+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
20+
use Symfony\Component\Cache\Traits\RelayClusterProxy;
21+
22+
/**
23+
* @requires extension relay
24+
*
25+
* @group integration
26+
*/
27+
class RelayClusterAdapterTest extends AbstractRedisAdapterTestCase
28+
{
29+
public static function setUpBeforeClass(): void
30+
{
31+
if (!class_exists(RelayCluster::class)) {
32+
self::markTestSkipped('The Relay\Cluster class is required.');
33+
}
34+
if (!$hosts = getenv('REDIS_CLUSTER_HOSTS')) {
35+
self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.');
36+
}
37+
38+
self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['lazy' => true, 'relay_cluster' => true]);
39+
self::$redis->setOption(Relay::OPT_PREFIX, 'prefix_');
40+
}
41+
42+
public function createCachePool(int $defaultLifetime = 0, ?string $testMethod = null): CacheItemPoolInterface
43+
{
44+
$this->assertInstanceOf(RelayClusterProxy::class, self::$redis);
45+
$adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
46+
47+
return $adapter;
48+
}
49+
50+
/**
51+
* @dataProvider provideFailedCreateConnection
52+
*/
53+
public function testFailedCreateConnection(string $dsn)
54+
{
55+
$this->expectException(InvalidArgumentException::class);
56+
$this->expectExceptionMessage('Relay cluster connection failed:');
57+
RedisAdapter::createConnection($dsn);
58+
}
59+
60+
public static function provideFailedCreateConnection(): array
61+
{
62+
return [
63+
['redis://localhost:1234?relay_cluster=1'],
64+
['redis://foo@localhost?relay_cluster=1'],
65+
['redis://localhost/123?relay_cluster=1'],
66+
];
67+
}
68+
}

src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Relay\Relay;
16+
use Relay\Cluster as RelayCluster;
1617
use Symfony\Component\Cache\Traits\RedisProxyTrait;
18+
use Symfony\Component\Cache\Traits\RelayClusterProxy;
1719
use Symfony\Component\Cache\Traits\RelayProxy;
1820
use Symfony\Component\VarExporter\LazyProxyTrait;
1921
use Symfony\Component\VarExporter\ProxyHelper;
@@ -121,4 +123,52 @@ public function testRelayProxy()
121123

122124
$this->assertEquals($expectedProxy, $proxy);
123125
}
126+
127+
128+
/**
129+
* @requires extension relay
130+
*/
131+
public function testRelayClusterProxy()
132+
{
133+
$proxy = file_get_contents(\dirname(__DIR__, 2).'/Traits/RelayClusterProxy.php');
134+
$proxy = substr($proxy, 0, 2 + strpos($proxy, '}'));
135+
$expectedProxy = $proxy;
136+
$methods = [];
137+
$expectedMethods = [];
138+
139+
foreach ((new \ReflectionClass(RelayClusterProxy::class))->getMethods() as $method) {
140 F438 +
if ('reset' === $method->name || method_exists(LazyProxyTrait::class, $method->name) || $method->isStatic()) {
141+
continue;
142+
}
143+
144+
$return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
145+
$expectedMethods[$method->name] = "\n ".ProxyHelper::exportSignature($method, false, $args)."\n".<<<EOPHP
146+
{
147+
{$return}\$this->initializeLazyObject()->{$method->name}({$args});
148+
}
149+
150+
EOPHP;
151+
}
152+
153+
foreach ((new \ReflectionClass(RelayCluster::class))->getMethods() as $method) {
154+
if ('reset' === $method->name || method_exists(RedisProxyTrait::class, $method->name) || $method->isStatic()) {
155+
continue;
156+
}
157+
$return = '__construct' === $method->name || $method->getReturnType() instanceof \ReflectionNamedType && 'void' === (string) $method->getReturnType() ? '' : 'return ';
158+
$methods[$method->name] = "\n ".ProxyHelper::exportSignature($method, false, $args)."\n".<<<EOPHP
159+
{
160+
{$return}\$this->initializeLazyObject()->{$method->name}({$args});
161+
}
162+
163+
EOPHP;
164+
}
165+
166+
uksort($methods, 'strnatcmp');
167+
$proxy .= implode('', $methods)."}\n";
168+
169+
uksort($expectedMethods, 'strnatcmp');
170+
$expectedProxy .= implode('', $expectedMethods)."}\n";
171+
172+
$this->assertEquals($expectedProxy, $proxy);
173+
}
124174
}

src/Symfony/Component/Cache/Traits/RedisTrait.php

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Predis\Response\ErrorInterface;
2222
use Predis\Response\Status;
2323
use Relay\Relay;
24+
use Relay\Cluster as RelayCluster;
2425
use Relay\Sentinel;
2526
use Symfony\Component\Cache\Exception\CacheException;
2627
use Symfony\Component\Cache\Exception\InvalidArgumentException;
@@ -41,19 +42,21 @@ trait RedisTrait
4142
'persistent_id' => null,
4243
'timeout' => 30,
4344
'read_timeout' => 0,
45+
'command_timeout' => 0,
4446
'retry_interval' => 0,
4547
'tcp_keepalive' => 0,
4648
'lazy' => null,
4749
'redis_cluster' => false,
50+
'relay_cluster' => false,
4851
'redis_sentinel' => null,
4952
'dbindex' => 0,
5053
'failover' => 'none',
5154
'ssl' => null, // see https://php.net/context.ssl
5255
];
53-
private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
56+
private \Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis;
5457
private MarshallerInterface $marshaller;
5558

56-
private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
59+
private function init(\Redis|Relay|RelayCluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller): void
5760
{
5861
parent::__construct($namespace, $defaultLifetime);
5962

@@ -85,7 +88,7 @@ private function init(\Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInter
8588
*
8689
* @throws InvalidArgumentException when the DSN is invalid
8790
*/
88-
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay
91+
public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay|RelayCluster
8992
{
9093
if (str_starts_with($dsn, 'redis:')) {
9194
$scheme = 'redis';
@@ -188,9 +191,20 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
188191
$params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
189192
}
190193
$params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
191-
192-
if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
193-
throw new InvalidArgumentException('Cannot use both "redis_cluster" and "redis_sentinel" at the same time.');
194+
$params['relay_cluster'] = filter_var($params['relay_cluster'], \FILTER_VALIDATE_BOOLEAN);
195+
196+
$conflictingOptions = array_filter([
197+
'redis_cluster' => $params['redis_cluster'],
198+
'relay_cluster' => $params['relay_cluster'],
199+
'redis_sentinel' => $params['redis_sentinel'],
200+
], fn ($value) => $value === true);
201+
202+
if (count($conflictingOptions) > 1) {
203+
$keys = implode('", "', array_keys($conflictingOptions));
204+
throw new InvalidArgumentException(sprintf(
205+
'Cannot use %s at the same time. Please configure only one of "redis_cluster", "relay_cluster", or "redis_sentinel".',
206+
$keys
207+
));
194208
}
195209

196210
$class = $params['class'] ?? match (true) {
@@ -200,9 +214,9 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
200214
\extension_loaded('relay') => Relay::class,
201215
default => \Predis\Client::class,
202216
},
203-
1 < \count($hosts) && \extension_loaded('redis') => \RedisArray::class,
204-
\extension_loaded('redis') => \Redis::class,
205-
\extension_loaded('relay') => Relay::class,
217+
$params['relay_cluster'] === false && 1 < \count($hosts) && \extension_loaded('redis') => \RedisArray::class,
218+
$params['relay_cluster'] === false && \extension_loaded('redis') => \Redis::class,
219+
\extension_loaded('relay') => $params['relay_cluster'] === true ? RelayCluster::class : Relay::class,
206220
default => \Predis\Client::class,
207221
};
208222

@@ -348,6 +362,46 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
348362
if (0 < $params['tcp_keepalive'] && (!$isRedisExt || \defined('Redis::OPT_TCP_KEEPALIVE'))) {
349363
$redis->setOption($isRedisExt ? \Redis::OPT_TCP_KEEPALIVE : Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
350364
}
365+
} elseif (is_a($class, RelayCluster::class, true)) {
366+
if (version_compare(phpversion('relay'), '0.10.0', '<')) {
367+
throw new InvalidArgumentException('Using RelayCluster is supported from ext-relay 0.10.0 or higher.');
368+
}
369+
370+
$initializer = static function () use ($class, $params, $hosts) {
371+
foreach ($hosts as $i => $host) {
372+
$hosts[$i] = match ($host['scheme']) {
373+
'tcp' => $host['host'].':'.$host['port'],
374+
'tls' => 'tls://'.$host['host'].':'.$host['port'],
375+
default => $host['path'],
376+
};
377+
}
378+
379+
try {
380+
$relayCluster = new $class(
381+
name: null,
382+
seeds: $hosts,
383+
connect_timeout: $params['timeout'],
384+
command_timeout: $params['command_timeout'],
385+
persistent: (bool) $params['persistent'],
386+
auth: $params['auth'] ?? null,
387+
context: []
388+
);
389+
} catch (\Relay\Exception $e) {
390+
throw new InvalidArgumentException('Relay cluster connection failed: '.$e->getMessage());
391+
}
392+
393+
if (0 < $params['tcp_keepalive']) {
394+
$relayCluster->setOption(Relay::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']);
395+
}
396+
397+
if (0 < $params['read_timeout']) {
398+
$relayCluster->setOption(Relay::OPT_READ_TIMEOUT, $params['read_timeout']);
399+
}
400+
401+
return $relayCluster;
402+
};
403+
404+
$redis = $params['lazy'] ? RelayClusterProxy::createLazyProxy($initializer) : $initializer();
351405
} elseif (is_a($class, \RedisCluster::class, true)) {
352406
$initializer = static function () use ($isRedisExt, $class, $params, $hosts) {
353407
foreach ($hosts as $i => $host) {
@@ -478,6 +532,40 @@ protected function doClear(string $namespace): bool
478532
}
479533

480534
$cleared = true;
535+
536+
if ($this->redis instanceof RelayCluster) {
537+
$prefix = Relay::SCAN_PREFIX & $this->redis->getOption(Relay::OPT_SCAN) ? '' : $this->redis->getOption(Relay::OPT_PREFIX);
538+
$prefixLen = \strlen($prefix);
539+
$pattern = $prefix.$namespace.'*';
540+
foreach ($this->redis->_masters() as $ipAndPort) {
541+
$address = implode(':', $ipAndPort);
542+
$cursor = null;
543+
do {
544+
// mixed &$iterator
545+
// array|string $key_or_address
546+
// mixed $match = null
547+
// int $count = 0
548+
// string|null $type = null
549+
$keys = $this->redis->scan($cursor, $address, $pattern, 1000);
550+
if (isset($keys[1]) && \is_array($keys[1])) {
551+
$cursor = $keys[0];
552+
$keys = $keys[1];
553+
}
554+
555+
if ($keys) {
556+
if ($prefixLen) {
557+
foreach ($keys as $i => $key) {
558+
$keys[$i] = substr($key, $prefixLen);
559+
}
560+
}
561+
$this->doDelete($keys);
562+
}
563+
} while ($cursor);
564+
}
565+
566+
return $cleared;
567+
}
568+
481569
$hosts = $this->getHosts();
482570
$host = reset($hosts);
483571
if ($host instanceof \Predis\Client) {
@@ -605,8 +693,9 @@ private function pipeline(\Closure $generator, ?object $redis = null): \Generato
605693
$ids = [];
606694
$redis ??= $this->redis;
607695

608-
if ($redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
696+
if ($redis instanceof \RedisCluster || $redis instanceof \Relay\Cluster || ($redis instanceof \Predis\ClientInterface && ($redis->getConnection() instanceof RedisCluster || $redis->getConnection() instanceof Predis2RedisCluster))) {
609697
// phpredis & predis don't support pipelining with RedisCluster
698+
// \Relay\Cluster does not support multi with pipeline mode
610699
// see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
611700
// see https://github.com/nrk/predis/issues/267#issuecomment-123781423
612701
$results = [];

0 commit comments

Comments
 (0)
0