2018-04-17 02:11:51 +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\Traits ;
use Predis\Connection\Aggregate\ClusterInterface ;
use Predis\Connection\Aggregate\RedisCluster ;
2024-01-12 05:08:24 +00:00
use Predis\Connection\Factory ;
2018-04-17 02:11:51 +00:00
use Predis\Response\Status ;
use Symfony\Component\Cache\Exception\CacheException ;
use Symfony\Component\Cache\Exception\InvalidArgumentException ;
/**
* @ author Aurimas Niekis < aurimas @ niekis . lt >
* @ author Nicolas Grekas < p @ tchwork . com >
*
* @ internal
*/
trait RedisTrait
{
2024-01-12 05:08:24 +00:00
private static $defaultConnectionOptions = [
2018-04-17 02:11:51 +00:00
'class' => null ,
'persistent' => 0 ,
'persistent_id' => null ,
'timeout' => 30 ,
'read_timeout' => 0 ,
'retry_interval' => 0 ,
'lazy' => false ,
2024-01-12 05:08:24 +00:00
];
2018-04-17 02:11:51 +00:00
private $redis ;
/**
* @ param \Redis | \RedisArray | \RedisCluster | \Predis\Client $redisClient
*/
2024-01-12 05:08:24 +00:00
private function init ( $redisClient , $namespace = '' , $defaultLifetime = 0 )
2018-04-17 02:11:51 +00:00
{
parent :: __construct ( $namespace , $defaultLifetime );
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 ]));
}
2024-01-12 05:08:24 +00:00
if ( ! $redisClient instanceof \Redis && ! $redisClient instanceof \RedisArray && ! $redisClient instanceof \RedisCluster && ! $redisClient instanceof \Predis\Client && ! $redisClient instanceof RedisProxy ) {
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 )));
2018-04-17 02:11:51 +00:00
}
$this -> redis = $redisClient ;
}
/**
* 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
*
* @ return \Redis | \Predis\Client According to the " class " option
*/
2024-01-12 05:08:24 +00:00
public static function createConnection ( $dsn , array $options = [])
2018-04-17 02:11:51 +00:00
{
if ( 0 !== strpos ( $dsn , 'redis://' )) {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( 'Invalid Redis DSN: "%s" does not start with "redis://".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
$params = preg_replace_callback ( '#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#' , function ( $m ) use ( & $auth ) {
if ( isset ( $m [ 1 ])) {
$auth = $m [ 1 ];
}
return 'file://' ;
}, $dsn );
if ( false === $params = parse_url ( $params )) {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( 'Invalid Redis DSN: "%s".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
if ( ! isset ( $params [ 'host' ]) && ! isset ( $params [ 'path' ])) {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( 'Invalid Redis DSN: "%s".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
if ( isset ( $params [ 'path' ]) && preg_match ( '#/(\d+)$#' , $params [ 'path' ], $m )) {
$params [ 'dbindex' ] = $m [ 1 ];
2024-01-12 05:08:24 +00:00
$params [ 'path' ] = substr ( $params [ 'path' ], 0 , - \strlen ( $m [ 0 ]));
2018-04-17 02:11:51 +00:00
}
if ( isset ( $params [ 'host' ])) {
$scheme = 'tcp' ;
} else {
$scheme = 'unix' ;
}
2024-01-12 05:08:24 +00:00
$params += [
2018-04-17 02:11:51 +00:00
'host' => isset ( $params [ 'host' ]) ? $params [ 'host' ] : $params [ 'path' ],
'port' => isset ( $params [ 'host' ]) ? 6379 : null ,
'dbindex' => 0 ,
2024-01-12 05:08:24 +00:00
];
2018-04-17 02:11:51 +00:00
if ( isset ( $params [ 'query' ])) {
parse_str ( $params [ 'query' ], $query );
$params += $query ;
}
$params += $options + self :: $defaultConnectionOptions ;
2024-01-12 05:08:24 +00:00
if ( null === $params [ 'class' ] && ! \extension_loaded ( 'redis' ) && ! class_exists ( \Predis\Client :: class )) {
throw new CacheException ( sprintf ( 'Cannot find the "redis" extension, and "predis/predis" is not installed: "%s".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
$class = null === $params [ 'class' ] ? ( \extension_loaded ( 'redis' ) ? \Redis :: class : \Predis\Client :: class ) : $params [ 'class' ];
2018-04-17 02:11:51 +00:00
if ( is_a ( $class , \Redis :: class , true )) {
$connect = $params [ 'persistent' ] || $params [ 'persistent_id' ] ? 'pconnect' : 'connect' ;
$redis = new $class ();
$initializer = function ( $redis ) use ( $connect , $params , $dsn , $auth ) {
try {
@ $redis -> { $connect }( $params [ 'host' ], $params [ 'port' ], $params [ 'timeout' ], $params [ 'persistent_id' ], $params [ 'retry_interval' ]);
2024-01-12 05:08:24 +00:00
set_error_handler ( function ( $type , $msg ) use ( & $error ) { $error = $msg ; });
$isConnected = $redis -> isConnected ();
restore_error_handler ();
if ( ! $isConnected ) {
$error = preg_match ( '/^Redis::p?connect\(\): (.*)/' , $error , $error ) ? sprintf ( ' (%s)' , $error [ 1 ]) : '' ;
throw new InvalidArgumentException ( sprintf ( 'Redis connection "%s" failed: ' , $dsn ) . $error . '.' );
}
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 "%s" failed: ' , $dsn ) . $e . '.' );
}
} catch ( \RedisException $e ) {
throw new InvalidArgumentException ( sprintf ( 'Redis connection "%s" failed: ' , $dsn ) . $e -> getMessage ());
2018-04-17 02:11:51 +00:00
}
return true ;
};
if ( $params [ 'lazy' ]) {
$redis = new RedisProxy ( $redis , $initializer );
} else {
$initializer ( $redis );
}
} elseif ( is_a ( $class , \Predis\Client :: class , true )) {
$params [ 'scheme' ] = $scheme ;
$params [ 'database' ] = $params [ 'dbindex' ] ? : null ;
$params [ 'password' ] = $auth ;
$redis = new $class (( new Factory ()) -> create ( $params ));
} elseif ( class_exists ( $class , false )) {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( '"%s" is not a subclass of "Redis" or "Predis\Client".' , $class ));
2018-04-17 02:11:51 +00:00
} else {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( 'Class "%s" does not exist.' , $class ));
2018-04-17 02:11:51 +00:00
}
return $redis ;
}
/**
* { @ inheritdoc }
*/
protected function doFetch ( array $ids )
{
2024-01-12 05:08:24 +00:00
if ( ! $ids ) {
return [];
}
$result = [];
if ( $this -> redis instanceof \Predis\Client && $this -> redis -> getConnection () instanceof ClusterInterface ) {
2018-04-17 02:11:51 +00:00
$values = $this -> pipeline ( function () use ( $ids ) {
foreach ( $ids as $id ) {
2024-01-12 05:08:24 +00:00
yield 'get' => [ $id ];
2018-04-17 02:11:51 +00:00
}
});
2024-01-12 05:08:24 +00:00
} else {
$values = $this -> redis -> mget ( $ids );
if ( ! \is_array ( $values ) || \count ( $values ) !== \count ( $ids )) {
return [];
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
$values = array_combine ( $ids , $values );
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
foreach ( $values as $id => $v ) {
if ( $v ) {
$result [ $id ] = parent :: unserialize ( $v );
}
}
return $result ;
2018-04-17 02:11:51 +00:00
}
/**
* { @ inheritdoc }
*/
protected function doHave ( $id )
{
return ( bool ) $this -> redis -> exists ( $id );
}
/**
* { @ inheritdoc }
*/
protected function doClear ( $namespace )
{
$cleared = true ;
2024-01-12 05:08:24 +00:00
$hosts = [ $this -> redis ];
$evalArgs = [[ $namespace ], 0 ];
2018-04-17 02:11:51 +00:00
if ( $this -> redis instanceof \Predis\Client ) {
2024-01-12 05:08:24 +00:00
$evalArgs = [ 0 , $namespace ];
2018-04-17 02:11:51 +00:00
$connection = $this -> redis -> getConnection ();
2024-01-12 05:08:24 +00:00
if ( $connection instanceof ClusterInterface && $connection instanceof \Traversable ) {
$hosts = [];
2018-04-17 02:11:51 +00:00
foreach ( $connection as $c ) {
$hosts [] = new \Predis\Client ( $c );
}
}
} elseif ( $this -> redis instanceof \RedisArray ) {
2024-01-12 05:08:24 +00:00
$hosts = [];
2018-04-17 02:11:51 +00:00
foreach ( $this -> redis -> _hosts () as $host ) {
$hosts [] = $this -> redis -> _instance ( $host );
}
} elseif ( $this -> redis instanceof \RedisCluster ) {
2024-01-12 05:08:24 +00:00
$hosts = [];
foreach ( $this -> redis -> _masters () as $host ) {
$hosts [] = $h = new \Redis ();
$h -> connect ( $host [ 0 ], $host [ 1 ]);
}
2018-04-17 02:11:51 +00:00
}
foreach ( $hosts as $host ) {
if ( ! isset ( $namespace [ 0 ])) {
$cleared = $host -> flushDb () && $cleared ;
continue ;
}
$info = $host -> info ( 'Server' );
$info = isset ( $info [ 'Server' ]) ? $info [ 'Server' ] : $info ;
if ( ! version_compare ( $info [ 'redis_version' ], '2.8' , '>=' )) {
// 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).
// 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 ;
}
$cursor = null ;
do {
$keys = $host instanceof \Predis\Client ? $host -> scan ( $cursor , 'MATCH' , $namespace . '*' , 'COUNT' , 1000 ) : $host -> scan ( $cursor , $namespace . '*' , 1000 );
2024-01-12 05:08:24 +00:00
if ( isset ( $keys [ 1 ]) && \is_array ( $keys [ 1 ])) {
2018-04-17 02:11:51 +00:00
$cursor = $keys [ 0 ];
$keys = $keys [ 1 ];
}
if ( $keys ) {
2024-01-12 05:08:24 +00:00
$this -> doDelete ( $keys );
2018-04-17 02:11:51 +00:00
}
} while ( $cursor = ( int ) $cursor );
}
return $cleared ;
}
/**
* { @ inheritdoc }
*/
protected function doDelete ( array $ids )
{
2024-01-12 05:08:24 +00:00
if ( ! $ids ) {
return true ;
}
if ( $this -> redis instanceof \Predis\Client && $this -> redis -> getConnection () instanceof ClusterInterface ) {
$this -> pipeline ( function () use ( $ids ) {
foreach ( $ids as $id ) {
yield 'del' => [ $id ];
}
}) -> rewind ();
} else {
2018-04-17 02:11:51 +00:00
$this -> redis -> del ( $ids );
}
return true ;
}
/**
* { @ inheritdoc }
*/
protected function doSave ( array $values , $lifetime )
{
2024-01-12 05:08:24 +00:00
$serialized = [];
$failed = [];
2018-04-17 02:11:51 +00:00
foreach ( $values as $id => $value ) {
try {
$serialized [ $id ] = serialize ( $value );
} catch ( \Exception $e ) {
$failed [] = $id ;
}
}
if ( ! $serialized ) {
return $failed ;
}
$results = $this -> pipeline ( function () use ( $serialized , $lifetime ) {
foreach ( $serialized as $id => $value ) {
if ( 0 >= $lifetime ) {
2024-01-12 05:08:24 +00:00
yield 'set' => [ $id , $value ];
2018-04-17 02:11:51 +00:00
} else {
2024-01-12 05:08:24 +00:00
yield 'setEx' => [ $id , $lifetime , $value ];
2018-04-17 02:11:51 +00:00
}
}
});
foreach ( $results as $id => $result ) {
if ( true !== $result && ( ! $result instanceof Status || $result !== Status :: get ( 'OK' ))) {
$failed [] = $id ;
}
}
return $failed ;
}
private function pipeline ( \Closure $generator )
{
2024-01-12 05:08:24 +00:00
$ids = [];
2018-04-17 02:11:51 +00:00
2024-01-12 05:08:24 +00:00
if ( $this -> redis instanceof \RedisCluster || ( $this -> redis instanceof \Predis\Client && $this -> redis -> getConnection () instanceof RedisCluster )) {
// phpredis & predis don't support pipelining with RedisCluster
// see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
// see https://github.com/nrk/predis/issues/267#issuecomment-123781423
$results = [];
foreach ( $generator () as $command => $args ) {
$results [] = \call_user_func_array ([ $this -> redis , $command ], $args );
$ids [] = $args [ 0 ];
}
} elseif ( $this -> redis instanceof \Predis\Client ) {
2018-04-17 02:11:51 +00:00
$results = $this -> redis -> pipeline ( function ( $redis ) use ( $generator , & $ids ) {
foreach ( $generator () as $command => $args ) {
2024-01-12 05:08:24 +00:00
\call_user_func_array ([ $redis , $command ], $args );
2018-04-17 02:11:51 +00:00
$ids [] = $args [ 0 ];
}
});
} elseif ( $this -> redis instanceof \RedisArray ) {
2024-01-12 05:08:24 +00:00
$connections = $results = $ids = [];
2018-04-17 02:11:51 +00:00
foreach ( $generator () as $command => $args ) {
if ( ! isset ( $connections [ $h = $this -> redis -> _target ( $args [ 0 ])])) {
2024-01-12 05:08:24 +00:00
$connections [ $h ] = [ $this -> redis -> _instance ( $h ), - 1 ];
2018-04-17 02:11:51 +00:00
$connections [ $h ][ 0 ] -> multi ( \Redis :: PIPELINE );
}
2024-01-12 05:08:24 +00:00
\call_user_func_array ([ $connections [ $h ][ 0 ], $command ], $args );
$results [] = [ $h , ++ $connections [ $h ][ 1 ]];
2018-04-17 02:11:51 +00:00
$ids [] = $args [ 0 ];
}
foreach ( $connections as $h => $c ) {
$connections [ $h ] = $c [ 0 ] -> exec ();
}
foreach ( $results as $k => list ( $h , $c )) {
$results [ $k ] = $connections [ $h ][ $c ];
}
} else {
$this -> redis -> multi ( \Redis :: PIPELINE );
foreach ( $generator () as $command => $args ) {
2024-01-12 05:08:24 +00:00
\call_user_func_array ([ $this -> redis , $command ], $args );
2018-04-17 02:11:51 +00:00
$ids [] = $args [ 0 ];
}
$results = $this -> redis -> exec ();
}
foreach ( $ids as $k => $id ) {
yield $id => $results [ $k ];
}
}
}