Skip to content

Commit 5ca5666

Browse files
[Cache] Add support for Predis, RedisArray and RedisCluster
1 parent cfcf263 commit 5ca5666

File tree

7 files changed

+223
-49
lines changed

7 files changed

+223
-49
lines changed

composer.json

Lines 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

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,43 @@
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) {
45+
} elseif ($redisClient instanceof \RedisArray) {
46+
} elseif ($redisClient instanceof \RedisCluster) {
47+
} elseif ($redisClient instanceof \Predis\Client) {
48+
} else {
49+
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)));
50+
}
3751
$this->redis = $redisClient;
3852
}
3953

@@ -52,7 +66,7 @@ public function __construct(\Redis $redisClient, $namespace = '', $defaultLifeti
5266
*
5367
* @throws InvalidArgumentException When the DSN is invalid.
5468
*
55-
* @return \Redis
69+
* @return \Redis|\Predis\Client According to the "class" option.
5670
*/
5771
public static function createConnection($dsn, array $options = array())
5872
{
@@ -86,7 +100,7 @@ public static function createConnection($dsn, array $options = array())
86100
$params += $query;
87101
}
88102
$params += $options + self::$defaultConnectionOptions;
89-
$class = $params['class'];
103+
$class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
90104

91105
if (is_a($class, \Redis::class, true)) {
92106
$connect = empty($params['persistent']) ? 'connect' : 'pconnect';
@@ -105,8 +119,13 @@ public static function createConnection($dsn, array $options = array())
105119
$e = preg_replace('/^ERR /', '', $redis->getLastError());
106120
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn));
107121
}
122+
} elseif (is_a($class, \Predis\Client::class, true)) {
123+
$params['scheme'] = isset($params['host']) ? 'tcp' : 'unix';
124+
$params['database'] = $params['dbindex'] ?: null;
125+
$params['password'] = $auth;
126+
$redis = new $class((new Factory())->create($params));
108127
} elseif (class_exists($class, false)) {
109-
throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis"', $class));
128+
throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
110129
} else {
111130
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
112131
}
@@ -139,24 +158,49 @@ protected function doFetch(array $ids)
139158
*/
140159
protected function doHave($id)
141160
{
142-
return $this->redis->exists($id);
161+
return (bool) $this->redis->exists($id);
143162
}
144163

145164
/**
146165
* {@inheritdoc}
147166
*/
148167
protected function doClear($namespace)
149168
{
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);
169+
// When using a native Redis cluster, clearing the cache cannot work and always returns false.
170+
// Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
171+
172+
$hosts = array($this->redis);
173+
$evalArgs = array(array($namespace), 0);
174+
175+
if ($this->redis instanceof \Predis\Client) {
176+
$evalArgs = array(0, $namespace);
177+
178+
$connection = $this->redis->getConnection();
179+
if ($connection instanceof PredisCluster) {
180+
$hosts = array();
181+
foreach ($connection as $c) {
182+
$hosts[] = new \Predis\Client($c);
183+
}
184+
} elseif ($connection instanceof RedisCluster) {
185+
return false;
186+
}
187+
} elseif ($this->redis instanceof \RedisArray) {
188+
foreach ($this->redis->_hosts() as $host) {
189+
$hosts[] = $this->redis->_instance($host);
190+
}
191+
} elseif ($this->redis instanceof \RedisCluster) {
192+
return false;
193+
}
194+
foreach ($hosts as $host) {
195+
if (!isset($namespace[0])) {
196+
$host->flushDb();
197+
} else {
198+
// As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
199+
// can hang your server when it is executed against large databases (millions of items).
200+
// Whenever you hit this scale, it is advised to deploy one Redis database per cache pool
201+
// instead of using namespaces, so that FLUSHDB is used instead.
202+
$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]);
203+
}
160204
}
161205

162206
return true;
@@ -194,17 +238,49 @@ protected function doSave(array $values, $lifetime)
194238
return $failed;
195239
}
196240
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;
241+
if ($this->redis instanceof \RedisArray) {
242+
$redis = array();
243+
foreach ($serialized as $id => $value) {
244+
if (!isset($redis[$h = $this->redis->_target($id)])) {
245+
$redis[$h] = $this->redis->_instance($h);
246+
$redis[$h]->multi(\Redis::PIPELINE);
247+
}
248+
$redis[$h]->setEx($id, $lifetime, $value);
249+
}
250+
foreach ($redis as $h) {
251+
if (!$h->exec()) {
252+
$failed = false;
253+
}
254+
}
255+
} else {
256+
$this->pipeline(function ($pipe) use ($serialized, $lifetime) {
257+
foreach ($serialized as $id => $value) {
258+
$pipe->setEx($id, $lifetime, $value);
259+
}
260+
});
203261
}
204262
} elseif (!$this->redis->mSet($serialized)) {
205263
return false;
206264
}
207265

208266
return $failed;
209267
}
268+
269+
private function pipeline(\Closure $callback)
270+
{
271+
if ($this->redis instanceof \Predis\Client) {
272+
return $this->redis->pipeline($callback);
273+
}
274+
$pipe = $this->redis instanceof \Redis && $this->redis->multi(\Redis::PIPELINE);
275+
try {
276+
$callback($this->redis);
277+
} catch (\Exception $e) {
278+
}
279+
if ($pipe) {
280+
$this->redis->exec();
281+
}
282+
if (isset($e)) {
283+
throw $e;
284+
}
285+
}
210286
}
Lines 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+
}
Lines 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

Lines 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()
Lines 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

Lines 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy