Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ddb8ecd

Browse filesBrowse files
committed
[Cache] Add \Relay\Cluster support to RedisAdapter
1 parent 0a10839 commit ddb8ecd
Copy full SHA for ddb8ecd

File tree

Expand file treeCollapse file tree

8 files changed

+1429
-14
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+1429
-14
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/RedisAdapter.php
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php
+2-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/CHANGELOG.md
+4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php
+2-1Lines 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' => '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
{
+68Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Tests/Traits/RedisProxiesTest.php
+50Lines 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+
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Traits/RedisTrait.php
+99-10Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.