8000 add support for redis cluster · symfony/symfony@a250c51 · GitHub
[go: up one dir, main page]

Skip to content

Commit a250c51

Browse files
committed
add support for redis cluster
1 parent eb607ae commit a250c51

File tree

8 files changed

+171
-25
lines changed

8 files changed

+171
-25
lines changed

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 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/PredisAdapterTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function testCreateConnection()
4747
'compression' => true,
4848
'tcp_keepalive' => 0,
4949
'lazy' => false,
50+
'cluster' => null,
5051
'database' => '1',
5152
'password' => null,
5253
);

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://'.str_replace(' ', ',', $hosts), array('lazy' => true, 'cluster' => 'server'));
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://'.$hosts.'?cluster=server');
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://localhost:1234?cluster=server'),
62+
array('redis://foo@localhost?cluster=server'),
63+
array('redis://localhost/123?cluster=server'),
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 <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\Traits;
13+
14+
/**
15+
* @author Alessandro Chitolina <alekitto@gmail.com>
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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ trait RedisTrait
3838
'compression' => true,
3939
'tcp_keepalive' => 0,
4040
'lazy' => false,
41+
'cluster' => null,
4142
);
4243
private $redis;
4344
private $marshaller;
@@ -52,7 +53,7 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
5253
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
5354
throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
5455
}
55-
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy) {
56+
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy && !$redisClient instanceof RedisClusterProxy) {
5657
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)));
5758
}
5859
$this->redis = $redisClient;
@@ -74,57 +75,59 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
7475
*
7576
* @throws InvalidArgumentException when the DSN is invalid
7677
*
77-
* @return \Redis|\Predis\Client According to the "class" option
78+
* @return \Redis|\RedisCluster|\Predis\Client According to the "class" option
7879
*/
7980
public static function createConnection($dsn, array $options = array())
8081
{
8182
if (0 !== strpos($dsn, 'redis://')) {
8283
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
8384
}
84-
$params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
85-
if (isset($m[1])) {
86-
$auth = $m[1];
87-
}
8885

89-
return 'file://';
90-
}, $dsn);
91-
if (false === $params = parse_url($params)) {
86+
if (false === $params = parse_url($dsn)) {
9287
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
9388
}
89+
9490
if (!isset($params['host']) && !isset($params['path'])) {
9591
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
9692
}
93+
94+
$auth = $params['password'] ?? $params['user'] ?? null;
95+
$scheme = isset($params['host']) ? 'tcp' : 'unix';
96+
9797
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
9898
$params['dbindex'] = $m[1];
9999
$params['path'] = substr($params['path'], 0, -\strlen($m[0]));
100100
}
101-
if (isset($params['host'])) {
102-
$scheme = 'tcp';
103-
} else {
104-
$scheme = 'unix';
105-
}
101+
106102
$params += array(
107-
'host' => isset($params['host']) ? $params['host'] : $params['path'],
103+
'host' => $params['path'] ?? '',
108104
'port' => isset($params['host']) ? 6379 : null,
109105
'dbindex' => 0,
110106
);
107+
111108
if (isset($params['query'])) {
112109
parse_str($params['query'], $query);
113110
$params += $query;
114111
}
112+
115113
$params += $options + self::$defaultConnectionOptions;
116114
if (null === $params['class'] && !\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
117115
throw new CacheException(sprintf('Cannot find the "redis" extension, and "predis/predis" is not installed: %s', $dsn));
118116
}
119-
$class = null === $params['class'] ? (\extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
117+
118+
if (null === $params['class'] && \extension_loaded('redis')) {
119+
$class = 'server' === $params['cluster'] ? \RedisCluster::class : \Redis::class;
120+
} else {
121+
$class = null === $params['class'] ? \Predis\Client::class : $params['class'];
122+
}
120123

121124
if (is_a($class, \Redis::class, true)) {
122125
$connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
123126
$redis = new $class();
124127

125128
$initializer = function ($redis) use ($connect, $params, $dsn, $auth) {
126129
try {
127-
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
130+
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']);
128131
} catch (\RedisException $e) {
129132
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn));
130133
}
@@ -160,6 +163,24 @@ public static function createConnection($dsn, array $options = array())
160163
} else {
161164
$initializer($redis);
162165
}
166+
} elseif (is_a($class, \RedisCluster::class, true)) {
167+
$initializer = function () use ($class, $params, $dsn, $auth) {
168+
$host = $params['host'];
169+
if (isset($params['port'])) {
170+
$host .= ':'.$params['port'];
171+
}
172+
173+
try {
174+
/** @var \RedisCluster $redis */
175+
$redis = new $class(null, explode(',', $host), $params['timeout'], $params['read_timeout'], (bool) $params['persistent']);
176+
} catch (\RedisClusterException $e) {
177+
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e->getMessage(), $dsn));
178+
}
179+
180+
return $redis;
181+
};
182+
183+
$redis = $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer();
163184
} elseif (is_a($class, \Predis\Client::class, true)) {
164185
$params['scheme'] = $scheme;
165186
$params['database'] = $params['dbindex'] ?: null;
@@ -183,9 +204,7 @@ protected function doFetch(array $ids)
183204
return array();
184205
}
185206

186-
$i = -1;
187207
$result = array();
188-
189208
if ($this->redis instanceof \Predis\Client) {
190209
$values = $this->pipeline(function () use ($ids) {
191210
foreach ($ids as $id) {
@@ -210,6 +229,11 @@ protected function doFetch(array $ids)
210229
*/
211230
protected function doHave($id)
212231
{
232+
if (!\is_string($id)) {
233+
// SEGFAULT on \RedisCluster
234+
return false;
235+
}
236+
213237
return (bool) $this->redis->exists($id);
214238
}
215239

@@ -237,13 +261,14 @@ protected function doClear($namespace)
237261
foreach ($this->redis->_hosts() as $host) {
238262
$hosts[] = $this->redis->_instance($host);
239263
}
240-
} elseif ($this->redis instanceof \RedisCluster) {
264+
} elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) {
241265
$hosts = array();
242266
foreach ($this->redis->_masters() as $host) {
243267
$hosts[] = $h = new \Redis();
244268
$h->connect($host[0], $host[1]);
245269
}
246270
}
271+
247272
foreach ($hosts as $host) {
248273
if (!isset($namespace[0])) {
249274
$cleared = $host->flushDb() && $cleared;
@@ -330,7 +355,7 @@ private function pipeline(\Closure $generator)
330355
{
331356
$ids = array();
332357

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