From 79e99d2af42bd6e7510730df37af6374e89eea0b Mon Sep 17 00:00:00 2001 From: Petr Levtonov Date: Sun, 28 Mar 2021 21:11:18 +0200 Subject: [PATCH] Add phpredis serialization and compression config support --- src/Illuminate/Cache/PhpRedisLock.php | 84 +-------- .../Redis/Connections/PhpRedisConnection.php | 93 ++++++++++ .../Redis/Connectors/PhpRedisConnector.php | 24 +++ tests/Redis/RedisConnectionTest.php | 174 ++++++++++++++++-- 4 files changed, 285 insertions(+), 90 deletions(-) diff --git a/src/Illuminate/Cache/PhpRedisLock.php b/src/Illuminate/Cache/PhpRedisLock.php index 9d0215f37d6d..33c767546796 100644 --- a/src/Illuminate/Cache/PhpRedisLock.php +++ b/src/Illuminate/Cache/PhpRedisLock.php @@ -3,11 +3,16 @@ namespace Illuminate\Cache; use Illuminate\Redis\Connections\PhpRedisConnection; -use Redis; -use UnexpectedValueException; class PhpRedisLock extends RedisLock { + /** + * The phpredis factory implementation. + * + * @var \Illuminate\Redis\Connections\PhpredisConnection + */ + protected $redis; + /** * Create a new phpredis lock instance. * @@ -31,80 +36,7 @@ public function release() LuaScripts::releaseLock(), 1, $this->name, - $this->serializedAndCompressedOwner() + ...$this->redis->serializeAndCompress([$this->owner]) ); } - - /** - * Get the owner key, serialized and compressed. - * - * @return string - */ - protected function serializedAndCompressedOwner(): string - { - $client = $this->redis->client(); - - $owner = $client->_serialize($this->owner); - - // https://github.com/phpredis/phpredis/issues/1938 - if ($this->compressed()) { - if ($this->lzfCompressed()) { - $owner = \lzf_compress($owner); - } elseif ($this->zstdCompressed()) { - $owner = \zstd_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); - } elseif ($this->lz4Compressed()) { - $owner = \lz4_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); - } else { - throw new UnexpectedValueException(sprintf( - 'Unknown phpredis compression in use [%d]. Unable to release lock.', - $client->getOption(Redis::OPT_COMPRESSION) - )); - } - } - - return $owner; - } - - /** - * Determine if compression is enabled. - * - * @return bool - */ - protected function compressed(): bool - { - return $this->redis->client()->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; - } - - /** - * Determine if LZF compression is enabled. - * - * @return bool - */ - protected function lzfCompressed(): bool - { - return defined('Redis::COMPRESSION_LZF') && - $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; - } - - /** - * Determine if ZSTD compression is enabled. - * - * @return bool - */ - protected function zstdCompressed(): bool - { - return defined('Redis::COMPRESSION_ZSTD') && - $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; - } - - /** - * Determine if LZ4 compression is enabled. - * - * @return bool - */ - protected function lz4Compressed(): bool - { - return defined('Redis::COMPRESSION_LZ4') && - $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; - } } diff --git a/src/Illuminate/Redis/Connections/PhpRedisConnection.php b/src/Illuminate/Redis/Connections/PhpRedisConnection.php index 86e239e6f118..37abb5ec48b5 100644 --- a/src/Illuminate/Redis/Connections/PhpRedisConnection.php +++ b/src/Illuminate/Redis/Connections/PhpRedisConnection.php @@ -9,6 +9,7 @@ use Redis; use RedisCluster; use RedisException; +use UnexpectedValueException; /** * @mixin \Redis @@ -573,4 +574,96 @@ public function __call($method, $parameters) { return parent::__call(strtolower($method), $parameters); } + + /** + * Prepares values to be used with e.g. the `eval` command, because the + * phpredis extension does not do it for us. + * + * @param array $values + * @return array + */ + public function serializeAndCompress(array $values): array + { + if (empty($values)) { + return $values; + } + + // https://github.com/phpredis/phpredis/issues/1938 + if ($this->compressed()) { + if ($this->lzfCompressed()) { + $processor = function ($value) { + return \lzf_compress($this->client->_serialize($value)); + }; + } elseif ($this->zstdCompressed()) { + $compressionLevel = $this->client->getOption(Redis::OPT_COMPRESSION_LEVEL); + $processor = function ($value) use ($compressionLevel) { + return \zstd_compress( + $this->client->_serialize($value), + $compressionLevel === 0 ? Redis::COMPRESSION_ZSTD_DEFAULT : $compressionLevel + ); + }; + } elseif ($this->lz4Compressed()) { + $processor = function ($value) { + return \lz4_compress( + $this->client->_serialize($value), + $this->client->getOption(Redis::OPT_COMPRESSION_LEVEL) + ); + }; + } else { + throw new UnexpectedValueException(sprintf( + 'Unknown phpredis compression in use [%d].', + $this->client->getOption(Redis::OPT_COMPRESSION) + )); + } + } else { + $processor = function ($value) { + return $this->client->_serialize($value); + }; + } + + return array_map($processor, $values); + } + + /** + * Determine if compression is enabled. + * + * @return bool + */ + protected function compressed(): bool + { + return $this->client->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; + } + + /** + * Determine if LZF compression is enabled. + * + * @return bool + */ + protected function lzfCompressed(): bool + { + return defined('Redis::COMPRESSION_LZF') && + $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; + } + + /** + * Determine if ZSTD compression is enabled. + * + * @return bool + */ + protected function zstdCompressed(): bool + { + return defined('Redis::COMPRESSION_ZSTD') && + $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; + } + + /** + * Determine if LZ4 compression is enabled. + * + * @return bool + */ + protected function lz4Compressed(): bool + { + return defined('Redis::COMPRESSION_LZ4') && + $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; + } } diff --git a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php index 37a980a1d779..1c48bdfaaa51 100644 --- a/src/Illuminate/Redis/Connectors/PhpRedisConnector.php +++ b/src/Illuminate/Redis/Connectors/PhpRedisConnector.php @@ -106,6 +106,18 @@ protected function createClient(array $config) if (! empty($config['name'])) { $client->client('SETNAME', $config['name']); } + + if (array_key_exists('serializer', $config)) { + $client->setOption(Redis::OPT_SERIALIZER, $config['serializer']); + } + + if (array_key_exists('compression', $config)) { + $client->setOption(Redis::OPT_COMPRESSION, $config['compression']); + } + + if (array_key_exists('compression_level', $config)) { + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, $config['compression_level']); + } }); } @@ -184,6 +196,18 @@ protected function createRedisClusterInstance(array $servers, array $options) if (! empty($options['name'])) { $client->client('SETNAME', $options['name']); } + + if (array_key_exists('serializer', $options)) { + $client->setOption(RedisCluster::OPT_SERIALIZER, $options['serializer']); + } + + if (array_key_exists('compression', $options)) { + $client->setOption(RedisCluster::OPT_COMPRESSION, $options['compression']); + } + + if (array_key_exists('compression_level', $options)) { + $client->setOption(RedisCluster::OPT_COMPRESSION_LEVEL, $options['compression_level']); + } }); } diff --git a/tests/Redis/RedisConnectionTest.php b/tests/Redis/RedisConnectionTest.php index 38ab3fb7452b..792b6d9b920d 100644 --- a/tests/Redis/RedisConnectionTest.php +++ b/tests/Redis/RedisConnectionTest.php @@ -6,6 +6,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Redis\Connections\Connection; +use Illuminate\Redis\Connections\PhpRedisConnection; use Illuminate\Redis\RedisManager; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -469,7 +470,18 @@ public function testItGetsMultipleKeys() public function testItRunsEval() { foreach ($this->connections() as $redis) { - $redis->eval('redis.call("set", KEYS[1], ARGV[1])', 1, 'name', 'mohamed'); + if ($redis instanceof PhpRedisConnection) { + // User must decide what needs to be serialized and compressed. + $redis->eval( + 'redis.call("set", KEYS[1], ARGV[1])', + 1, + 'name', + ...$redis->serializeAndCompress(['mohamed']) + ); + } else { + $redis->eval('redis.call("set", KEYS[1], ARGV[1])', 1, 'name', 'mohamed'); + } + $this->assertSame('mohamed', $redis->get('name')); $redis->flushall(); @@ -777,7 +789,7 @@ public function connections() $host = env('REDIS_HOST', '127.0.0.1'); $port = env('REDIS_PORT', 6379); - $prefixedPhpredis = new RedisManager(new Application, 'phpredis', [ + $connections[] = (new RedisManager(new Application, 'phpredis', [ 'cluster' => false, 'default' => [ 'url' => "redis://user@$host:$port", @@ -787,9 +799,9 @@ public function connections() 'options' => ['prefix' => 'laravel:'], 'timeout' => 0.5, ], - ]); + ]))->connection(); - $persistentPhpRedis = new RedisManager(new Application, 'phpredis', [ + $connections['persistent'] = (new RedisManager(new Application, 'phpredis', [ 'cluster' => false, 'default' => [ 'host' => $host, @@ -800,9 +812,9 @@ public function connections() 'persistent' => true, 'persistent_id' => 'laravel', ], - ]); + ]))->connection(); - $serializerPhpRedis = new RedisManager(new Application, 'phpredis', [ + $connections[] = (new RedisManager(new Application, 'phpredis', [ 'cluster' => false, 'default' => [ 'host' => $host, @@ -811,9 +823,9 @@ public function connections() 'options' => ['serializer' => Redis::SERIALIZER_JSON], 'timeout' => 0.5, ], - ]); + ]))->connection(); - $scanRetryPhpRedis = new RedisManager(new Application, 'phpredis', [ + $connections[] = (new RedisManager(new Application, 'phpredis', [ 'cluster' => false, 'default' => [ 'host' => $host, @@ -822,12 +834,146 @@ public function connections() 'options' => ['scan' => Redis::SCAN_RETRY], 'timeout' => 0.5, ], - ]); - - $connections[] = $prefixedPhpredis->connection(); - $connections[] = $serializerPhpRedis->connection(); - $connections[] = $scanRetryPhpRedis->connection(); - $connections['persistent'] = $persistentPhpRedis->connection(); + ]))->connection(); + + if (defined('Redis::COMPRESSION_LZF')) { + $connections['compression_lzf'] = (new RedisManager(new Application, 'phpredis', [ + 'cluster' => false, + 'default' => [ + 'host' => $host, + 'port' => $port, + 'database' => 9, + 'options' => [ + 'compression' => Redis::COMPRESSION_LZF, + 'name' => 'compression_lzf', + ], + 'timeout' => 0.5, + ], + ]))->connection(); + } + + if (defined('Redis::COMPRESSION_ZSTD')) { + $connections['compression_zstd'] = (new RedisManager(new Application, 'phpredis', [ + 'cluster' => false, + 'default' => [ + 'host' => $host, + 'port' => $port, + 'database' => 10, + 'options' => [ + 'compression' => Redis::COMPRESSION_ZSTD, + 'name' => 'compression_zstd', + ], + 'timeout' => 0.5, + ], + ]))->connection(); + + $connections['compression_zstd_default'] = (new RedisManager(new Application, 'phpredis', [ + 'cluster' => false, + 'default' => [ + 'host' => $host, + 'port' => $port, + 'database' => 11, + 'options' => [ + 'compression' => Redis::COMPRESSION_ZSTD, + 'compression_level' => Redis::COMPRESSION_ZSTD_DEFAULT, + 'name' => 'compression_zstd_default', + ], + 'timeout' => 0.5, + ], + ]))->connection(); + + $connections['compression_zstd_min'] = (new RedisManager(new Application, 'phpredis', [ + 'cluster' => false, + 'default' => [ + 'host' => $host, + 'port' => $port, + 'database' => 12, + 'options' => [ + 'compression' => Redis::COMPRESSION_ZSTD, + 'compression_level' => Redis::COMPRESSION_ZSTD_MIN, + 'name' => 'compression_zstd_min', + ], + 'timeout' => 0.5, + ], + ]))->connection(); + + $connections['compression_zstd_max'] = (new RedisManager(new Application, 'phpredis', [ + 'cluster' => false, + 'default' => [ + 'host' => $host, + 'port' => $port, + 'database' => 13, + 'options' => [ + 'compression' => Redis::COMPRESSION_ZSTD, + 'compression_level' => Redis::COMPRESSION_ZSTD_MAX, + 'name' => 'compression_zstd_max', + ], + 'timeout' => 0.5, + ], + ]))->connection(); + } + + // TODO: Uncomment after https://github.com/phpredis/phpredis/issues/1939 + // if (defined('Redis::COMPRESSION_LZ4')) { + // $connections['compression_lz4'] = (new RedisManager(new Application, 'phpredis', [ + // 'cluster' => false, + // 'default' => [ + // 'host' => $host, + // 'port' => $port, + // 'database' => 14, + // 'options' => [ + // 'compression' => Redis::COMPRESSION_LZ4, + // 'name' => 'compression_lz4', + // ], + // 'timeout' => 0.5, + // ], + // ]))->connection(); + + // $connections['compression_lz4_default'] = (new RedisManager(new Application, 'phpredis', [ + // 'cluster' => false, + // 'default' => [ + // 'host' => $host, + // 'port' => $port, + // 'database' => 15, + // 'options' => [ + // 'compression' => Redis::COMPRESSION_LZ4, + // 'compression_level' => 0, + // 'name' => 'compression_lz4_default', + // ], + // 'timeout' => 0.5, + // ], + // ]))->connection(); + + // $connections['compression_lz4_min'] = (new RedisManager(new Application, 'phpredis', [ + // 'cluster' => false, + // 'default' => [ + // 'host' => $host, + // 'port' => $port, + // 'database' => 16, + // 'options' => [ + // 'compression' => Redis::COMPRESSION_LZ4, + // 'compression_level' => 1, + // 'name' => 'compression_lz4_min', + // ], + // 'timeout' => 0.5, + // ], + // ]))->connection(); + + // $connections['compression_lz4_max'] = (new RedisManager(new Application, 'phpredis', [ + // 'cluster' => false, + // 'default' => [ + // 'host' => $host, + // 'port' => $port, + // 'database' => 17, + // 'options' => [ + // 'compression' => Redis::COMPRESSION_LZ4, + // 'compression_level' => 12, + // 'name' => 'compression_lz4_max', + // ], + // 'timeout' => 0.5, + // ], + // ]))->connection(); + // } return $connections; }