From 1f5e3538d80322d1c2af5ee3b7b0048bff1fcdd8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 9 Nov 2017 17:55:39 +0100 Subject: [PATCH] [Cache][Lock] Add RedisProxy for lazy Redis connections --- .../Compiler/CachePoolPass.php | 2 +- .../FrameworkExtension.php | 2 +- .../Cache/Tests/Adapter/PredisAdapterTest.php | 1 + .../Cache/Tests/Adapter/RedisAdapterTest.php | 11 +++- .../Component/Cache/Traits/RedisProxy.php | 65 +++++++++++++++++++ .../Component/Cache/Traits/RedisTrait.php | 36 ++++++---- .../Component/Lock/Store/RedisStore.php | 13 +--- .../Component/Lock/Store/StoreFactory.php | 3 +- 8 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 src/Symfony/Component/Cache/Traits/RedisProxy.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php index 004a2c544d..51b383bc38 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php @@ -134,7 +134,7 @@ class CachePoolPass implements CompilerPassInterface $definition = new Definition(AbstractAdapter::class); $definition->setPublic(false); $definition->setFactory(array(AbstractAdapter::class, 'createConnection')); - $definition->setArguments(array($dsn)); + $definition->setArguments(array($dsn, array('lazy' => true))); $container->setDefinition($name, $definition); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 3b624d81e5..57a15d05a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1547,7 +1547,7 @@ class FrameworkExtension extends Extension $connectionDefinition = new Definition(\stdClass::class); $connectionDefinition->setPublic(false); $connectionDefinition->setFactory(array(AbstractAdapter::class, 'createConnection')); - $connectionDefinition->setArguments(array($storeDsn)); + $connectionDefinition->setArguments(array($storeDsn, array('lazy' => true))); $container->setDefinition($connectionDefinitionId, $connectionDefinition); } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php index 85ca36c9ef..c005d64abf 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php @@ -44,6 +44,7 @@ class PredisAdapterTest extends AbstractRedisAdapterTest 'persistent_id' => null, 'read_timeout' => 0, 'retry_interval' => 0, + 'lazy' => false, 'database' => '1', 'password' => null, ); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php index a8f7a673f8..28c310fb18 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php @@ -13,13 +13,22 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Traits\RedisProxy; class RedisAdapterTest extends AbstractRedisAdapterTest { public static function setupBeforeClass() { parent::setupBeforeClass(); - self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST')); + self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST'), array('lazy' => true)); + } + + public function createCachePool($defaultLifetime = 0) + { + $adapter = parent::createCachePool($defaultLifetime); + $this->assertInstanceOf(RedisProxy::class, self::$redis); + + return $adapter; } public function testCreateConnection() diff --git a/src/Symfony/Component/Cache/Traits/RedisProxy.php b/src/Symfony/Component/Cache/Traits/RedisProxy.php new file mode 100644 index 0000000000..b328f94cd8 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/RedisProxy.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class RedisProxy +{ + private $redis; + private $initializer; + private $ready = false; + + public function __construct(\Redis $redis, \Closure $initializer) + { + $this->redis = $redis; + $this->initializer = $initializer; + } + + public function __call($method, array $args) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return \call_user_func_array(array($this->redis, $method), $args); + } + + public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->scan($iIterator, $strPattern, $iCount); + } + + public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount); + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index f4b43a939e..625ca12b7a 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -34,6 +34,7 @@ trait RedisTrait 'timeout' => 30, 'read_timeout' => 0, 'retry_interval' => 0, + 'lazy' => false, ); private $redis; @@ -49,7 +50,7 @@ trait RedisTrait } if ($redisClient instanceof \RedisCluster) { $this->enableVersioning(); - } elseif (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \Predis\Client) { + } elseif (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$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))); } $this->redis = $redisClient; @@ -117,19 +118,30 @@ trait RedisTrait if (is_a($class, \Redis::class, true)) { $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect'; $redis = new $class(); - @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']); - 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)); - } + $initializer = function ($redis) use ($connect, $params, $dsn, $auth) { + @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']); - 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)); + 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)); + } + + 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)); + } + + return true; + }; + + if ($params['lazy']) { + $redis = new RedisProxy($redis, $initializer); + } else { + $initializer($redis); } } elseif (is_a($class, \Predis\Client::class, true)) { $params['scheme'] = $scheme; diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index 9f17e49b78..4d057337b6 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Lock\Store; +use Symfony\Component\Cache\Traits\RedisProxy; use Symfony\Component\Lock\Exception\InvalidArgumentException; use Symfony\Component\Lock\Exception\LockConflictedException; use Symfony\Component\Lock\Exception\LockExpiredException; @@ -24,14 +25,6 @@ use Symfony\Component\Lock\StoreInterface; */ class RedisStore implements StoreInterface { - private static $defaultConnectionOptions = array( - 'class' => null, - 'persistent' => 0, - 'persistent_id' => null, - 'timeout' => 30, - 'read_timeout' => 0, - 'retry_interval' => 0, - ); private $redis; private $initialTtl; @@ -41,7 +34,7 @@ class RedisStore implements StoreInterface */ public function __construct($redisClient, $initialTtl = 300.0) { - if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) { + 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))); } @@ -139,7 +132,7 @@ class RedisStore implements StoreInterface */ private function evaluate($script, $resource, array $args) { - if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster) { + if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster || $this->redis instanceof RedisProxy) { return $this->redis->eval($script, array_merge(array($resource), $args), 1); } diff --git a/src/Symfony/Component/Lock/Store/StoreFactory.php b/src/Symfony/Component/Lock/Store/StoreFactory.php index eccaeaf974..a768d19746 100644 --- a/src/Symfony/Component/Lock/Store/StoreFactory.php +++ b/src/Symfony/Component/Lock/Store/StoreFactory.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Lock\Store; +use Symfony\Component\Cache\Traits\RedisProxy; use Symfony\Component\Lock\Exception\InvalidArgumentException; /** @@ -27,7 +28,7 @@ class StoreFactory */ public static function createStore($connection) { - if ($connection instanceof \Redis || $connection instanceof \RedisArray || $connection instanceof \RedisCluster || $connection instanceof \Predis\Client) { + if ($connection instanceof \Redis || $connection instanceof \RedisArray || $connection instanceof \RedisCluster || $connection instanceof \Predis\Client || $connection instanceof RedisProxy) { return new RedisStore($connection); } if ($connection instanceof \Memcached) {