2016-01-19 13:27:46 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Cache\Adapter ;
2016-05-01 18:15:43 +01:00
use Predis\Connection\Factory ;
use Predis\Connection\Aggregate\PredisCluster ;
use Predis\Connection\Aggregate\RedisCluster ;
2016-03-15 10:06:34 +00:00
use Symfony\Component\Cache\Exception\InvalidArgumentException ;
2016-01-19 13:27:46 +00:00
/**
* @ author Aurimas Niekis < aurimas @ niekis . lt >
2016-05-01 18:15:43 +01:00
* @ author Nicolas Grekas < p @ tchwork . com >
2016-01-19 13:27:46 +00:00
*/
class RedisAdapter extends AbstractAdapter
{
2016-04-29 13:31:16 +01:00
private static $defaultConnectionOptions = array (
2016-05-01 18:15:43 +01:00
'class' => null ,
2016-04-29 13:31:16 +01:00
'persistent' => 0 ,
2016-09-09 13:04:14 +01:00
'persistent_id' => null ,
2016-12-19 15:58:51 +00:00
'timeout' => 30 ,
2016-04-29 13:31:16 +01:00
'read_timeout' => 0 ,
'retry_interval' => 0 ,
);
2016-01-19 13:27:46 +00:00
private $redis ;
2016-05-01 18:15:43 +01:00
/**
* @ param \Redis | \RedisArray | \RedisCluster | \Predis\Client $redisClient
*/
public function __construct ( $redisClient , $namespace = '' , $defaultLifetime = 0 )
2016-01-19 13:27:46 +00:00
{
2016-05-02 13:16:25 +01:00
parent :: __construct ( $namespace , $defaultLifetime );
2016-03-15 10:06:34 +00:00
if ( preg_match ( '#[^-+_.A-Za-z0-9]#' , $namespace , $match )) {
throw new InvalidArgumentException ( sprintf ( 'RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.' , $match [ 0 ]));
}
2016-05-01 18:15:43 +01:00
if ( ! $redisClient instanceof \Redis && ! $redisClient instanceof \RedisArray && ! $redisClient instanceof \RedisCluster && ! $redisClient instanceof \Predis\Client ) {
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 )));
}
2016-05-02 13:16:25 +01:00
$this -> redis = $redisClient ;
2016-01-19 13:27:46 +00:00
}
2016-04-29 13:31:16 +01:00
/**
* Creates a Redis connection using a DSN configuration .
*
* Example DSN :
* - redis :// localhost
* - redis :// example . com : 1234
* - redis :// secret @ example . com / 13
* - redis :/// var / run / redis . sock
* - redis :// secret @/ var / run / redis . sock / 13
*
* @ param string $dsn
* @ param array $options See self :: $defaultConnectionOptions
*
* @ throws InvalidArgumentException When the DSN is invalid .
*
2016-06-29 06:42:25 +01:00
* @ return \Redis | \Predis\Client According to the " class " option
2016-04-29 13:31:16 +01:00
*/
public static function createConnection ( $dsn , array $options = array ())
{
if ( 0 !== strpos ( $dsn , 'redis://' )) {
throw new InvalidArgumentException ( sprintf ( 'Invalid Redis DSN: %s does not start with "redis://"' , $dsn ));
}
2016-09-23 14:37:10 +01:00
$params = preg_replace_callback ( '#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#' , function ( $m ) use ( & $auth ) {
2016-04-29 13:31:16 +01:00
if ( isset ( $m [ 1 ])) {
$auth = $m [ 1 ];
}
return 'file://' ;
}, $dsn );
if ( false === $params = parse_url ( $params )) {
throw new InvalidArgumentException ( sprintf ( 'Invalid Redis DSN: %s' , $dsn ));
}
if ( ! isset ( $params [ 'host' ]) && ! isset ( $params [ 'path' ])) {
throw new InvalidArgumentException ( sprintf ( 'Invalid Redis DSN: %s' , $dsn ));
}
if ( isset ( $params [ 'path' ]) && preg_match ( '#/(\d+)$#' , $params [ 'path' ], $m )) {
$params [ 'dbindex' ] = $m [ 1 ];
$params [ 'path' ] = substr ( $params [ 'path' ], 0 , - strlen ( $m [ 0 ]));
}
$params += array (
'host' => isset ( $params [ 'host' ]) ? $params [ 'host' ] : $params [ 'path' ],
'port' => isset ( $params [ 'host' ]) ? 6379 : null ,
'dbindex' => 0 ,
);
if ( isset ( $params [ 'query' ])) {
parse_str ( $params [ 'query' ], $query );
$params += $query ;
}
$params += $options + self :: $defaultConnectionOptions ;
2016-05-01 18:15:43 +01:00
$class = null === $params [ 'class' ] ? ( extension_loaded ( 'redis' ) ? \Redis :: class : \Predis\Client :: class ) : $params [ 'class' ];
2016-04-29 13:31:16 +01:00
2016-05-02 13:16:25 +01:00
if ( is_a ( $class , \Redis :: class , true )) {
2016-09-09 13:04:14 +01:00
$connect = $params [ 'persistent' ] || $params [ 'persistent_id' ] ? 'pconnect' : 'connect' ;
2016-05-02 13:16:25 +01:00
$redis = new $class ();
2016-09-09 13:04:14 +01:00
@ $redis -> { $connect }( $params [ 'host' ], $params [ 'port' ], $params [ 'timeout' ], $params [ 'persistent_id' ], $params [ 'retry_interval' ]);
2016-04-29 13:31:16 +01:00
2016-05-02 13:16:25 +01:00
if ( @! $redis -> isConnected ()) {
$e = ( $e = error_get_last ()) && preg_match ( '/^Redis::p?connect\(\): (.*)/' , $e [ 'message' ], $e ) ? sprintf ( ' (%s)' , $e [ 1 ]) : '' ;
throw new InvalidArgumentException ( sprintf ( 'Redis connection failed%s: %s' , $e , $dsn ));
}
2016-04-29 13:31:16 +01:00
2016-05-02 13:16:25 +01:00
if (( null !== $auth && ! $redis -> auth ( $auth ))
|| ( $params [ 'dbindex' ] && ! $redis -> select ( $params [ 'dbindex' ]))
|| ( $params [ 'read_timeout' ] && ! $redis -> setOption ( \Redis :: OPT_READ_TIMEOUT , $params [ 'read_timeout' ]))
) {
$e = preg_replace ( '/^ERR /' , '' , $redis -> getLastError ());
throw new InvalidArgumentException ( sprintf ( 'Redis connection failed (%s): %s' , $e , $dsn ));
}
2016-05-01 18:15:43 +01:00
} elseif ( is_a ( $class , \Predis\Client :: class , true )) {
$params [ 'scheme' ] = isset ( $params [ 'host' ]) ? 'tcp' : 'unix' ;
$params [ 'database' ] = $params [ 'dbindex' ] ? : null ;
$params [ 'password' ] = $auth ;
$redis = new $class (( new Factory ()) -> create ( $params ));
2016-05-02 13:16:25 +01:00
} elseif ( class_exists ( $class , false )) {
2016-05-01 18:15:43 +01:00
throw new InvalidArgumentException ( sprintf ( '"%s" is not a subclass of "Redis" or "Predis\Client"' , $class ));
2016-05-02 13:16:25 +01:00
} else {
throw new InvalidArgumentException ( sprintf ( 'Class "%s" does not exist' , $class ));
2016-04-29 13:31:16 +01:00
}
return $redis ;
}
2016-01-19 13:27:46 +00:00
/**
* { @ inheritdoc }
*/
protected function doFetch ( array $ids )
{
2016-04-28 08:51:36 +01:00
if ( $ids ) {
2016-05-02 13:16:25 +01:00
$values = $this -> redis -> mGet ( $ids );
2016-04-28 08:51:36 +01:00
$index = 0 ;
foreach ( $ids as $id ) {
2016-05-02 13:16:25 +01:00
if ( $value = $values [ $index ++ ]) {
2016-08-08 14:21:59 +01:00
yield $id => parent :: unserialize ( $value );
2016-04-28 08:51:36 +01:00
}
2016-01-19 13:27:46 +00:00
}
}
}
/**
* { @ inheritdoc }
*/
protected function doHave ( $id )
{
2016-05-01 18:15:43 +01:00
return ( bool ) $this -> redis -> exists ( $id );
2016-01-19 13:27:46 +00:00
}
/**
* { @ inheritdoc }
*/
2016-03-15 10:06:34 +00:00
protected function doClear ( $namespace )
2016-01-19 13:27:46 +00:00
{
2016-05-01 18:15:43 +01:00
// When using a native Redis cluster, clearing the cache cannot work and always returns false.
// Clearing the cache should then be done by any other means (e.g. by restarting the cluster).
2016-08-05 19:26:55 +01:00
$cleared = true ;
2016-05-01 18:15:43 +01:00
$hosts = array ( $this -> redis );
$evalArgs = array ( array ( $namespace ), 0 );
if ( $this -> redis instanceof \Predis\Client ) {
$evalArgs = array ( 0 , $namespace );
$connection = $this -> redis -> getConnection ();
if ( $connection instanceof PredisCluster ) {
$hosts = array ();
foreach ( $connection as $c ) {
$hosts [] = new \Predis\Client ( $c );
}
} elseif ( $connection instanceof RedisCluster ) {
return false ;
}
} elseif ( $this -> redis instanceof \RedisArray ) {
2016-08-05 19:26:55 +01:00
$hosts = array ();
2016-05-01 18:15:43 +01:00
foreach ( $this -> redis -> _hosts () as $host ) {
$hosts [] = $this -> redis -> _instance ( $host );
}
} elseif ( $this -> redis instanceof \RedisCluster ) {
return false ;
}
foreach ( $hosts as $host ) {
if ( ! isset ( $namespace [ 0 ])) {
2016-08-05 19:26:55 +01:00
$cleared = $host -> flushDb () && $cleared ;
continue ;
}
$info = $host -> info ( 'Server' );
$info = isset ( $info [ 'Server' ]) ? $info [ 'Server' ] : $info ;
if ( ! version_compare ( $info [ 'redis_version' ], '2.8' , '>=' )) {
2016-05-01 18:15:43 +01:00
// As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
// can hang your server when it is executed against large databases (millions of items).
2016-08-05 19:26:55 +01:00
// Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
$cleared = $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 return 1 " , $evalArgs [ 0 ], $evalArgs [ 1 ]) && $cleared ;
continue ;
2016-05-01 18:15:43 +01:00
}
2016-08-05 19:26:55 +01:00
$cursor = null ;
do {
$keys = $host instanceof \Predis\Client ? $host -> scan ( $cursor , 'MATCH' , $namespace . '*' , 'COUNT' , 1000 ) : $host -> scan ( $cursor , $namespace . '*' , 1000 );
if ( isset ( $keys [ 1 ]) && is_array ( $keys [ 1 ])) {
$cursor = $keys [ 0 ];
$keys = $keys [ 1 ];
}
if ( $keys ) {
$host -> del ( $keys );
}
} while ( $cursor = ( int ) $cursor );
2016-03-15 10:06:34 +00:00
}
2016-08-05 19:26:55 +01:00
return $cleared ;
2016-01-19 13:27:46 +00:00
}
/**
* { @ inheritdoc }
*/
protected function doDelete ( array $ids )
{
2016-04-28 08:51:36 +01:00
if ( $ids ) {
$this -> redis -> del ( $ids );
}
2016-01-19 13:27:46 +00:00
return true ;
}
/**
* { @ inheritdoc }
*/
protected function doSave ( array $values , $lifetime )
{
2016-03-20 20:40:04 +00:00
$serialized = array ();
2016-03-15 10:06:34 +00:00
$failed = array ();
2016-03-21 10:14:47 +00:00
foreach ( $values as $id => $value ) {
2016-03-15 10:06:34 +00:00
try {
2016-03-21 10:14:47 +00:00
$serialized [ $id ] = serialize ( $value );
2016-03-15 10:06:34 +00:00
} catch ( \Exception $e ) {
$failed [] = $id ;
2016-01-19 13:27:46 +00:00
}
2016-03-15 10:06:34 +00:00
}
2016-04-28 08:51:36 +01:00
if ( ! $serialized ) {
return $failed ;
}
2016-04-26 16:44:27 +01:00
if ( 0 >= $lifetime ) {
$this -> redis -> mSet ( $serialized );
return $failed ;
2016-01-19 13:27:46 +00:00
}
2016-04-26 16:44:27 +01:00
$this -> pipeline ( function ( $pipe ) use ( & $serialized , $lifetime ) {
foreach ( $serialized as $id => $value ) {
$pipe ( 'setEx' , $id , array ( $lifetime , $value ));
}
});
2016-03-15 10:06:34 +00:00
return $failed ;
2016-01-19 13:27:46 +00:00
}
2016-05-01 18:15:43 +01:00
2016-04-26 16:44:27 +01:00
private function execute ( $command , $id , array $args , $redis = null )
{
array_unshift ( $args , $id );
call_user_func_array ( array ( $redis ? : $this -> redis , $command ), $args );
}
2016-05-01 18:15:43 +01:00
private function pipeline ( \Closure $callback )
{
2016-04-26 16:44:27 +01:00
$redis = $this -> redis ;
2016-05-01 18:15:43 +01:00
try {
2016-04-26 16:44:27 +01:00
if ( $redis instanceof \Predis\Client ) {
$redis -> pipeline ( function ( $pipe ) use ( $callback ) {
$this -> redis = $pipe ;
$callback ( array ( $this , 'execute' ));
});
} elseif ( $redis instanceof \RedisArray ) {
$connections = array ();
$callback ( function ( $command , $id , $args ) use ( & $connections ) {
if ( ! isset ( $connections [ $h = $this -> redis -> _target ( $id )])) {
$connections [ $h ] = $this -> redis -> _instance ( $h );
$connections [ $h ] -> multi ( \Redis :: PIPELINE );
}
$this -> execute ( $command , $id , $args , $connections [ $h ]);
});
foreach ( $connections as $c ) {
$c -> exec ();
}
} else {
$pipe = $redis -> multi ( \Redis :: PIPELINE );
try {
$callback ( array ( $this , 'execute' ));
} finally {
if ( $pipe ) {
$redis -> exec ();
}
}
}
} finally {
$this -> redis = $redis ;
2016-05-01 18:15:43 +01:00
}
}
2016-01-19 13:27:46 +00:00
}