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 b85ab60

Browse filesBrowse files
committed
feature #18689 [Cache] Add support for Predis, RedisArray and RedisCluster (nicolas-grekas)
This PR was merged into the 3.1-dev branch. Discussion ---------- [Cache] Add support for Predis, RedisArray and RedisCluster | Q | A | ------------- | --- | Branch? | 3.1 ideally | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Commits ------- b004243 [Cache] Add support for Predis, RedisArray and RedisCluster
2 parents 2849654 + b004243 commit b85ab60
Copy full SHA for b85ab60

File tree

7 files changed

+220
-49
lines changed
Filter options

7 files changed

+220
-49
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"doctrine/doctrine-bundle": "~1.4",
8484
"monolog/monolog": "~1.11",
8585
"ocramius/proxy-manager": "~0.4|~1.0|~2.0",
86+
"predis/predis": "~1.0",
8687
"egulias/email-validator": "~1.2",
8788
"symfony/polyfill-apcu": "~1.1",
8889
"symfony/security-acl": "~2.8|~3.0",

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/RedisAdapter.php
+95-22Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,39 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Predis\Connection\Factory;
15+
use Predis\Connection\Aggregate\PredisCluster;
16+
use Predis\Connection\Aggregate\RedisCluster;
1417
use Symfony\Component\Cache\Exception\InvalidArgumentException;
1518

1619
/**
1720
* @author Aurimas Niekis <aurimas@niekis.lt>
21+
* @author Nicolas Grekas <p@tchwork.com>
1822
*/
1923
class RedisAdapter extends AbstractAdapter
2024
{
2125
private static $defaultConnectionOptions = array(
22-
'class' => \Redis::class,
26+
'class' => null,
2327
'persistent' => 0,
2428
'timeout' => 0,
2529
'read_timeout' => 0,
2630
'retry_interval' => 0,
2731
);
2832
private $redis;
2933

30-
public function __construct(\Redis $redisClient, $namespace = '', $defaultLifetime = 0)
34+
/**
35+
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
36+
*/
37+
public function __construct($redisClient, $namespace = '', $defaultLifetime = 0)
3138
{
3239
parent::__construct($namespace, $defaultLifetime);
3340

3441
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
3542
throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
3643
}
44+
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
45+
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)));
46+
}
3747
$this->redis = $redisClient;
3848
}
3949

@@ -52,7 +62,7 @@ public function __construct(\Redis $redisClient, $namespace = '', $defaultLifeti
5262
*
5363
* @throws InvalidArgumentException When the DSN is invalid.
5464
*
55-
* @return \Redis
65+
* @return \Redis|\Predis\Client According to the "class" option.
5666
*/
5767
public static function createConnection($dsn, array $options = array())
5868
{
@@ -86,7 +96,7 @@ public static function createConnection($dsn, array $options = array())
8696
$params += $query;
8797
}
8898
$params += $options + self::$defaultConnectionOptions;
89-
$class = $params['class'];
99+
$class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
90100

91101
if (is_a($class, \Redis::class, true)) {
92102
$connect = empty($params['persistent']) ? 'connect' : 'pconnect';
@@ -105,8 +115,13 @@ public static function createConnection($dsn, array $options = array())
105115
$e = preg_replace('/^ERR /', '', $redis->getLastError());
106116
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn));
107117
}
118+
} elseif (is_a($class, \Predis\Client::class, true)) {
119+
$params['scheme'] = isset($params['host']) ? 'tcp' : 'unix';
120+
$params['database'] = $params['dbindex'] ?: null;
121+
$params['password'] = $auth;
122+
$redis = new $class((new Factory())->create($params));
108123
} elseif (class_exists($class, false)) {
109-
throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis"', $class));
124+
throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
110125
} else {
111126
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
112127
}
@@ -139,24 +154,49 @@ protected function doFetch(array $ids)
139154
*/
140155
protected function doHave($id)
141156
{
142-
return $this->redis->exists($id);
157+
return (bool) $this->redis->exists($id);
143158
}
144159

145160
/**
146161
* {@inheritdoc}
147162
*/
148163
protected function doClear($namespace)
149164
{
150-
// As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
151-
// can hang your server when it is executed against large databases (millions of items).
152-
// Whenever you hit this scale, it is advised to deploy one Redis database per cache pool
153-
// instead of using namespaces, so that FLUSHDB is used instead.
154-
$lua = "local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end";
155-
156-
if (!isset($namespace[0])) {
157-
$this->redis->flushDb();
158-
} else {
159-
$this->redis->eval($lua, array($namespace), 0);
165+
// When using a native Redis cluster, clearing the cache cannot work and always returns false.
166+
// Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
167+
168+
$hosts = array($this->redis);
169+
$evalArgs = array(array($namespace), 0);
170+
171+
if ($this->redis instanceof \Predis\Client) {
172+
$evalArgs = array(0, $namespace);
173+
174+
$connection = $this->redis->getConnection();
175+
if ($connection instanceof PredisCluster) {
176+
$hosts = array();
177+
foreach ($connection as $c) {
178+
$hosts[] = new \Predis\Client($c);
179+
}
180+
} elseif ($connection instanceof RedisCluster) {
181+
return false;
182+
}
183+
} elseif ($this->redis instanceof \RedisArray) {
184+
foreach ($this->redis->_hosts() as $host) {
185+
$hosts[] = $this->redis->_instance($host);
186+
}
187+
} elseif ($this->redis instanceof \RedisCluster) {
188+
return false;
189+
}
190+
foreach ($hosts as $host) {
191+
if (!isset($namespace[0])) {
192+
$host->flushDb();
193+
} else {
194+
// As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
195+
// can hang your server when it is executed against large databases (millions of items).
196+
// Whenever you hit this scale, it is advised to deploy one Redis database per cache pool
197+
// instead of using namespaces, so that FLUSHDB is used instead.
198+
$host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end", $evalArgs[0], $evalArgs[1]);
199+
}
160200
}
161201

162202
return true;
@@ -194,17 +234,50 @@ protected function doSave(array $values, $lifetime)
194234
return $failed;
195235
}
196236
if ($lifetime > 0) {
197-
$this->redis->multi(\Redis::PIPELINE);
198-
foreach ($serialized as $id => $value) {
199-
$this->redis->setEx($id, $lifetime, $value);
200-
}
201-
if (!$this->redis->exec()) {
202-
return false;
237+
if ($this->redis instanceof \RedisArray) {
238+
$redis = array();
239+
foreach ($serialized as $id => $value) {
240+
if (!isset($redis[$h = $this->redis->_target($id)])) {
241+
$redis[$h] = $this->redis->_instance($h);
242+
$redis[$h]->multi(\Redis::PIPELINE);
243+
}
244+
$redis[$h]->setEx($id, $lifetime, $value);
245+
}
246+
foreach ($redis as $h) {
247+
if (!$h->exec()) {
248+
$failed = false;
249+
}
250+
}
251+
} else {
252+
$this->pipeline(function ($pipe) use ($serialized, $lifetime) {
253+
foreach ($serialized as $id => $value) {
254+
$pipe->setEx($id, $lifetime, $value);
255+
}
256+
});
203257
}
204258
} elseif (!$this->redis->mSet($serialized)) {
205259
return false;
206260
}
207261

208262
return $failed;
209263
}
264+
265+
private function pipeline(\Closure $callback)
266+
{
267+
if ($this->redis instanceof \Predis\Client) {
268+
return $this->redis->pipeline($callback);
269+
}
270+
$pipe = $this->redis instanceof \Redis && $this->redis->multi(\Redis::PIPELINE);
271+
try {
272+
$e = null;
273+
$callback($this->redis);
274+
} catch (\Exception $e) {
275+
}
276+
if ($pipe) {
277+
$this->redis->exec();
278+
}
279+
if (null !== $e) {
280+
throw $e;
281+
}
282+
}
210283
}
+46Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 Cache\IntegrationTests\CachePoolTest;
15+
use Symfony\Component\Cache\Adapter\RedisAdapter;
16+
17+
abstract class AbstractRedisAdapterTest extends CachePoolTest
18+
{
19+
protected static $redis;
20+
21+
public function createCachePool()
22+
{
23+
if (defined('HHVM_VERSION')) {
24+
$this->skippedTests['testDeferredSaveWithoutCommit'] = 'Fails on HHVM';
25+
}
26+
27+
return new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__));
28+
}
29+
30+
public static function setupBeforeClass()
31+
{
32+
if (!extension_loaded('redis')) {
33+
self::markTestSkipped('Extension redis required.');
34+
}
35+
if (!@((new \Redis())->connect('127.0.0.1'))) {
36+
$e = error_get_last();
37+
self::markTestSkipped($e['message']);
38+
}
39+
}
40+
41+
public static function tearDownAfterClass()
42+
{
43+
self::$redis->flushDB();
44+
self::$redis = null;
45+
}
46+
}
+49Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 Predis\Connection\StreamConnection;
15+
use Symfony\Component\Cache\Adapter\RedisAdapter;
16+
17+
class PredisAdapterTest extends AbstractRedisAdapterTest
18+
{
19+
public static function setupBeforeClass()
20+
{
21+
parent::setupBeforeClass();
22+
self::$redis = new \Predis\Client();
23+
}
24+
25+
public function testCreateConnection()
26+
{
27+
$redis = RedisAdapter::createConnection('redis://localhost/1', array('class' => \Predis\Client::class, 'timeout' => 3));
28+
$this->assertInstanceOf(\Predis\Client::class, $redis);
29+
30+
$connection = $redis->getConnection();
31+
$this->assertInstanceOf(StreamConnection::class, $connection);
32+
33+
$params = array(
34+
'scheme' => 'tcp',
35+
'host' => 'localhost',
36+
'path' => '',
37+
'dbindex' => '1',
38+
'port' => 6379,
39+
'class' => 'Predis\Client',
40+
'timeout' => 3,
41+
'persistent' => 0,
42+
'read_timeout' => 0,
43+
'retry_interval' => 0,
44+
'database' => '1',
45+
'password' => null,
46+
);
47+
$this->assertSame($params, $connection->getParameters()->toArray());
48+
}
49+
}

‎src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
+3-26Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,15 @@
1111

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

14-
use Cache\IntegrationTests\CachePoolTest;
1514
use Symfony\Component\Cache\Adapter\RedisAdapter;
1615

17-
/**
18-
* @requires extension redis
19-
*/
20-
class RedisAdapterTest extends CachePoolTest
16+
class RedisAdapterTest extends AbstractRedisAdapterTest
2117
{
22-
private static $redis;
23-
24-
public function createCachePool()
25-
{
26-
if (defined('HHVM_VERSION')) {
27-
$this->skippedTests['testDeferredSaveWithoutCommit'] = 'Fails on HHVM';
28-
}
29-
30-
return new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__));
31-
}
32-
3318
public static function setupBeforeClass()
3419
{
20+
parent::setupBeforeClass();
3521
self::$redis = new \Redis();
36-
if (!@self::$redis->connect('127.0.0.1')) {
37-
$e = error_get_last();
38-
self::markTestSkipped($e['message']);
39-
}
40-
}
41-
42-
public static function tearDownAfterClass()
43-
{
44-
self::$redis->flushDB();
45-
self::$redis->close();
22+
self::$redis->connect('127.0.0.1');
4623
}
4724

4825
public function testCreateConnection()
+24Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
class RedisArrayAdapterTest extends AbstractRedisAdapterTest
15+
{
16+
public static function setupBeforeClass()
17+
{
18+
parent::setupBeforeClass();
19+
if (!class_exists('RedisArray')) {
20+
self::markTestSkipped('The RedisArray class is required.');
21+
}
22+
self::$redis = new \RedisArray(array('localhost'), array('lazy_connect' => true));
23+
}
24+
}

‎src/Symfony/Component/Cache/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/composer.json
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"require-dev": {
2727
"cache/integration-tests": "dev-master",
28-
"doctrine/cache": "~1.6"
28+
"doctrine/cache": "~1.6",
29+
"predis/predis": "~1.0"
2930
},
3031
"suggest": {
3132
"symfony/polyfill-apcu": "For using ApcuAdapter on HHVM"

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.