Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0e0598d

Browse files
committed
add support for redis cluster
1 parent 4edbd60 commit 0e0598d

File tree

7 files changed

+175
-25
lines changed

7 files changed

+175
-25
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public static function getServiceProvider(ContainerBuilder $container, $name)
133133
{
134134
$container->resolveEnvPlaceholders($name, null, $usedEnvs);
135135

136-
if ($usedEnvs || preg_match('#^[a-z]++://#', $name)) {
136+
if ($usedEnvs || preg_match('#^[a-z\+]++://#', $name)) {
137137
$dsn = $name;
138138

139139
if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public static function createConnection($dsn, array $options = array())
148148
if (!\is_string($dsn)) {
149149
throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, \gettype($dsn)));
150150
}
151-
if (0 === strpos($dsn, 'redis://')) {
151+
if (0 === strpos($dsn, 'redis://') || 0 === strpos($dsn, 'redis+cluster://')) {
152152
return RedisAdapter::createConnection($dsn, $options);
153153
}
154154
if (0 === strpos($dsn, 'memcached://')) {

src/Symfony/Component/Cache/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* added sub-second expiry accuracy for backends that support it
1010
* added support for phpredis 4 `compression` and `tcp_keepalive` options
1111
* added automatic table creation when using Doctrine DBAL with PDO-based backends
12+
* added support for redis cluster via `redis+cluster://` DSN
1213
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
1314
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead
1415
* deprecated the `AbstractAdapter::createSystemCache()` method

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace Symfony\Component\Cache\Tests\Adapter;
1313

14+
use Symfony\Component\Cache\Adapter\AbstractAdapter;
15+
use Symfony\Component\Cache\Adapter\RedisAdapter;
16+
use Symfony\Component\Cache\Traits\RedisClusterProxy;
17+
1418
class RedisClusterAdapterTest extends AbstractRedisAdapterTest
1519
{
1620
public static function setupBeforeClass()
@@ -22,6 +26,41 @@ public static function setupBeforeClass()
2226
self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.');
2327
}
2428

25-
self::$redis = new \RedisCluster(null, explode(' ', $hosts));
29+
self::$redis = AbstractAdapter::createConnection('redis+cluster://'.str_replace(' ', ',', $hosts), array('lazy' => true));
30+
}
31+
32+
public function createCachePool($defaultLifetime = 0)
33+
{
34+
$this->assertInstanceOf(RedisClusterProxy::class, self::$redis);
35+
$adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
36+
37+
return $adapter;
38+
}
39+
40+
public function testCreateConnection()
41+
{
42+
$hosts = str_replace(' ', ',', getenv('REDIS_CLUSTER_HOSTS'));
43+
44+
$redis = RedisAdapter::createConnection('redis+cluster://'.$hosts);
45+
$this->assertInstanceOf(\RedisCluster::class, $redis);
46+
}
47+
48+
/**
49+
* @dataProvider provideFailedCreateConnection
50+
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
51+
* @expectedExceptionMessage Redis connection failed
52+
*/
53+
public function testFailedCreateConnection($dsn)
54+
{
55+
RedisAdapter::createConnection($dsn);
56+
}
57+
58+
public function provideFailedCreateConnection()
59+
{
60+
return array(
61+
array('redis+cluster://localhost:1234'),
62+
array('redis+cluster://foo@localhost'),
63+
array('redis+cluster://localhost/123'),
64+
);
2665
}
2766
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Traits;
13+
14+
/**
15+
* @author Alessandro Chitolina <[email protected]>
16+
*
17+
* @internal
18+
*/
19+
class RedisClusterProxy
20+
{
21+
private $redis;
22+
private $initializer;
23+
24+
public function __construct(\Closure $initializer)
25+
{
26+
$this->redis = null;
27+
$this->initializer = $initializer;
28+
}
29+
30+
public function __call($method, array $args)
31+
{
32+
$this->redis ?: $this->redis = $this->initializer->__invoke();
33+
34+
return \call_user_func_array(array($this->redis, $method), $args);
35+
}
36+
37+
public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
38+
{
39+
$this->redis ?: $this->redis = $this->initializer->__invoke();
40+
41+
return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount);
42+
}
43+
44+
public function scan(&$iIterator, $strPattern = null, $iCount = null)
45+
{
46+
$this->redis ?: $this->redis = $this->initializer->__invoke();
47+
48+
return $this->redis->scan($iIterator, $strPattern, $iCount);
49+
}
50+
51+
public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
52+
{
53+
$this->redis ?: $this->redis = $this->initializer->__invoke();
54+
55+
return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount);
56+
}
57+
58+
public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null)
59+
{
60+
$this->redis ?: $this->redis = $this->initializer->__invoke();
61+
62+
return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount);
63+
}
64+
}

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

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
5555
}
5656
if ($redisClient instanceof \RedisCluster) {
5757
$this->enableVersioning();
58-
} elseif (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy) {
58+
} elseif (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy && !$redisClient instanceof RedisClusterProxy) {
5959
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient)));
6060
}
6161
$this->redis = $redisClient;
@@ -81,53 +81,55 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
8181
*/
8282
public static function createConnection($dsn, array $options = array())
8383
{
84-
if (0 !== strpos($dsn, 'redis://')) {
85-
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
84+
if (0 !== strpos($dsn, 'redis://') && 0 !== strpos($dsn, 'redis+cluster://')) {
85+
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://" or "redis+cluster://"', $dsn));
8686
}
87-
$params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
88-
if (isset($m[1])) {
89-
$auth = $m[1];
90-
}
9187

92-
return 'file://';
93-
}, $dsn);
94-
if (false === $params = parse_url($params)) {
88+
if (false === $params = parse_url($dsn)) {
9589
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
9690
}
91+
9792
if (!isset($params['host']) && !isset($params['path'])) {
9893
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
9994
}
95+
96+
$auth = $params['password'] ?? $params['user'] ?? null;
97+
$scheme = isset($params['host']) ? 'tcp' : 'unix';
98+
10099
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
101100
$params['dbindex'] = $m[1];
102101
$params['path'] = substr($params['path'], 0, -\strlen($m[0]));
103102
}
104-
if (isset($params['host'])) {
105-
$scheme = 'tcp';
106-
} else {
107-
$scheme = 'unix';
108-
}
103+
109104
$params += array(
110-
'host' => isset($params['host']) ? $params['host'] : $params['path'],
105+
'host' => $params['path'] ?? '',
111106
'port' => isset($params['host']) ? 6379 : null,
112107
'dbindex' => 0,
113108
);
109+
114110
if (isset($params['query'])) {
115111
parse_str($params['query'], $query);
116112
$params += $query;
117113
}
114+
118115
$params += $options + self::$defaultConnectionOptions;
119116
if (null === $params['class'] && !\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
120117
throw new CacheException(sprintf('Cannot find the "redis" extension, and "predis/predis" is not installed: %s', $dsn));
121118
}
122-
$class = null === $params['class'] ? (\extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
119+
120+
if (null === $params['class'] && \extension_loaded('redis')) {
121+
$class = 'redis+cluster' === $params['scheme'] ? \RedisCluster::class : \Redis::class;
122+
} else {
123+
$class = null === $params['class'] ? \Predis\Client::class : $params['class'];
124+
}
123125

124126
if (is_a($class, \Redis::class, true)) {
125127
$connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
126128
$redis = new $class();
127129

128130
$initializer = function ($redis) use ($connect, $params, $dsn, $auth) {
129131
try {
130-
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
132+
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']);
131133
} catch (\RedisException $e) {
132134
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn));
133135
}
@@ -163,6 +165,24 @@ public static function createConnection($dsn, array $options = array())
163165
} else {
164166
$initializer($redis);
165167
}
168+
} elseif (is_a($class, \RedisCluster::class, true)) {
169+
$initializer = function () use ($class, $params, $dsn, $auth) {
170+
$host = $params['host'];
171+
if (isset($params['port'])) {
172+
$host .= ':'.$params['port'];
173+
}
174+
175+
try {
176+
/** @var \RedisCluster $redis */
177+
$redis = new $class(null, explode(',', $host), $params['timeout'], $params['read_timeout'], (bool) $params['persistent']);
178+
} catch (\RedisClusterException $e) {
179+
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn));
180+
}
181+
182+
return $redis;
183+
};
184+
185+
$redis = $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer();
166186
} elseif (is_a($class, \Predis\Client::class, true)) {
167187
$params['scheme'] = $scheme;
168188
$params['database'] = $params['dbindex'] ?: null;
@@ -201,6 +221,11 @@ protected function doFetch(array $ids)
201221
*/
202222
protected function doHave($id)
203223
{
224+
if (!\is_string($id)) {
225+
// SEGFAULT on \RedisCluster
226+
return false;
227+
}
228+
204229
return (bool) $this->redis->exists($id);
205230
}
206231

@@ -233,9 +258,28 @@ protected function doClear($namespace)
233258
foreach ($this->redis->_hosts() as $host) {
234259
$hosts[] = $this->redis->_instance($host);
235260
}
236-
} elseif ($this->redis instanceof \RedisCluster) {
237-
return false;
261+
} elseif ($this->redis instanceof \RedisCluster || $this->redis instanceof RedisClusterProxy) {
262+
$keys = array();
263+
264+
foreach ($this->redis->_masters() as $nodeParams) {
265+
$host = new \Redis();
266+
$host->connect($nodeParams[0], $nodeParams[1]);
267+
268+
$cursor = null;
269+
do {
270+
$keys = array_merge($keys, $host->scan($cursor, $namespace.'*', 1000));
271+
if (isset($keys[1]) && \is_array($keys[1])) {
272+
$cursor = $keys[0];
273+
$keys = $keys[1];
274+
}
275+
} while ($cursor = (int) $cursor);
276+
}
277+
278+
$this->redis->del($keys);
279+
280+
return true;
238281
}
282+
239283
foreach ($hosts as $host) {
240284
if (!isset($namespace[0])) {
241285
$cleared = $host->flushDb() && $cleared;
@@ -336,7 +380,7 @@ private function pipeline(\Closure $generator)
336380
foreach ($results as $k => list($h, $c)) {
337381
$results[$k] = $connections[$h][$c];
338382
}
339-
} elseif ($this->redis instanceof \RedisCluster || ($this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof ClusterInterface)) {
383+
} elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster || ($this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof ClusterInterface)) {
340384
// phpredis & predis don't support pipelining with RedisCluster
341385
// see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
342386
// see https://github.com/nrk/predis/issues/267#issuecomment-123781423

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
1313

1414
use Predis\Response\ErrorInterface;
15+
use Symfony\Component\Cache\Traits\RedisClusterProxy;
1516
use Symfony\Component\Cache\Traits\RedisProxy;
1617

1718
/**
@@ -45,7 +46,8 @@ public function __construct($redis, array $options = array())
4546
!$redis instanceof \RedisArray &&
4647
!$redis instanceof \RedisCluster &&
4748
!$redis instanceof \Predis\Client &&
48-
!$redis instanceof RedisProxy
49+
!$redis instanceof RedisProxy &&
50+
!$redis instanceof RedisClusterProxy
4951
) {
5052
throw new \InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, \is_object($redis) ? \get_class($redis) : \gettype($redis)));
5153
}

0 commit comments

Comments
 (0)