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

Skip to content

Commit 023911c

Browse files
committed
add support for redis cluster
1 parent c3ec061 commit 023911c

File tree

9 files changed

+172
-26
lines changed

9 files changed

+172
-26
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: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
5252
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
5353
throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
5454
}
55-
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy) {
55+
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy && !$redisClient instanceof RedisClusterProxy) {
5656
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)));
5757
}
5858
$this->redis = $redisClient;
@@ -74,57 +74,59 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
7474
*
7575
* @throws InvalidArgumentException when the DSN is invalid
7676
*
77-
* @return \Redis|\Predis\Client According to the "class" option
77+
* @return \Redis|\RedisCluster|\Predis\Client According to the "class" option
7878
*/
7979
public static function createConnection($dsn, array $options = array())
8080
{
81-
if (0 !== strpos($dsn, 'redis://')) {
82-
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
81+
if (0 !== strpos($dsn, 'redis://') && 0 !== strpos($dsn, 'redis+cluster://')) {
82+
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://" or "redis+cluster://"', $dsn));
8383
}
84-
$params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
85-
if (isset($m[1])) {
86-
$auth = $m[1];
87-
}
8884

89-
return 'file://';
90-
}, $dsn);
91-
if (false === $params = parse_url($params)) {
85+
if (false === $params = parse_url($dsn)) {
9286
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
9387
}
88+
9489
if (!isset($params['host']) && !isset($params['path'])) {
9590
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
9691
}
92+
93+
$auth = $params['password'] ?? $params['user'] ?? null;
94+
$scheme = isset($params['host']) ? 'tcp' : 'unix';
95+
9796
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
9897
$params['dbindex'] = $m[1];
9998
$params['path'] = substr($params['path'], 0, -\strlen($m[0]));
10099
}
101-
if (isset($params['host'])) {
102-
$scheme = 'tcp';
103-
} else {
104-
$scheme = 'unix';
105-
}
100+
106101
$params += array(
107-
'host' => isset($params['host']) ? $params['host'] : $params['path'],
102+
'host' => $params['path'] ?? '',
108103
'port' => isset($params['host']) ? 6379 : null,
109104
'dbindex' => 0,
110105
);
106+
111107
if (isset($params['query'])) {
112108
parse_str($params['query'], $query);
113109
$params += $query;
114110
}
111+
115112
$params += $options + self::$defaultConnectionOptions;
116113
if (null === $params['class'] && !\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
117114
throw new CacheException(sprintf('Cannot find the "redis" extension, and "predis/predis" is not installed: %s', $dsn));
118115
}
119-
$class = null === $params['class'] ? (\extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
116+
117+
if (null === $params['class'] && \extension_loaded('redis')) {
118+
$class = 'redis+cluster' === $params['scheme'] ? \RedisCluster::class : \Redis::class;
119+
} else {
120+
$class = null === $params['class'] ? \Predis\Client::class : $params['class'];
121+
}
120122

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

125127
$initializer = function ($redis) use ($connect, $params, $dsn, $auth) {
126128
try {
127-
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
129+
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']);
128130
} catch (\RedisException $e) {
129131
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn));
130132
}
@@ -160,6 +162,24 @@ public static function createConnection($dsn, array $options = array())
160162
} else {
161163
$initializer($redis);
162164
}
165+
} elseif (is_a($class, \RedisCluster::class, true)) {
166+
$initializer = function () use ($class, $params, $dsn, $auth) {
167+
$host = $params['host'];
168+
if (isset($params['port'])) {
169+
$host .= ':'.$params['port'];
170+
}
171+
172+
try {
173+
/** @var \RedisCluster $redis */
174+
$redis = new $class(null, explode(',', $host), $params['timeout'], $params['read_timeout'], (bool) $params['persistent']);
175+
} catch (\RedisClusterException $e) {
176+
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn));
177+
}
178+
179+
return $redis;
180+
};
181+
182+
$redis = $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer();
163183
} elseif (is_a($class, \Predis\Client::class, true)) {
164184
$params['scheme'] = $scheme;
165185
$params['database'] = $params['dbindex'] ?: null;
@@ -210,6 +230,11 @@ protected function doFetch(array $ids)
210230
*/
211231
protected function doHave($id)
212232
{
233+
if (!\is_string($id)) {
234+
// SEGFAULT on \RedisCluster
235+
return false;
236+
}
237+
213238
return (bool) $this->redis->exists($id);
214239
}
215240

@@ -244,6 +269,7 @@ protected function doClear($namespace)
244269
$h->connect($host[0], $host[1]);
245270
}
246271
}
272+
247273
foreach ($hosts as $host) {
248274
if (!isset($namespace[0])) {
249275
$cleared = $host->flushDb() && $cleared;
@@ -330,7 +356,7 @@ private function pipeline(\Closure $generator)
330356
{
331357
$ids = array();
332358

333-
if ($this->redis instanceof \RedisCluster || ($this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof RedisCluster)) {
359+
if ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster || ($this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof RedisCluster)) {
334360
// phpredis & predis don't support pipelining with RedisCluster
335361
// see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
336362
// 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
}

src/Symfony/Component/Lock/Store/RedisStore.php

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

1212
namespace Symfony\Component\Lock\Store;
1313

14+
use Symfony\Component\Cache\Traits\RedisClusterProxy;
1415
use Symfony\Component\Cache\Traits\RedisProxy;
1516
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1617
use Symfony\Component\Lock\Exception\LockConflictedException;
@@ -130,7 +131,12 @@ public function exists(Key $key)
130131
*/
131132
private function evaluate(string $script, string $resource, array $args)
132133
{
133-
if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster || $this->redis instanceof RedisProxy) {
134+
if (
135+
$this->redis instanceof \Redis ||
136+
$this->redis instanceof \RedisCluster ||
137+
$this->redis instanceof RedisProxy ||
138+
$this->redis instanceof RedisClusterProxy
139+
) {
134140
return $this->redis->eval($script, array_merge(array($resource), $args), 1);
135141
}
136142

src/Symfony/Component/Lock/Store/StoreFactory.php

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

1212
namespace Symfony\Component\Lock\Store;
1313

14+
use Symfony\Component\Cache\Traits\RedisClusterProxy;
1415
use Symfony\Component\Cache\Traits\RedisProxy;
1516
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1617

@@ -28,7 +29,14 @@ class StoreFactory
2829
*/
2930
public static function createStore($connection)
3031
{
31-
if ($connection instanceof \Redis || $connection instanceof \RedisArray || $connection instanceof \RedisCluster || $connection instanceof \Predis\Client || $connection instanceof RedisProxy) {
32+
if (
33+
$connection instanceof \Redis ||
34+
$connection instanceof \RedisArray ||
35+
$connection instanceof \RedisCluster ||
36+
$connection instanceof \Predis\Client ||
37+
$connection instanceof RedisProxy ||
38+
$connection instanceof RedisClusterProxy
39+
) {
3240
return new RedisStore($connection);
3341
}
3442
if ($connection instanceof \Memcached) {

0 commit comments

Comments
 (0)