From d15d74f8439c64b02ab60e324f15aa251ab0bf2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Tue, 18 May 2021 23:15:21 +0200 Subject: [PATCH 01/21] qa: add redis cluster for CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 4 ++++ .laminas-ci.json | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index a8acc90..cb0e422 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -29,6 +29,9 @@ jobs: ports: - 6379:6379 options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 + redis-cluster: + image: vishnunair/docker-redis-cluster:latest + options: --health-cmd="redis-cli -c -p 6379 cluster nodes" --health-interval=10s --health-timeout=5s --health-retries=3 strategy: fail-fast: false matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} @@ -39,3 +42,4 @@ jobs: job: ${{ matrix.job }} env: TESTS_LAMINAS_CACHE_REDIS_HOST: redis + TESTS_LAMINAS_CACHE_REDIS_CLUSTER_NODENAME: cluster diff --git a/.laminas-ci.json b/.laminas-ci.json index 063369f..eddbf6b 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,5 +1,11 @@ { "extensions": [ - "redis" + "redis", + "igbinary" + ], + "ini": [ + "redis.clusters.seeds = 'cluster[]=redis-cluster:6379&cluster[]=redis-cluster:6380&cluster[]=redis-cluster:6381&cluster[]=redis-cluster:6382&cluster[]=redis-cluster:6383&cluster[]=redis-cluster:6384'", + "redis.clusters.timeout = 'cluster=5'", + "redis.clusters.read_timeout = 'cluster=10'" ] } From da1e796349d92e58c7dc19952bb095eb99263d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Tue, 18 May 2021 23:16:21 +0200 Subject: [PATCH 02/21] feature: add `RedisCluster` adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../InvalidRedisConfigurationException.php | 45 ++ src/Exception/MetadataErrorException.php | 14 + src/Exception/RedisRuntimeException.php | 29 + src/RedisCluster.php | 555 ++++++++++++++++++ src/RedisClusterOptions.php | 188 ++++++ src/RedisClusterOptionsFromIni.php | 98 ++++ src/RedisClusterResourceManager.php | 244 ++++++++ src/RedisClusterResourceManagerInterface.php | 22 + .../RedisClusterWithPhpIgbinaryTest.php | 31 + .../RedisClusterWithPhpSerializeTest.php | 31 + .../RedisClusterWithoutSerializerTest.php | 31 + .../RedisClusterWithPhpIgbinaryTest.php | 58 ++ .../RedisClusterWithPhpSerializerTest.php | 48 ++ .../RedisClusterWithoutSerializerTest.php | 48 ++ .../RedisClusterStorageCreationTrait.php | 59 ++ test/integration/Redis/RedisClusterTest.php | 170 ++++++ ...RedisConfigurationFromEnvironmentTrait.php | 60 ++ .../Redis/RedisStorageCreationTrait.php | 43 ++ .../InvalidConfigurationExceptionTest.php | 18 + test/unit/RedisClusterOptionsFromIniTest.php | 80 +++ test/unit/RedisClusterOptionsTest.php | 80 +++ test/unit/RedisClusterResourceManagerTest.php | 93 +++ test/unit/RedisClusterTest.php | 80 +++ 23 files changed, 2125 insertions(+) create mode 100644 src/Exception/InvalidRedisConfigurationException.php create mode 100644 src/Exception/MetadataErrorException.php create mode 100644 src/Exception/RedisRuntimeException.php create mode 100644 src/RedisCluster.php create mode 100644 src/RedisClusterOptions.php create mode 100644 src/RedisClusterOptionsFromIni.php create mode 100644 src/RedisClusterResourceManager.php create mode 100644 src/RedisClusterResourceManagerInterface.php create mode 100644 test/integration/Psr/CacheItemPool/RedisClusterWithPhpIgbinaryTest.php create mode 100644 test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php create mode 100644 test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php create mode 100644 test/integration/Psr/SimpleCache/RedisClusterWithPhpIgbinaryTest.php create mode 100644 test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php create mode 100644 test/integration/Psr/SimpleCache/RedisClusterWithoutSerializerTest.php create mode 100644 test/integration/Redis/RedisClusterStorageCreationTrait.php create mode 100644 test/integration/Redis/RedisClusterTest.php create mode 100644 test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php create mode 100644 test/integration/Redis/RedisStorageCreationTrait.php create mode 100644 test/unit/Exception/InvalidConfigurationExceptionTest.php create mode 100644 test/unit/RedisClusterOptionsFromIniTest.php create mode 100644 test/unit/RedisClusterOptionsTest.php create mode 100644 test/unit/RedisClusterResourceManagerTest.php create mode 100644 test/unit/RedisClusterTest.php diff --git a/src/Exception/InvalidRedisConfigurationException.php b/src/Exception/InvalidRedisConfigurationException.php new file mode 100644 index 0000000..16a6de0 --- /dev/null +++ b/src/Exception/InvalidRedisConfigurationException.php @@ -0,0 +1,45 @@ +getLastError() ?? $exception->getMessage(); + + return new self($message, (int) $exception->getCode(), $exception); + } + + public static function connectionFailed(Throwable $exception): self + { + return new self( + 'Could not establish connection to redis cluster', + (int) $exception->getCode(), + $exception + ); + } +} diff --git a/src/RedisCluster.php b/src/RedisCluster.php new file mode 100644 index 0000000..efa2f9e --- /dev/null +++ b/src/RedisCluster.php @@ -0,0 +1,555 @@ +|RedisClusterOptions|Traversable $options + */ + public function __construct($options = null) + { + /** @psalm-suppress PossiblyInvalidArgument */ + parent::__construct($options); + $eventManager = $this->getEventManager(); + + $eventManager->attach('option', function (): void { + $this->resource = null; + $this->capabilities = null; + $this->capabilityMarker = null; + $this->namespacePrefix = null; + }); + } + + /** + * @param array|Traversable|AdapterOptions $options + * @return self + */ + public function setOptions($options) + { + if (! $options instanceof RedisClusterOptions) { + /** @psalm-suppress PossiblyInvalidArgument */ + $options = new RedisClusterOptions($options); + } + + $options->setAdapter($this); + + parent::setOptions($options); + return $this; + } + + /** + * In RedisCluster, it is totally okay if just one master is being flushed. If one master is not reachable, it will + * re-sync if that master is coming back online. + */ + public function flush(): bool + { + $resource = $this->getRedisResource(); + $anyMasterSuccessfullyFlushed = false; + /** @psalm-var array $masters */ + $masters = $resource->_masters(); + + foreach ($masters as [$host, $port]) { + $redis = new Redis(); + try { + $redis->connect($host, $port); + } catch (RedisException $exception) { + continue; + } + + if (! $redis->flushDB()) { + continue; + } + + $anyMasterSuccessfullyFlushed = true; + } + + return $anyMasterSuccessfullyFlushed; + } + + private function getRedisResource(): RedisClusterFromExtension + { + if ($this->resource instanceof RedisClusterFromExtension) { + return $this->resource; + } + + $options = $this->getOptions(); + $resourceManager = $options->getResourceManager(); + + try { + return $this->resource = $resourceManager->getResource(); + } catch (RedisClusterException $exception) { + throw RedisRuntimeException::connectionFailed($exception); + } + } + + public function getOptions(): RedisClusterOptions + { + $options = parent::getOptions(); + if (! $options instanceof RedisClusterOptions) { + $options = new RedisClusterOptions($options); + $this->options = $options; + } + + return $options; + } + + /** + * @param string $namespace + */ + public function clearByNamespace($namespace): bool + { + /** @psalm-suppress RedundantCast */ + $namespace = (string) $namespace; + if ($namespace === '') { + throw new Exception\InvalidArgumentException('Invalid namespace provided'); + } + + return $this->searchAndDelete('', $namespace); + } + + /** + * @param string $prefix + */ + public function clearByPrefix($prefix): bool + { + /** @psalm-suppress RedundantCast */ + $prefix = (string) $prefix; + if ($prefix === '') { + throw new Exception\InvalidArgumentException('No prefix given'); + } + + $options = $this->getOptions(); + + return $this->searchAndDelete($prefix, $options->getNamespace()); + } + + /** + * @param string $normalizedKey + * @param bool|null $success + * @param mixed|null $casToken + * @return mixed|null + */ + protected function internalGetItem(&$normalizedKey, &$success = null, &$casToken = null) + { + $normalizedKeys = [$normalizedKey]; + $values = $this->internalGetItems($normalizedKeys); + if (! array_key_exists($normalizedKey, $values)) { + $success = false; + return null; + } + + /** @psalm-suppress MixedAssignment */ + $value = $casToken = $values[$normalizedKey]; + $success = true; + return $value; + } + + protected function internalGetItems(array &$normalizedKeys): array + { + /** @var array $normalizedKeys */ + $normalizedKeys = array_values($normalizedKeys); + $namespacedKeys = []; + foreach ($normalizedKeys as $normalizedKey) { + /** @psalm-suppress RedundantCast */ + $namespacedKeys[] = $this->key((string) $normalizedKey); + } + + $redis = $this->getRedisResource(); + + try { + /** @var array $resultsByIndex */ + $resultsByIndex = $redis->mget($namespacedKeys); + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + + $result = []; + /** @psalm-suppress MixedAssignment */ + foreach ($resultsByIndex as $keyIndex => $value) { + $normalizedKey = $normalizedKeys[$keyIndex]; + $namespacedKey = $namespacedKeys[$keyIndex]; + if ($value === false && ! $this->falseReturnValueIsIntended($redis, $namespacedKey)) { + continue; + } + + /** @psalm-suppress MixedAssignment */ + $result[$normalizedKey] = $value; + } + + return $result; + } + + private function key(string $key): string + { + if ($this->namespacePrefix !== null) { + return $this->namespacePrefix . $key; + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $this->namespacePrefix = $namespace; + if ($namespace !== '') { + $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); + } + + return $this->namespacePrefix . $key; + } + + /** + * @param string $normalizedKey + * @param mixed $value + */ + protected function internalSetItem(&$normalizedKey, &$value): bool + { + $redis = $this->getRedisResource(); + $options = $this->getOptions(); + $ttl = (int) $options->getTtl(); + + $namespacedKey = $this->key($normalizedKey); + try { + if ($ttl) { + /** + * @psalm-suppress MixedArgument + * Redis & RedisCluster do allow mixed values when a serializer is configured. + */ + return $redis->setex($namespacedKey, $ttl, $value); + } + + /** + * @psalm-suppress MixedArgument + * Redis & RedisCluster do allow mixed values when a serializer is configured. + */ + return $redis->set($namespacedKey, $value); + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + } + + /** + * @param string $normalizedKey + */ + protected function internalRemoveItem(&$normalizedKey): bool + { + $redis = $this->getRedisResource(); + + try { + return $redis->del($this->key($normalizedKey)) === 1; + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + } + + /** + * @param string $normalizedKey + */ + protected function internalHasItem(&$normalizedKey): bool + { + $redis = $this->getRedisResource(); + + try { + /** @psalm-var 0|1 $exists */ + $exists = $redis->exists($this->key($normalizedKey)); + return (bool) $exists; + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + } + + protected function internalSetItems(array &$normalizedKeyValuePairs): array + { + $redis = $this->getRedisResource(); + $ttl = (int) $this->getOptions()->getTtl(); + + $namespacedKeyValuePairs = []; + /** @psalm-suppress MixedAssignment */ + foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { + $namespacedKeyValuePairs[$this->key((string) $normalizedKey)] = $value; + } + + $successByKey = []; + + try { + /** @psalm-suppress MixedAssignment */ + foreach ($namespacedKeyValuePairs as $key => $value) { + if ($ttl) { + /** + * @psalm-suppress MixedArgument + * Redis & RedisCluster do allow mixed values when a serializer is configured. + */ + $successByKey[$key] = $redis->setex($key, $ttl, $value); + continue; + } + + /** + * @psalm-suppress MixedArgument + * Redis & RedisCluster do allow mixed values when a serializer is configured. + */ + $successByKey[$key] = $redis->set($key, $value); + } + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + + $statuses = []; + foreach ($successByKey as $key => $success) { + if ($success) { + continue; + } + + $statuses[] = $key; + } + + return $statuses; + } + + protected function internalGetCapabilities(): Capabilities + { + if ($this->capabilities !== null) { + return $this->capabilities; + } + + $this->capabilityMarker = new stdClass(); + $redisVersion = $this->getRedisVersion(); + $serializer = $this->hasSerializationSupport(); + $redisVersionLessThanV2 = version_compare($redisVersion, '2.0', '<'); + $minTtl = $redisVersionLessThanV2 ? 0 : 1; + $supportedMetadata = ! $redisVersionLessThanV2 ? ['ttl'] : []; + + $this->capabilities = new Capabilities( + $this, + $this->capabilityMarker, + [ + 'supportedDatatypes' => $this->supportedDatatypes($serializer), + 'supportedMetadata' => $supportedMetadata, + 'minTtl' => $minTtl, + 'maxTtl' => 0, + 'staticTtl' => true, + 'ttlPrecision' => 1, + 'useRequestTime' => false, + 'maxKeyLength' => 255, + 'namespaceIsPrefix' => true, + ] + ); + + return $this->capabilities; + } + + /** + * @psalm-return array + */ + private function supportedDatatypes(bool $serializer): array + { + if ($serializer) { + return [ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => 'array', + 'object' => 'object', + 'resource' => false, + ]; + } + + return [ + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ]; + } + + /** + * @return mixed + */ + private function getLibOption(int $option) + { + $options = $this->getOptions(); + $resourceManager = $options->getResourceManager(); + return $resourceManager->getLibOption($option); + } + + private function searchAndDelete(string $prefix, string $namespace): bool + { + $redis = $this->getRedisResource(); + $options = $this->getOptions(); + + $prefix = $namespace === '' ? '' : $namespace . $options->getNamespaceSeparator() . $prefix; + + /** @var array $keys */ + $keys = $redis->keys($prefix . '*'); + if (! $keys) { + return true; + } + + return $redis->del($keys) === count($keys); + } + + private function clusterException( + RedisClusterException $exception, + RedisClusterFromExtension $redis + ): Exception\RuntimeException { + return RedisRuntimeException::fromClusterException($exception, $redis); + } + + /** + * This method verifies that the return value from {@see RedisClusterFromExtension::get} or + * {@see RedisClusterFromExtension::mget} is `false` because the key does not exist or because the keys value + * is `false` at type-level. + */ + private function falseReturnValueIsIntended(RedisClusterFromExtension $redis, string $key): bool + { + /** @psalm-suppress MixedAssignment */ + $serializer = $this->getLibOption(RedisClusterFromExtension::OPT_SERIALIZER); + if ($serializer === RedisClusterFromExtension::SERIALIZER_NONE) { + return false; + } + + try { + /** @psalm-var 0|1 $exists */ + $exists = $redis->exists($key); + return (bool) $exists; + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + } + + /** + * Internal method to get metadata of an item. + * + * @param string $normalizedKey + * @return array|bool Metadata on success, false on failure + * @throws Exception\ExceptionInterface + */ + protected function internalGetMetadata(&$normalizedKey) + { + /** @psalm-suppress RedundantCastGivenDocblockType */ + $namespacedKey = $this->key((string) $normalizedKey); + $redis = $this->getRedisResource(); + $metadata = []; + $capabilities = $this->internalGetCapabilities(); + try { + if (in_array('ttl', $capabilities->getSupportedMetadata(), true)) { + $ttl = $this->detectTtlForKey($redis, $namespacedKey); + $metadata['ttl'] = $ttl; + } + } catch (MetadataErrorException $exception) { + return false; + } catch (RedisClusterException $exception) { + throw $this->clusterException($exception, $redis); + } + + return $metadata; + } + + private function detectTtlForKey(RedisClusterFromExtension $redis, string $namespacedKey): ?int + { + $redisVersion = $this->getRedisVersion(); + $ttl = $redis->ttl($namespacedKey); + + // redis >= 2.8 + // The command 'ttl' returns -2 if the item does not exist + // and -1 if the item has no associated expire + if (version_compare($redisVersion, '2.8', '>=')) { + if ($ttl <= -2) { + throw new MetadataErrorException(); + } + + return $ttl === -1 ? null : $ttl; + } + + // redis >= 2.6, < 2.8 + // The command 'tttl' returns -1 if the item does not exist or the item has no associated expire + if (version_compare($redisVersion, '2.6', '>=')) { + if ($ttl <= -1) { + if (! $this->internalHasItem($namespacedKey)) { + throw new MetadataErrorException(); + } + + return null; + } + + return $ttl; + } + + // redis >= 2, < 2.6 + // The command 'pttl' is not supported but 'ttl' + // The command 'ttl' returns 0 if the item does not exist same as if the item is going to be expired + // NOTE: In case of ttl=0 we return false because the item is going to be expired in a very near future + // and then doesn't exist any more + if (version_compare($redisVersion, '2', '>=')) { + if ($ttl <= -1) { + if (! $this->internalHasItem($namespacedKey)) { + throw new MetadataErrorException(); + } + + return null; + } + + return $ttl; + } + + throw new Exception\LogicException( + sprintf( + '%s must not be called for current redis version.', + __METHOD__ + ) + ); + } + + private function getRedisVersion(): string + { + $options = $this->getOptions(); + $resourceManager = $options->getResourceManager(); + return $resourceManager->getVersion(); + } + + private function hasSerializationSupport(): bool + { + $options = $this->getOptions(); + $resourceManager = $options->getResourceManager(); + return $resourceManager->hasSerializationSupport($this); + } +} diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php new file mode 100644 index 0000000..af69bd6 --- /dev/null +++ b/src/RedisClusterOptions.php @@ -0,0 +1,188 @@ + */ + private $seeds = []; + + /** @var string */ + private $version = ''; + + /** @psalm-var array */ + private $libOptions = []; + + /** @var RedisClusterResourceManagerInterface|null */ + private $resourceManager; + + /** + * @param array|Traversable|null|AdapterOptions $options + * @psalm-param array|Traversable|null|AdapterOptions $options + */ + public function __construct($options = null) + { + if ($options instanceof AdapterOptions) { + $options = $options->toArray(); + } + + /** @psalm-suppress InvalidArgument */ + parent::__construct($options); + $hasNodename = $this->hasNodename(); + $hasSeeds = $this->seeds() !== []; + + if (! $hasNodename && ! $hasSeeds) { + throw InvalidRedisConfigurationException::fromMissingRequiredValues(); + } + + if ($hasNodename && $hasSeeds) { + throw InvalidRedisConfigurationException::nodenameAndSeedsProvided(); + } + } + + public function setTimeout(float $timeout): void + { + $this->timeout = $timeout; + $this->triggerOptionEvent('timeout', $timeout); + } + + public function setReadTimeout(float $readTimeout): void + { + $this->readTimeout = $readTimeout; + $this->triggerOptionEvent('read_timeout', $readTimeout); + } + + public function setPersistent(bool $persistent): void + { + $this->persistent = $persistent; + } + + public function getNamespaceSeparator(): string + { + return $this->namespaceSeparator; + } + + public function setNamespaceSeparator(string $namespaceSeparator): void + { + if ($this->namespaceSeparator === $namespaceSeparator) { + return; + } + + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); + $this->namespaceSeparator = $namespaceSeparator; + } + + public function hasNodename(): bool + { + return $this->nodename !== ''; + } + + public function nodename(): string + { + return $this->nodename; + } + + public function setNodename(string $nodename): void + { + $this->nodename = $nodename; + $this->triggerOptionEvent('nodename', $nodename); + } + + public function timeout(): float + { + return $this->timeout; + } + + public function readTimeout(): float + { + return $this->readTimeout; + } + + public function persistent(): bool + { + return $this->persistent; + } + + /** + * @return array + * @psalm-return list + */ + public function seeds(): array + { + return $this->seeds; + } + + /** + * @param array $seeds + * @psalm-param list $seeds + */ + public function setSeeds(array $seeds): void + { + $this->seeds = $seeds; + + $this->triggerOptionEvent('seeds', $seeds); + } + + /** + * @param non-empty-string $version + */ + public function setRedisVersion(string $version): void + { + $this->version = $version; + } + + public function redisVersion(): string + { + return $this->version; + } + + /** + * @psalm-param array $options + */ + public function setLibOptions(array $options): void + { + $this->libOptions = $options; + } + + /** + * @psalm-return array + */ + public function libOptions(): array + { + return $this->libOptions; + } + + public function setResourceManager(RedisClusterResourceManagerInterface $resourceManager): void + { + $this->resourceManager = $resourceManager; + } + + public function getResourceManager(): RedisClusterResourceManagerInterface + { + if ($this->resourceManager) { + return $this->resourceManager; + } + + return $this->resourceManager = new RedisClusterResourceManager($this); + } +} diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php new file mode 100644 index 0000000..1f7f7c6 --- /dev/null +++ b/src/RedisClusterOptionsFromIni.php @@ -0,0 +1,98 @@ +> */ + private $seedsByNodename; + + /** @psalm-var array */ + private $timeoutByNodename; + + /** @psalm-var array */ + private $readTimeoutByNodename; + + public function __construct() + { + $seedsConfiguration = ini_get('redis.clusters.seeds'); + if (! is_string($seedsConfiguration)) { + $seedsConfiguration = ''; + } + + if ($seedsConfiguration === '') { + throw InvalidRedisConfigurationException::fromMissingSeedsConfiguration(); + } + + $seedsByNodename = []; + parse_str($seedsConfiguration, $seedsByNodename); + /** @psalm-var non-empty-array> $seedsByNodename */ + $this->seedsByNodename = $seedsByNodename; + + $timeoutConfiguration = ini_get('redis.clusters.timeout'); + if (! is_string($timeoutConfiguration)) { + $timeoutConfiguration = ''; + } + + $timeoutByNodename = []; + parse_str($timeoutConfiguration, $timeoutByNodename); + foreach ($timeoutByNodename as $nodename => $timeout) { + assert($nodename !== '' && is_numeric($timeout)); + $timeoutByNodename[$nodename] = (float) $timeout; + } + /** @psalm-var array $timeoutByNodename */ + $this->timeoutByNodename = $timeoutByNodename; + + $readTimeoutConfiguration = ini_get('redis.clusters.read_timeout'); + if (! is_string($readTimeoutConfiguration)) { + $readTimeoutConfiguration = ''; + } + + $readTimeoutByNodename = []; + parse_str($readTimeoutConfiguration, $readTimeoutByNodename); + foreach ($readTimeoutByNodename as $nodename => $readTimeout) { + assert($nodename !== '' && is_numeric($readTimeout)); + $readTimeoutByNodename[$nodename] = (float) $readTimeout; + } + + /** @psalm-var array $readTimeoutByNodename */ + $this->readTimeoutByNodename = $readTimeoutByNodename; + } + + /** + * @return array + * @psalm-return list + */ + public function seeds(string $nodename): array + { + $seeds = $this->seedsByNodename[$nodename] ?? []; + if (! $seeds) { + throw InvalidRedisConfigurationException::forMissingSeedsForNodename($nodename); + } + + return $seeds; + } + + public function timeout(string $nodename, float $fallback): float + { + return $this->timeoutByNodename[$nodename] ?? $fallback; + } + + public function readTimeout(string $nodename, float $fallback): float + { + return $this->readTimeoutByNodename[$nodename] ?? $fallback; + } +} diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php new file mode 100644 index 0000000..8b03c23 --- /dev/null +++ b/src/RedisClusterResourceManager.php @@ -0,0 +1,244 @@ +&array{redis_version:string} + */ +final class RedisClusterResourceManager implements RedisClusterResourceManagerInterface +{ + /** @var array|null */ + private static $clusterOptionsCache; + + /** @var RedisClusterOptions */ + private $options; + + /** @psalm-var array */ + private $libraryOptions = []; + + public function __construct(RedisClusterOptions $options) + { + $this->options = $options; + if (! extension_loaded('redis')) { + throw new ExtensionNotLoadedException('Redis extension is not loaded'); + } + } + + /** + * @return array + */ + private static function getRedisClusterOptions(): array + { + if (self::$clusterOptionsCache !== null) { + return self::$clusterOptionsCache; + } + + $reflection = new ReflectionClass(RedisClusterFromExtension::class); + + $options = []; + foreach ($reflection->getConstants() as $constant => $constantValue) { + if (strpos($constant, 'OPT_') !== 0) { + continue; + } + assert($constant !== ''); + assert(is_int($constantValue)); + + $options[$constant] = $constantValue; + } + + return self::$clusterOptionsCache = $options; + } + + public function getVersion(): string + { + $versionFromOptions = $this->options->redisVersion(); + if ($versionFromOptions) { + return $versionFromOptions; + } + + $resource = $this->getResource(); + try { + $info = $this->info($resource); + } catch (RedisClusterException $exception) { + throw RedisRuntimeException::fromClusterException($exception, $resource); + } + + $version = $info['redis_version']; + assert($version !== ''); + $this->options->setRedisVersion($version); + + return $version; + } + + public function getResource(): RedisClusterFromExtension + { + try { + $resource = $this->createRedisResource($this->options); + } catch (RedisClusterException $exception) { + throw RedisRuntimeException::connectionFailed($exception); + } + + $libraryOptions = $this->options->libOptions(); + + try { + $resource = $this->applyLibraryOptions($resource, $libraryOptions); + $this->libraryOptions = $this->mergeLibraryOptionsFromCluster($libraryOptions, $resource); + } catch (RedisClusterException $exception) { + throw RedisRuntimeException::fromClusterException($exception, $resource); + } + + return $resource; + } + + private function createRedisResource(RedisClusterOptions $options): RedisClusterFromExtension + { + if ($options->hasNodename()) { + return $this->createRedisResourceFromNodename( + $options->nodename(), + $options->timeout(), + $options->readTimeout(), + $options->persistent() + ); + } + + return new RedisClusterFromExtension( + null, + $options->seeds(), + $options->timeout(), + $options->readTimeout(), + $options->persistent() + ); + } + + private function createRedisResourceFromNodename( + string $nodename, + float $fallbackTimeout, + float $fallbackReadTimeout, + bool $persistent + ): RedisClusterFromExtension { + $options = new RedisClusterOptionsFromIni(); + $seeds = $options->seeds($nodename); + $timeout = $options->timeout($nodename, $fallbackTimeout); + $readTimeout = $options->readTimeout($nodename, $fallbackReadTimeout); + + return new RedisClusterFromExtension(null, $seeds, $timeout, $readTimeout, $persistent); + } + + /** + * @param array $options + */ + private function applyLibraryOptions( + RedisClusterFromExtension $resource, + array $options + ): RedisClusterFromExtension { + /** @psalm-suppress MixedAssignment */ + foreach ($options as $option => $value) { + /** @psalm-suppress InvalidArgument,MixedArgument */ + $resource->setOption($option, $value); + } + + return $resource; + } + + /** + * @param array $options + * @return array + */ + private function mergeLibraryOptionsFromCluster(array $options, RedisClusterFromExtension $resource): array + { + foreach (self::getRedisClusterOptions() as $constantValue) { + if (array_key_exists($constantValue, $options)) { + continue; + } + + /** + * @see https://github.com/phpredis/phpredis#getoption + * + * @psalm-suppress InvalidArgument + */ + $options[$constantValue] = $resource->getOption($constantValue); + } + + return $options; + } + + /** + * @return mixed + */ + public function getLibOption(int $option) + { + /** + * @see https://github.com/phpredis/phpredis#getoption + * + * @psalm-suppress InvalidArgument + */ + return $this->libraryOptions[$option] ?? $this->getResource()->getOption($option); + } + + public function hasSerializationSupport(PluginCapableInterface $adapter): bool + { + $options = $this->options; + $libraryOptions = $options->libOptions(); + $serializer = $libraryOptions[RedisClusterFromExtension::OPT_SERIALIZER] ?? + RedisClusterFromExtension::SERIALIZER_NONE; + + if ($serializer !== RedisClusterFromExtension::SERIALIZER_NONE) { + return true; + } + + /** @var iterable $plugins */ + $plugins = $adapter->getPluginRegistry(); + foreach ($plugins as $plugin) { + if (! $plugin instanceof Serializer) { + continue; + } + + return true; + } + + return false; + } + + /** + * @psalm-return RedisClusterInfoType + */ + private function info(RedisClusterFromExtension $resource): array + { + $nodename = $this->options->nodename(); + + if ($nodename !== '') { + /** @psalm-var RedisClusterInfoType $info */ + $info = $resource->info($nodename); + return $info; + } + + $seeds = $this->options->seeds(); + if ($seeds === []) { + throw new RuntimeException('Neither the node name nor any seed is configured.'); + } + + $seed = $seeds[0]; + /** @psalm-var RedisClusterInfoType $info */ + $info = $resource->info($seed); + + return $info; + } +} diff --git a/src/RedisClusterResourceManagerInterface.php b/src/RedisClusterResourceManagerInterface.php new file mode 100644 index 0000000..964a8e3 --- /dev/null +++ b/src/RedisClusterResourceManagerInterface.php @@ -0,0 +1,22 @@ +createRedisClusterStorage(RedisCluster::SERIALIZER_IGBINARY, false); + /** @psalm-suppress MixedArrayAssignment */ + $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = sprintf( + '%s storage doesn\'t support driver deferred', + get_class($storage) + ); + + return new CacheItemPoolDecorator($storage); + } +} diff --git a/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php b/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php new file mode 100644 index 0000000..0fc46b3 --- /dev/null +++ b/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php @@ -0,0 +1,31 @@ +createRedisClusterStorage(RedisCluster::SERIALIZER_PHP, false); + /** @psalm-suppress MixedArrayAssignment */ + $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = sprintf( + '%s storage doesn\'t support driver deferred', + get_class($storage) + ); + + return new CacheItemPoolDecorator($storage); + } +} diff --git a/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php b/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php new file mode 100644 index 0000000..5f78d06 --- /dev/null +++ b/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php @@ -0,0 +1,31 @@ +createRedisClusterStorage(RedisCluster::SERIALIZER_NONE, true); + /** @psalm-suppress MixedArrayAssignment */ + $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = sprintf( + '%s storage doesn\'t support driver deferred', + get_class($storage) + ); + + return new CacheItemPoolDecorator($storage); + } +} diff --git a/test/integration/Psr/SimpleCache/RedisClusterWithPhpIgbinaryTest.php b/test/integration/Psr/SimpleCache/RedisClusterWithPhpIgbinaryTest.php new file mode 100644 index 0000000..8289310 --- /dev/null +++ b/test/integration/Psr/SimpleCache/RedisClusterWithPhpIgbinaryTest.php @@ -0,0 +1,58 @@ +createRedisClusterStorage(RedisCluster::SERIALIZER_IGBINARY, false); + + return new SimpleCacheDecorator($storage); + } + + protected function setUp(): void + { + parent::setUp(); + + $laminasCacheVersion = InstalledVersions::getVersion('laminas/laminas-cache'); + if (! is_string($laminasCacheVersion)) { + self::fail('Could not determine `laminas-cache` version!'); + } + + if ( + version_compare( + $laminasCacheVersion, + '2.12', + 'lt' + ) + ) { + /** @psalm-suppress MixedArrayAssignment */ + $this->skippedTests['testBasicUsageWithLongKey'] + = 'Long keys will be supported for the redis adapter with `laminas-cache` v2.12+'; + } + } + + /** + * Remove the property cache as we do want to create a new instance for the next test. + */ + protected function tearDown(): void + { + $this->storage = null; + parent::tearDown(); + } +} diff --git a/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php b/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php new file mode 100644 index 0000000..ab0b471 --- /dev/null +++ b/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php @@ -0,0 +1,48 @@ +skippedTests['testBasicUsageWithLongKey'] + = 'Long keys will be supported for the redis adapter with `laminas-cache` v2.12+'; + } + } + + public function createSimpleCache(): CacheInterface + { + $storage = $this->createRedisClusterStorage(RedisCluster::SERIALIZER_PHP, false); + + return new SimpleCacheDecorator($storage); + } +} diff --git a/test/integration/Psr/SimpleCache/RedisClusterWithoutSerializerTest.php b/test/integration/Psr/SimpleCache/RedisClusterWithoutSerializerTest.php new file mode 100644 index 0000000..73d88ae --- /dev/null +++ b/test/integration/Psr/SimpleCache/RedisClusterWithoutSerializerTest.php @@ -0,0 +1,48 @@ +createRedisClusterStorage(RedisCluster::SERIALIZER_NONE, true); + + return new SimpleCacheDecorator($storage); + } + + protected function setUp(): void + { + parent::setUp(); + $laminasCacheVersion = InstalledVersions::getVersion('laminas/laminas-cache'); + if (! is_string($laminasCacheVersion)) { + self::fail('Could not determine `laminas-cache` version!'); + } + + if ( + version_compare( + $laminasCacheVersion, + '2.12', + 'lt' + ) + ) { + /** @psalm-suppress MixedArrayAssignment */ + $this->skippedTests['testBasicUsageWithLongKey'] + = 'Long keys will be supported for the redis adapter with `laminas-cache` v2.12+'; + } + } +} diff --git a/test/integration/Redis/RedisClusterStorageCreationTrait.php b/test/integration/Redis/RedisClusterStorageCreationTrait.php new file mode 100644 index 0000000..947f033 --- /dev/null +++ b/test/integration/Redis/RedisClusterStorageCreationTrait.php @@ -0,0 +1,59 @@ +storage) { + return $this->storage; + } + + $node = $this->nodename(); + + if ($node === '') { + throw new RuntimeException('Could not find nodename environment configuration.'); + } + + $this->options = new RedisClusterOptions([ + 'nodename' => $node, + 'lib_options' => [ + RedisClusterFromExtension::OPT_SERIALIZER => $serializerOption, + ], + 'namespace' => str_shuffle(implode('', ['a', 'b', 'c', 'd', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'])), + ]); + + $storage = new RedisCluster($this->options); + if ($serializerOption === RedisClusterFromExtension::SERIALIZER_NONE && $serializerPlugin) { + $storage->addPlugin(new Serializer()); + } + + return $this->storage = $storage; + } +} diff --git a/test/integration/Redis/RedisClusterTest.php b/test/integration/Redis/RedisClusterTest.php new file mode 100644 index 0000000..e7fc458 --- /dev/null +++ b/test/integration/Redis/RedisClusterTest.php @@ -0,0 +1,170 @@ +storage; + self::assertInstanceOf(StorageInterface::class, $storage); + $storage->setItem('foo', 'bar'); + $flushed = $storage->flush(); + $this->assertTrue($flushed); + $this->assertFalse($storage->hasItem('foo')); + } + + public function testCanCreateResourceFromSeeds(): void + { + $options = new RedisClusterOptions([ + 'seeds' => ['localhost:7000'], + ]); + + $storage = new RedisCluster($options); + $this->assertTrue($storage->flush()); + } + + public function testWillHandleIntegratedSerializerInformation(): void + { + $storage = $this->storage; + self::assertInstanceOf(StorageInterface::class, $storage); + $this->removeSerializer($storage); + + $options = $storage->getOptions(); + $options->setLibOptions([ + RedisClusterFromExtension::OPT_SERIALIZER => RedisClusterFromExtension::SERIALIZER_PHP, + ]); + + $capabilities = $storage->getCapabilities(); + $dataTypes = $capabilities->getSupportedDatatypes(); + $this->assertEquals([ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => 'array', + 'object' => 'object', + 'resource' => false, + ], $dataTypes); + } + + private function removeSerializer(AbstractAdapter $storage): void + { + foreach ($storage->getPluginRegistry() as $plugin) { + if (! $plugin instanceof Serializer) { + continue; + } + + $storage->removePlugin($plugin); + } + } + + public function testWillHandleNonSupportedSerializerInformation(): void + { + $storage = $this->storage; + self::assertInstanceOf(StorageInterface::class, $storage); + $this->removeSerializer($storage); + $options = $storage->getOptions(); + $options->setLibOptions([ + RedisClusterFromExtension::OPT_SERIALIZER => RedisClusterFromExtension::SERIALIZER_NONE, + ]); + + $capabilities = $storage->getCapabilities(); + $dataTypes = $capabilities->getSupportedDatatypes(); + $this->assertEquals([ + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ], $dataTypes); + } + + public function testClearsByNamespace(): void + { + $namespace = 'foo'; + $anotherNamespace = 'bar'; + $storage = $this->storage; + self::assertInstanceOf(StorageInterface::class, $storage); + $options = $storage->getOptions(); + $options->setNamespace($namespace); + + $storage->setItem('bar', 'baz'); + $storage->setItem('qoo', 'ooq'); + + $options->setNamespace($anotherNamespace); + + $storage->setItem('bar', 'baz'); + $storage->setItem('qoo', 'ooq'); + + $storage->clearByNamespace($namespace); + + $options->setNamespace($namespace); + + $result = $storage->getItems(['bar', 'qoo']); + self::assertEmpty($result); + + $options->setNamespace($anotherNamespace); + $result = $storage->getItems(['bar', 'qoo']); + self::assertEquals($result['bar'], 'baz'); + self::assertEquals($result['qoo'], 'ooq'); + } + + protected function setUp(): void + { + $this->storage = $this->createRedisClusterStorage( + RedisClusterFromExtension::SERIALIZER_PHP, + false + ); + // Clear storage before executing tests. + $this->storage->flush(); + + parent::setUp(); + } + + /** + * Remove the property cache as we do want to create a new instance for the next test. + */ + protected function tearDown(): void + { + $this->storage = null; + parent::tearDown(); + } + + /** + * @dataProvider getCommonAdapterNamesProvider + */ + public function testAdapterPluginManagerWithCommonNames(string $commonAdapterName): void + { + self::markTestSkipped('RedisCluster is available by static factory.'); + } + + /** + * @psalm-return array> + */ + public function getCommonAdapterNamesProvider(): array + { + return [['dummy']]; + } + + public function testOptionsFluentInterface(): void + { + self::markTestSkipped('Redis cluster specific options do not provide fluent interface!'); + } +} diff --git a/test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php b/test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php new file mode 100644 index 0000000..2c42fa5 --- /dev/null +++ b/test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php @@ -0,0 +1,60 @@ + self::class]; + + $host = $this->host(); + $port = $this->port(); + + if ($host && $port) { + $options['server'] = [$host, $port]; + } elseif ($host) { + $options['server'] = [$host]; + } + + $options['database'] = $this->database(); + + $password = $this->password(); + if ($password) { + $options['password'] = $password; + } + + $storage = new Redis(new RedisOptions($options)); + if ($serializerOption === RedisFromExtension::SERIALIZER_NONE && $serializerPlugin) { + $storage->addPlugin(new Serializer()); + } + + return $storage; + } +} diff --git a/test/unit/Exception/InvalidConfigurationExceptionTest.php b/test/unit/Exception/InvalidConfigurationExceptionTest.php new file mode 100644 index 0000000..b1fe6f4 --- /dev/null +++ b/test/unit/Exception/InvalidConfigurationExceptionTest.php @@ -0,0 +1,18 @@ +assertInstanceOf(ExceptionInterface::class, $exception); + } +} diff --git a/test/unit/RedisClusterOptionsFromIniTest.php b/test/unit/RedisClusterOptionsFromIniTest.php new file mode 100644 index 0000000..cd38f2d --- /dev/null +++ b/test/unit/RedisClusterOptionsFromIniTest.php @@ -0,0 +1,80 @@ +expectException(InvalidRedisConfigurationException::class); + new RedisClusterOptionsFromIni(); + } + + /** + * @dataProvider seedsByNodenameProvider + */ + public function testWillDetectSeedsByNodename(string $nodename, string $config, array $expected): void + { + ini_set('redis.clusters.seeds', $config); + $options = new RedisClusterOptionsFromIni(); + $seeds = $options->seeds($nodename); + $this->assertEquals($expected, $seeds); + } + + public function testWillThrowExceptionOnMissingNodenameInSeeds(): void + { + ini_set('redis.clusters.seeds', 'foo[]=bar:123'); + $options = new RedisClusterOptionsFromIni(); + $this->expectException(InvalidRedisConfigurationException::class); + $options->seeds('bar'); + } + + /** + * @psalm-return non-empty-array}> + */ + public function seedsByNodenameProvider(): array + { + return [ + 'simple' => [ + 'foo', + 'foo[]=localhost:1234', + ['localhost:1234'], + ], + 'multiple seeds' => [ + 'bar', + 'bar[]=localhost:4321&bar[]=localhost:1234', + ['localhost:4321', 'localhost:1234'], + ], + 'multiple nodes' => [ + 'baz', + 'foo[]=localhost:7000&foo[]=localhost=7001&baz[]=localhost:7002&baz[]=localhost:7003', + ['localhost:7002', 'localhost:7003'], + ], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->seedsConfigurationFromIni = ini_get('redis.clusters.seeds'); + ini_set('redis.clusters.seeds', ''); + } + + protected function tearDown(): void + { + parent::tearDown(); + ini_set('redis.clusters.seeds', $this->seedsConfigurationFromIni); + } +} diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php new file mode 100644 index 0000000..7ad083a --- /dev/null +++ b/test/unit/RedisClusterOptionsTest.php @@ -0,0 +1,80 @@ + 'foo', + 'timeout' => 1.0, + 'read_timeout' => 2.0, + 'persistent' => false, + 'redis_version' => '1.0', + ]); + + $this->assertEquals($options->nodename(), 'foo'); + $this->assertEquals($options->timeout(), 1.0); + $this->assertEquals($options->readTimeout(), 2.0); + $this->assertEquals($options->persistent(), false); + $this->assertEquals($options->redisVersion(), '1.0'); + } + + public function testCanHandleOptionsWithSeeds(): void + { + $options = new RedisClusterOptions([ + 'seeds' => ['localhost:1234'], + 'timeout' => 1.0, + 'read_timeout' => 2.0, + 'persistent' => false, + 'redis_version' => '1.0', + ]); + + $this->assertEquals($options->seeds(), ['localhost:1234']); + $this->assertEquals($options->timeout(), 1.0); + $this->assertEquals($options->readTimeout(), 2.0); + $this->assertEquals($options->persistent(), false); + $this->assertEquals($options->redisVersion(), '1.0'); + } + + public function testWillDetectSeedsAndNodenameConfiguration(): void + { + $this->expectException(InvalidRedisConfigurationException::class); + $this->expectExceptionMessage('Please provide either `nodename` or `seeds` configuration, not both.'); + new RedisClusterOptions([ + 'seeds' => ['localhost:1234'], + 'nodename' => 'foo', + ]); + } + + public function testWillValidateVersionFormat(): void + { + $this->expectException(InvalidArgumentException::class); + new RedisClusterOptions([ + 'redis_version' => 'foo', + ]); + } + + public function testWillValidateEmptyVersion(): void + { + $this->expectException(InvalidArgumentException::class); + new RedisClusterOptions([ + 'redis_version' => '', + ]); + } + + public function testWillDetectMissingRequiredValues(): void + { + $this->expectException(InvalidRedisConfigurationException::class); + $this->expectExceptionMessage('Missing either `nodename` or `seeds`.'); + new RedisClusterOptions(); + } +} diff --git a/test/unit/RedisClusterResourceManagerTest.php b/test/unit/RedisClusterResourceManagerTest.php new file mode 100644 index 0000000..2a9098d --- /dev/null +++ b/test/unit/RedisClusterResourceManagerTest.php @@ -0,0 +1,93 @@ +createMock(AbstractAdapter::class); + $adapter + ->expects($this->never()) + ->method('getPluginRegistry'); + + $this->assertTrue($manager->hasSerializationSupport($adapter)); + } + + public function testCanDetectSerializationSupportFromSerializerPlugin(): void + { + $registry = $this->createMock(SplObjectStorage::class); + $registry + ->expects($this->any()) + ->method('current') + ->willReturn(new Serializer()); + + $registry + ->expects($this->once()) + ->method('valid') + ->willReturn(true); + + $manager = new RedisClusterResourceManager(new RedisClusterOptions([ + 'nodename' => uniqid('', true), + ])); + $adapter = $this->createMock(AbstractAdapter::class); + $adapter + ->expects($this->once()) + ->method('getPluginRegistry') + ->willReturn($registry); + + $this->assertTrue($manager->hasSerializationSupport($adapter)); + } + + public function testWillReturnVersionFromOptions(): void + { + $manager = new RedisClusterResourceManager(new RedisClusterOptions([ + 'nodename' => uniqid('', true), + 'redis_version' => '1.0.0', + ])); + + $version = $manager->getVersion(); + $this->assertEquals('1.0.0', $version); + } + + /** + * @psalm-return array + */ + public function serializationSupportOptionsProvider(): array + { + return [ + 'php-serialize' => [ + new RedisClusterOptions([ + 'nodename' => uniqid('', true), + 'lib_options' => [ + RedisCluster::OPT_SERIALIZER => RedisCluster::SERIALIZER_PHP, + ], + ]), + ], + 'igbinary-serialize' => [ + new RedisClusterOptions([ + 'nodename' => uniqid('', true), + 'lib_options' => [ + RedisCluster::OPT_SERIALIZER => RedisCluster::SERIALIZER_IGBINARY, + ], + ]), + ], + ]; + } +} diff --git a/test/unit/RedisClusterTest.php b/test/unit/RedisClusterTest.php new file mode 100644 index 0000000..c47d2a6 --- /dev/null +++ b/test/unit/RedisClusterTest.php @@ -0,0 +1,80 @@ +createMock(RedisClusterResourceManagerInterface::class); + + $adapter = new RedisCluster([ + 'nodename' => 'bar', + ]); + $adapter->getOptions()->setResourceManager($resourceManager); + + $resourceManager + ->expects($this->once()) + ->method('hasSerializationSupport') + ->with($adapter) + ->willReturn(true); + + $resourceManager + ->expects($this->once()) + ->method('getVersion') + ->willReturn('5.0.0'); + + $capabilities = $adapter->getCapabilities(); + $datatypes = $capabilities->getSupportedDatatypes(); + $this->assertEquals([ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => 'array', + 'object' => 'object', + 'resource' => false, + ], $datatypes); + } + + public function testCanDetectCapabilitiesWithoutSerializationSupport(): void + { + $resourceManager = $this->createMock(RedisClusterResourceManagerInterface::class); + + $adapter = new RedisCluster([ + 'nodename' => 'bar', + ]); + $adapter->getOptions()->setResourceManager($resourceManager); + + $resourceManager + ->expects($this->once()) + ->method('hasSerializationSupport') + ->with($adapter) + ->willReturn(false); + + $resourceManager + ->expects($this->once()) + ->method('getVersion') + ->willReturn('5.0.0'); + + $capabilities = $adapter->getCapabilities(); + $datatypes = $capabilities->getSupportedDatatypes(); + $this->assertEquals([ + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ], $datatypes); + } +} From be764586d183520ddc703e819a9f79591a88edc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Tue, 18 May 2021 23:16:32 +0200 Subject: [PATCH 03/21] qa: normalize integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../CacheItemPool/RedisIntegrationTest.php | 44 ++++++------------- .../Psr/SimpleCache/RedisIntegrationTest.php | 34 +++++--------- 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php b/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php index 731bb2e..f1e0850 100644 --- a/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php +++ b/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php @@ -4,18 +4,17 @@ use Cache\IntegrationTests\CachePoolTest; use Laminas\Cache\Psr\CacheItemPool\CacheItemPoolDecorator; -use Laminas\Cache\Storage\Plugin\Serializer; -use Laminas\Cache\StorageFactory; +use LaminasTest\Cache\Storage\Adapter\Redis\RedisStorageCreationTrait; use Psr\Cache\CacheItemPoolInterface; +use Redis; use function date_default_timezone_get; use function date_default_timezone_set; -use function get_class; -use function getenv; -use function sprintf; class RedisIntegrationTest extends CachePoolTest { + use RedisStorageCreationTrait; + /** * Backup default timezone * @@ -25,6 +24,10 @@ class RedisIntegrationTest extends CachePoolTest protected function setUp(): void { + /** @psalm-suppress MixedArrayAssignment */ + $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] + = 'Cache decorator does not support deferred deletion'; + // set non-UTC timezone $this->tz = date_default_timezone_get(); date_default_timezone_set('America/Vancouver'); @@ -35,36 +38,15 @@ protected function setUp(): void protected function tearDown(): void { date_default_timezone_set($this->tz); + parent::tearDown(); } public function createCachePool(): CacheItemPoolInterface { - $options = ['resource_id' => self::class]; - - if (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') && getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST'), getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')]; - } elseif (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')]; - } - - if (getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE')) { - $options['database'] = getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE'); - } - - if (getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD')) { - $options['password'] = getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD'); - } - - $storage = StorageFactory::adapterFactory('redis', $options); - $storage->addPlugin(new Serializer()); - - $deferredSkippedMessage = sprintf( - '%s storage doesn\'t support driver deferred', - get_class($storage) - ); - $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = $deferredSkippedMessage; - - return new CacheItemPoolDecorator($storage); + return new CacheItemPoolDecorator($this->createRedisStorage( + Redis::SERIALIZER_NONE, + true + )); } } diff --git a/test/integration/Psr/SimpleCache/RedisIntegrationTest.php b/test/integration/Psr/SimpleCache/RedisIntegrationTest.php index b19c047..e516c92 100644 --- a/test/integration/Psr/SimpleCache/RedisIntegrationTest.php +++ b/test/integration/Psr/SimpleCache/RedisIntegrationTest.php @@ -5,18 +5,19 @@ use Cache\IntegrationTests\SimpleCacheTest; use Composer\InstalledVersions; use Laminas\Cache\Psr\SimpleCache\SimpleCacheDecorator; -use Laminas\Cache\Storage\Plugin\Serializer; -use Laminas\Cache\StorageFactory; +use LaminasTest\Cache\Storage\Adapter\Redis\RedisStorageCreationTrait; use Psr\SimpleCache\CacheInterface; +use Redis; use function date_default_timezone_get; use function date_default_timezone_set; -use function getenv; use function is_string; use function version_compare; -class RedisIntegrationTest extends SimpleCacheTest +final class RedisIntegrationTest extends SimpleCacheTest { + use RedisStorageCreationTrait; + /** * Backup default timezone * @@ -43,7 +44,7 @@ protected function setUp(): void ) { /** @psalm-suppress MixedArrayAssignment */ $this->skippedTests['testBasicUsageWithLongKey'] - = 'Long keys will be supported for the redis adapter with 2.12+ of `laminas-cache`'; + = 'Long keys will be supported for the redis adapter with `laminas-cache` v2.12+'; } parent::setUp(); @@ -58,24 +59,9 @@ protected function tearDown(): void public function createSimpleCache(): CacheInterface { - $options = ['resource_id' => self::class]; - - if (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') && getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST'), getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')]; - } elseif (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')]; - } - - if (getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE')) { - $options['database'] = getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE'); - } - - if (getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD')) { - $options['password'] = getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD'); - } - - $storage = StorageFactory::adapterFactory('redis', $options); - $storage->addPlugin(new Serializer()); - return new SimpleCacheDecorator($storage); + return new SimpleCacheDecorator($this->createRedisStorage( + Redis::SERIALIZER_NONE, + true + )); } } From 04100e6485c261ff4f70f7f9231e3df9beace049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Tue, 18 May 2021 23:16:59 +0200 Subject: [PATCH 04/21] qa: require `ext-posix` as dev-dependency until next major MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- composer.json | 4 +++- composer.lock | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0662e05..2b4b4bb 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "laminas/laminas-cache-storage-implementation": "1.0" }, "require-dev": { + "ext-posix": "*", "ext-redis": "*", "composer-runtime-api": "^2", "laminas/laminas-cache": "^2.10", @@ -37,7 +38,8 @@ "autoload-dev": { "psr-4": { "LaminasTest\\Cache\\Storage\\Adapter\\": "test/unit", - "LaminasTest\\Cache\\Psr\\": "test/integration/Psr" + "LaminasTest\\Cache\\Psr\\": "test/integration/Psr", + "LaminasTest\\Cache\\Storage\\Adapter\\Redis\\": "test/integration/Redis" } }, "scripts": { diff --git a/composer.lock b/composer.lock index 1833738..d802afd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "42eb02471957956073e0906fb391675d", + "content-hash": "4937b8f273805c1792a9db61d540bdd4", "packages": [], "packages-dev": [ { @@ -5749,6 +5749,7 @@ "php": "^7.3 || ~8.0.0" }, "platform-dev": { + "ext-posix": "*", "ext-redis": "*", "composer-runtime-api": "^2" }, From 964b839261a9c2664e64713d6416b56ca3ba7396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 15:58:26 +0200 Subject: [PATCH 05/21] qa: ensure `Laminas\Cache\Exception\ExceptionInterface` catches wont be detected as `InvalidThrow` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- psalm-baseline.xml | 22 ---------------------- psalm.xml | 9 ++++++++- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index ca97017..43b7e52 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -21,9 +21,6 @@ $ttl $ttl - - Exception\ExceptionInterface - parent::setOptions($options) @@ -283,25 +280,6 @@ $info - - - $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] - - - $this->storage - - - addPlugin - - - - - $this->storage - - - addPlugin - - 'Redis' diff --git a/psalm.xml b/psalm.xml index 18e250f..cbfd14e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -5,7 +5,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" - errorBaseline="psalm-baseline.xml" + errorBaseline="psalm-baseline.xml" > @@ -14,6 +14,13 @@ + + + + + + + From 7384551c411ea266da62e39b50544ddaf1a1eac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 16:02:18 +0200 Subject: [PATCH 06/21] qa: move `laminas-cache` integration tests to own namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- composer.json | 2 +- psalm-baseline.xml | 2 +- .../{Redis => Laminas}/RedisClusterStorageCreationTrait.php | 2 +- test/integration/{Redis => Laminas}/RedisClusterTest.php | 2 +- .../RedisConfigurationFromEnvironmentTrait.php | 2 +- .../{Redis => Laminas}/RedisStorageCreationTrait.php | 2 +- test/{unit => integration/Laminas}/RedisTest.php | 3 ++- .../Psr/CacheItemPool/RedisClusterWithPhpIgbinaryTest.php | 2 +- .../Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php | 2 +- .../Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php | 2 +- test/integration/Psr/CacheItemPool/RedisIntegrationTest.php | 2 +- .../Psr/SimpleCache/RedisClusterWithPhpIgbinaryTest.php | 2 +- .../Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php | 2 +- .../Psr/SimpleCache/RedisClusterWithoutSerializerTest.php | 2 +- test/integration/Psr/SimpleCache/RedisIntegrationTest.php | 2 +- 15 files changed, 16 insertions(+), 15 deletions(-) rename test/integration/{Redis => Laminas}/RedisClusterStorageCreationTrait.php (96%) rename test/integration/{Redis => Laminas}/RedisClusterTest.php (98%) rename test/integration/{Redis => Laminas}/RedisConfigurationFromEnvironmentTrait.php (95%) rename test/integration/{Redis => Laminas}/RedisStorageCreationTrait.php (95%) rename test/{unit => integration/Laminas}/RedisTest.php (99%) diff --git a/composer.json b/composer.json index 2b4b4bb..24abb7b 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "psr-4": { "LaminasTest\\Cache\\Storage\\Adapter\\": "test/unit", "LaminasTest\\Cache\\Psr\\": "test/integration/Psr", - "LaminasTest\\Cache\\Storage\\Adapter\\Redis\\": "test/integration/Redis" + "LaminasTest\\Cache\\Storage\\Adapter\\Laminas\\": "test/integration/Laminas" } }, "scripts": { diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 43b7e52..a2e092d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -295,7 +295,7 @@ testValidPersistentId - + testGetCapabilitiesTtl testGetSetDatabase diff --git a/test/integration/Redis/RedisClusterStorageCreationTrait.php b/test/integration/Laminas/RedisClusterStorageCreationTrait.php similarity index 96% rename from test/integration/Redis/RedisClusterStorageCreationTrait.php rename to test/integration/Laminas/RedisClusterStorageCreationTrait.php index 947f033..872ffb4 100644 --- a/test/integration/Redis/RedisClusterStorageCreationTrait.php +++ b/test/integration/Laminas/RedisClusterStorageCreationTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace LaminasTest\Cache\Storage\Adapter\Redis; +namespace LaminasTest\Cache\Storage\Adapter\Laminas; use Laminas\Cache\Storage\Adapter\RedisCluster; use Laminas\Cache\Storage\Adapter\RedisClusterOptions; diff --git a/test/integration/Redis/RedisClusterTest.php b/test/integration/Laminas/RedisClusterTest.php similarity index 98% rename from test/integration/Redis/RedisClusterTest.php rename to test/integration/Laminas/RedisClusterTest.php index e7fc458..1a43006 100644 --- a/test/integration/Redis/RedisClusterTest.php +++ b/test/integration/Laminas/RedisClusterTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace LaminasTest\Cache\Storage\Adapter\Redis; +namespace LaminasTest\Cache\Storage\Adapter\Laminas; use Laminas\Cache\Storage\Adapter\AbstractAdapter; use Laminas\Cache\Storage\Adapter\RedisCluster; diff --git a/test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php b/test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php similarity index 95% rename from test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php rename to test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php index 2c42fa5..8564e29 100644 --- a/test/integration/Redis/RedisConfigurationFromEnvironmentTrait.php +++ b/test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace LaminasTest\Cache\Storage\Adapter\Redis; +namespace LaminasTest\Cache\Storage\Adapter\Laminas; use function getenv; diff --git a/test/integration/Redis/RedisStorageCreationTrait.php b/test/integration/Laminas/RedisStorageCreationTrait.php similarity index 95% rename from test/integration/Redis/RedisStorageCreationTrait.php rename to test/integration/Laminas/RedisStorageCreationTrait.php index 04e9694..9d5ce52 100644 --- a/test/integration/Redis/RedisStorageCreationTrait.php +++ b/test/integration/Laminas/RedisStorageCreationTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace LaminasTest\Cache\Storage\Adapter\Redis; +namespace LaminasTest\Cache\Storage\Adapter\Laminas; use Laminas\Cache\Storage\Adapter\Redis; use Laminas\Cache\Storage\Adapter\RedisOptions; diff --git a/test/unit/RedisTest.php b/test/integration/Laminas/RedisTest.php similarity index 99% rename from test/unit/RedisTest.php rename to test/integration/Laminas/RedisTest.php index 66df60b..90b80d5 100644 --- a/test/unit/RedisTest.php +++ b/test/integration/Laminas/RedisTest.php @@ -1,12 +1,13 @@ Date: Mon, 24 May 2021 16:09:20 +0200 Subject: [PATCH 07/21] qa: fix seeds integration test not using the provided ENV configuration from INI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisCluster.php | 2 +- test/integration/Laminas/RedisClusterTest.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/RedisCluster.php b/src/RedisCluster.php index efa2f9e..f6ade93 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -93,7 +93,7 @@ public function flush(): bool continue; } - if (! $redis->flushDB()) { + if (! $redis->flushAll()) { continue; } diff --git a/test/integration/Laminas/RedisClusterTest.php b/test/integration/Laminas/RedisClusterTest.php index 1a43006..007d4c0 100644 --- a/test/integration/Laminas/RedisClusterTest.php +++ b/test/integration/Laminas/RedisClusterTest.php @@ -7,6 +7,7 @@ use Laminas\Cache\Storage\Adapter\AbstractAdapter; use Laminas\Cache\Storage\Adapter\RedisCluster; use Laminas\Cache\Storage\Adapter\RedisClusterOptions; +use Laminas\Cache\Storage\Adapter\RedisClusterOptionsFromIni; use Laminas\Cache\Storage\Plugin\Serializer; use Laminas\Cache\Storage\StorageInterface; use LaminasTest\Cache\Storage\Adapter\AbstractCommonAdapterTest; @@ -28,8 +29,10 @@ public function testWillProperlyFlush(): void public function testCanCreateResourceFromSeeds(): void { - $options = new RedisClusterOptions([ - 'seeds' => ['localhost:7000'], + $nodename = $this->nodename(); + $optionsFromIni = new RedisClusterOptionsFromIni(); + $options = new RedisClusterOptions([ + 'seeds' => $optionsFromIni->seeds($nodename), ]); $storage = new RedisCluster($options); From 7c817e01ce0a763f7751f38b3c47a4415a75c877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 20:40:34 +0200 Subject: [PATCH 08/21] qa: rename `RedisRuntimeException::connectionFailed` to `RedisRuntimeException::fromFailedConnection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Exception/RedisRuntimeException.php | 4 ++-- src/RedisCluster.php | 2 +- src/RedisClusterResourceManager.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Exception/RedisRuntimeException.php b/src/Exception/RedisRuntimeException.php index 015d14c..3f7301c 100644 --- a/src/Exception/RedisRuntimeException.php +++ b/src/Exception/RedisRuntimeException.php @@ -18,10 +18,10 @@ public static function fromClusterException(RedisClusterException $exception, Re return new self($message, (int) $exception->getCode(), $exception); } - public static function connectionFailed(Throwable $exception): self + public static function fromFailedConnection(Throwable $exception): self { return new self( - 'Could not establish connection to redis cluster', + 'Could not establish connection', (int) $exception->getCode(), $exception ); diff --git a/src/RedisCluster.php b/src/RedisCluster.php index f6ade93..e6a7192 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -115,7 +115,7 @@ private function getRedisResource(): RedisClusterFromExtension try { return $this->resource = $resourceManager->getResource(); } catch (RedisClusterException $exception) { - throw RedisRuntimeException::connectionFailed($exception); + throw RedisRuntimeException::fromFailedConnection($exception); } } diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 8b03c23..a464ea5 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -93,7 +93,7 @@ public function getResource(): RedisClusterFromExtension try { $resource = $this->createRedisResource($this->options); } catch (RedisClusterException $exception) { - throw RedisRuntimeException::connectionFailed($exception); + throw RedisRuntimeException::fromFailedConnection($exception); } $libraryOptions = $this->options->libOptions(); From 0f71b1330bb43390012fd10c35f4db0483b35d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 20:41:23 +0200 Subject: [PATCH 09/21] qa: rename `InvalidRedisConfigurationException` to `InvalidRedisClusterConfigurationException` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- ...on.php => InvalidRedisClusterConfigurationException.php} | 2 +- src/RedisClusterOptions.php | 6 +++--- src/RedisClusterOptionsFromIni.php | 6 +++--- test/unit/Exception/InvalidConfigurationExceptionTest.php | 4 ++-- test/unit/RedisClusterOptionsFromIniTest.php | 6 +++--- test/unit/RedisClusterOptionsTest.php | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) rename src/Exception/{InvalidRedisConfigurationException.php => InvalidRedisClusterConfigurationException.php} (93%) diff --git a/src/Exception/InvalidRedisConfigurationException.php b/src/Exception/InvalidRedisClusterConfigurationException.php similarity index 93% rename from src/Exception/InvalidRedisConfigurationException.php rename to src/Exception/InvalidRedisClusterConfigurationException.php index 16a6de0..59fa1b0 100644 --- a/src/Exception/InvalidRedisConfigurationException.php +++ b/src/Exception/InvalidRedisClusterConfigurationException.php @@ -8,7 +8,7 @@ use function sprintf; -final class InvalidRedisConfigurationException extends InvalidArgumentException +final class InvalidRedisClusterConfigurationException extends InvalidArgumentException { public static function fromMissingSeedsConfiguration(): self { diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index af69bd6..d5140ed 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -4,7 +4,7 @@ namespace Laminas\Cache\Storage\Adapter; -use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisConfigurationException; +use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use Traversable; final class RedisClusterOptions extends AdapterOptions @@ -52,11 +52,11 @@ public function __construct($options = null) $hasSeeds = $this->seeds() !== []; if (! $hasNodename && ! $hasSeeds) { - throw InvalidRedisConfigurationException::fromMissingRequiredValues(); + throw InvalidRedisClusterConfigurationException::fromMissingRequiredValues(); } if ($hasNodename && $hasSeeds) { - throw InvalidRedisConfigurationException::nodenameAndSeedsProvided(); + throw InvalidRedisClusterConfigurationException::nodenameAndSeedsProvided(); } } diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php index 1f7f7c6..29edfe7 100644 --- a/src/RedisClusterOptionsFromIni.php +++ b/src/RedisClusterOptionsFromIni.php @@ -4,7 +4,7 @@ namespace Laminas\Cache\Storage\Adapter; -use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisConfigurationException; +use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use function assert; use function ini_get; @@ -34,7 +34,7 @@ public function __construct() } if ($seedsConfiguration === '') { - throw InvalidRedisConfigurationException::fromMissingSeedsConfiguration(); + throw InvalidRedisClusterConfigurationException::fromMissingSeedsConfiguration(); } $seedsByNodename = []; @@ -80,7 +80,7 @@ public function seeds(string $nodename): array { $seeds = $this->seedsByNodename[$nodename] ?? []; if (! $seeds) { - throw InvalidRedisConfigurationException::forMissingSeedsForNodename($nodename); + throw InvalidRedisClusterConfigurationException::forMissingSeedsForNodename($nodename); } return $seeds; diff --git a/test/unit/Exception/InvalidConfigurationExceptionTest.php b/test/unit/Exception/InvalidConfigurationExceptionTest.php index b1fe6f4..8b9c026 100644 --- a/test/unit/Exception/InvalidConfigurationExceptionTest.php +++ b/test/unit/Exception/InvalidConfigurationExceptionTest.php @@ -5,14 +5,14 @@ namespace LaminasTest\Cache\Storage\Adapter\Exception; use Laminas\Cache\Exception\ExceptionInterface; -use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisConfigurationException; +use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use PHPUnit\Framework\TestCase; final class InvalidConfigurationExceptionTest extends TestCase { public function testInstanceOfLaminasCacheException(): void { - $exception = new InvalidRedisConfigurationException(); + $exception = new InvalidRedisClusterConfigurationException(); $this->assertInstanceOf(ExceptionInterface::class, $exception); } } diff --git a/test/unit/RedisClusterOptionsFromIniTest.php b/test/unit/RedisClusterOptionsFromIniTest.php index cd38f2d..378a97c 100644 --- a/test/unit/RedisClusterOptionsFromIniTest.php +++ b/test/unit/RedisClusterOptionsFromIniTest.php @@ -4,7 +4,7 @@ namespace LaminasTest\Cache\Storage\Adapter; -use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisConfigurationException; +use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use Laminas\Cache\Storage\Adapter\RedisClusterOptionsFromIni; use PHPUnit\Framework\TestCase; @@ -18,7 +18,7 @@ final class RedisClusterOptionsFromIniTest extends TestCase public function testWillThrowExceptionOnMissingSeedsConfiguration(): void { - $this->expectException(InvalidRedisConfigurationException::class); + $this->expectException(InvalidRedisClusterConfigurationException::class); new RedisClusterOptionsFromIni(); } @@ -37,7 +37,7 @@ public function testWillThrowExceptionOnMissingNodenameInSeeds(): void { ini_set('redis.clusters.seeds', 'foo[]=bar:123'); $options = new RedisClusterOptionsFromIni(); - $this->expectException(InvalidRedisConfigurationException::class); + $this->expectException(InvalidRedisClusterConfigurationException::class); $options->seeds('bar'); } diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php index 7ad083a..f3bc7f9 100644 --- a/test/unit/RedisClusterOptionsTest.php +++ b/test/unit/RedisClusterOptionsTest.php @@ -5,7 +5,7 @@ namespace LaminasTest\Cache\Storage\Adapter; use InvalidArgumentException; -use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisConfigurationException; +use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use PHPUnit\Framework\TestCase; @@ -47,7 +47,7 @@ public function testCanHandleOptionsWithSeeds(): void public function testWillDetectSeedsAndNodenameConfiguration(): void { - $this->expectException(InvalidRedisConfigurationException::class); + $this->expectException(InvalidRedisClusterConfigurationException::class); $this->expectExceptionMessage('Please provide either `nodename` or `seeds` configuration, not both.'); new RedisClusterOptions([ 'seeds' => ['localhost:1234'], @@ -73,7 +73,7 @@ public function testWillValidateEmptyVersion(): void public function testWillDetectMissingRequiredValues(): void { - $this->expectException(InvalidRedisConfigurationException::class); + $this->expectException(InvalidRedisClusterConfigurationException::class); $this->expectExceptionMessage('Missing either `nodename` or `seeds`.'); new RedisClusterOptions(); } From f5bb3ecd9c61f4c590b84e61c2fc944ecf3192c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 20:42:09 +0200 Subject: [PATCH 10/21] qa: rename `InvalidRedisClusterConfigurationException::forMissingSeedsForNodename` to `InvalidRedisClusterConfigurationException::fromMissingSeedsForNamedConfiguration` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Exception/InvalidRedisClusterConfigurationException.php | 4 ++-- src/RedisClusterOptionsFromIni.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Exception/InvalidRedisClusterConfigurationException.php b/src/Exception/InvalidRedisClusterConfigurationException.php index 59fa1b0..e01b8a8 100644 --- a/src/Exception/InvalidRedisClusterConfigurationException.php +++ b/src/Exception/InvalidRedisClusterConfigurationException.php @@ -15,11 +15,11 @@ public static function fromMissingSeedsConfiguration(): self return new self('Could not find `redis.clusters.seeds` entry in the php.ini file(s).'); } - public static function forMissingSeedsForNodename(string $nodename): self + public static function fromMissingSeedsForNamedConfiguration(string $name): self { return new self(sprintf( 'Missing `%s` within the configured `redis.cluster.seeds` entry in the php.ini file(s).', - $nodename + $name )); } diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php index 29edfe7..525b425 100644 --- a/src/RedisClusterOptionsFromIni.php +++ b/src/RedisClusterOptionsFromIni.php @@ -80,7 +80,7 @@ public function seeds(string $nodename): array { $seeds = $this->seedsByNodename[$nodename] ?? []; if (! $seeds) { - throw InvalidRedisClusterConfigurationException::forMissingSeedsForNodename($nodename); + throw InvalidRedisClusterConfigurationException::fromMissingSeedsForNamedConfiguration($nodename); } return $seeds; From 26b5f643f0e3211ba20324a8d4a215f71bd270bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 20:54:45 +0200 Subject: [PATCH 11/21] qa: rename `nodename` to `name` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 2 +- ...alidRedisClusterConfigurationException.php | 6 +- src/RedisClusterOptions.php | 29 +++++----- src/RedisClusterOptionsFromIni.php | 56 +++++++++---------- src/RedisClusterResourceManager.php | 22 ++++---- .../RedisClusterStorageCreationTrait.php | 6 +- test/integration/Laminas/RedisClusterTest.php | 5 +- ...RedisConfigurationFromEnvironmentTrait.php | 8 +-- test/unit/RedisClusterOptionsFromIniTest.php | 11 ++-- test/unit/RedisClusterOptionsTest.php | 12 ++-- test/unit/RedisClusterResourceManagerTest.php | 8 +-- test/unit/RedisClusterTest.php | 4 +- 12 files changed, 87 insertions(+), 82 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index cb0e422..8a6d338 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -42,4 +42,4 @@ jobs: job: ${{ matrix.job }} env: TESTS_LAMINAS_CACHE_REDIS_HOST: redis - TESTS_LAMINAS_CACHE_REDIS_CLUSTER_NODENAME: cluster + TESTS_LAMINAS_CACHE_REDIS_CLUSTER_NAME: cluster diff --git a/src/Exception/InvalidRedisClusterConfigurationException.php b/src/Exception/InvalidRedisClusterConfigurationException.php index e01b8a8..b482331 100644 --- a/src/Exception/InvalidRedisClusterConfigurationException.php +++ b/src/Exception/InvalidRedisClusterConfigurationException.php @@ -25,12 +25,12 @@ public static function fromMissingSeedsForNamedConfiguration(string $name): self public static function fromMissingRequiredValues(): self { - return new self('Missing either `nodename` or `seeds`.'); + return new self('Missing either `name` or `seeds`.'); } - public static function nodenameAndSeedsProvided(): self + public static function fromNameAndSeedsProvidedViaConfiguration(): self { - return new self('Please provide either `nodename` or `seeds` configuration, not both.'); + return new self('Please provide either `name` or `seeds` configuration, not both.'); } public static function fromInvalidSeedsConfiguration(string $seed): self diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index d5140ed..a97f938 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -13,7 +13,7 @@ final class RedisClusterOptions extends AdapterOptions protected $namespaceSeparator = ':'; /** @var string */ - private $nodename = ''; + private $name = ''; /** @var float */ private $timeout = 1.0; @@ -48,15 +48,15 @@ public function __construct($options = null) /** @psalm-suppress InvalidArgument */ parent::__construct($options); - $hasNodename = $this->hasNodename(); - $hasSeeds = $this->seeds() !== []; + $hasName = $this->hasName(); + $hasSeeds = $this->seeds() !== []; - if (! $hasNodename && ! $hasSeeds) { + if (! $hasName && ! $hasSeeds) { throw InvalidRedisClusterConfigurationException::fromMissingRequiredValues(); } - if ($hasNodename && $hasSeeds) { - throw InvalidRedisClusterConfigurationException::nodenameAndSeedsProvided(); + if ($hasName && $hasSeeds) { + throw InvalidRedisClusterConfigurationException::fromNameAndSeedsProvidedViaConfiguration(); } } @@ -92,20 +92,23 @@ public function setNamespaceSeparator(string $namespaceSeparator): void $this->namespaceSeparator = $namespaceSeparator; } - public function hasNodename(): bool + public function hasName(): bool { - return $this->nodename !== ''; + return $this->name !== ''; } - public function nodename(): string + public function getName(): string { - return $this->nodename; + return $this->name; } - public function setNodename(string $nodename): void + /** + * @psalm-param non-empty-string $name + */ + public function setName(string $name): void { - $this->nodename = $nodename; - $this->triggerOptionEvent('nodename', $nodename); + $this->name = $name; + $this->triggerOptionEvent('name', $name); } public function timeout(): float diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php index 525b425..5db196d 100644 --- a/src/RedisClusterOptionsFromIni.php +++ b/src/RedisClusterOptionsFromIni.php @@ -18,13 +18,13 @@ final class RedisClusterOptionsFromIni { /** @psalm-var array> */ - private $seedsByNodename; + private $seedsByName; /** @psalm-var array */ - private $timeoutByNodename; + private $timeoutByName; /** @psalm-var array */ - private $readTimeoutByNodename; + private $readTimeoutByName; public function __construct() { @@ -37,62 +37,62 @@ public function __construct() throw InvalidRedisClusterConfigurationException::fromMissingSeedsConfiguration(); } - $seedsByNodename = []; - parse_str($seedsConfiguration, $seedsByNodename); - /** @psalm-var non-empty-array> $seedsByNodename */ - $this->seedsByNodename = $seedsByNodename; + $seedsByName = []; + parse_str($seedsConfiguration, $seedsByName); + /** @psalm-var non-empty-array> $seedsByName */ + $this->seedsByName = $seedsByName; $timeoutConfiguration = ini_get('redis.clusters.timeout'); if (! is_string($timeoutConfiguration)) { $timeoutConfiguration = ''; } - $timeoutByNodename = []; - parse_str($timeoutConfiguration, $timeoutByNodename); - foreach ($timeoutByNodename as $nodename => $timeout) { - assert($nodename !== '' && is_numeric($timeout)); - $timeoutByNodename[$nodename] = (float) $timeout; + $timeoutByName = []; + parse_str($timeoutConfiguration, $timeoutByName); + foreach ($timeoutByName as $name => $timeout) { + assert($name !== '' && is_numeric($timeout)); + $timeoutByName[$name] = (float) $timeout; } - /** @psalm-var array $timeoutByNodename */ - $this->timeoutByNodename = $timeoutByNodename; + /** @psalm-var array $timeoutByName */ + $this->timeoutByName = $timeoutByName; $readTimeoutConfiguration = ini_get('redis.clusters.read_timeout'); if (! is_string($readTimeoutConfiguration)) { $readTimeoutConfiguration = ''; } - $readTimeoutByNodename = []; - parse_str($readTimeoutConfiguration, $readTimeoutByNodename); - foreach ($readTimeoutByNodename as $nodename => $readTimeout) { - assert($nodename !== '' && is_numeric($readTimeout)); - $readTimeoutByNodename[$nodename] = (float) $readTimeout; + $readTimeoutByName = []; + parse_str($readTimeoutConfiguration, $readTimeoutByName); + foreach ($readTimeoutByName as $name => $readTimeout) { + assert($name !== '' && is_numeric($readTimeout)); + $readTimeoutByName[$name] = (float) $readTimeout; } - /** @psalm-var array $readTimeoutByNodename */ - $this->readTimeoutByNodename = $readTimeoutByNodename; + /** @psalm-var array $readTimeoutByName */ + $this->readTimeoutByName = $readTimeoutByName; } /** * @return array * @psalm-return list */ - public function seeds(string $nodename): array + public function seeds(string $name): array { - $seeds = $this->seedsByNodename[$nodename] ?? []; + $seeds = $this->seedsByName[$name] ?? []; if (! $seeds) { - throw InvalidRedisClusterConfigurationException::fromMissingSeedsForNamedConfiguration($nodename); + throw InvalidRedisClusterConfigurationException::fromMissingSeedsForNamedConfiguration($name); } return $seeds; } - public function timeout(string $nodename, float $fallback): float + public function timeout(string $name, float $fallback): float { - return $this->timeoutByNodename[$nodename] ?? $fallback; + return $this->timeoutByName[$name] ?? $fallback; } - public function readTimeout(string $nodename, float $fallback): float + public function readTimeout(string $name, float $fallback): float { - return $this->readTimeoutByNodename[$nodename] ?? $fallback; + return $this->readTimeoutByName[$name] ?? $fallback; } } diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index a464ea5..24f068f 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -110,9 +110,9 @@ public function getResource(): RedisClusterFromExtension private function createRedisResource(RedisClusterOptions $options): RedisClusterFromExtension { - if ($options->hasNodename()) { - return $this->createRedisResourceFromNodename( - $options->nodename(), + if ($options->hasName()) { + return $this->createRedisResourceFromName( + $options->getName(), $options->timeout(), $options->readTimeout(), $options->persistent() @@ -128,16 +128,16 @@ private function createRedisResource(RedisClusterOptions $options): RedisCluster ); } - private function createRedisResourceFromNodename( - string $nodename, + private function createRedisResourceFromName( + string $name, float $fallbackTimeout, float $fallbackReadTimeout, bool $persistent ): RedisClusterFromExtension { $options = new RedisClusterOptionsFromIni(); - $seeds = $options->seeds($nodename); - $timeout = $options->timeout($nodename, $fallbackTimeout); - $readTimeout = $options->readTimeout($nodename, $fallbackReadTimeout); + $seeds = $options->seeds($name); + $timeout = $options->timeout($name, $fallbackTimeout); + $readTimeout = $options->readTimeout($name, $fallbackReadTimeout); return new RedisClusterFromExtension(null, $seeds, $timeout, $readTimeout, $persistent); } @@ -222,11 +222,11 @@ public function hasSerializationSupport(PluginCapableInterface $adapter): bool */ private function info(RedisClusterFromExtension $resource): array { - $nodename = $this->options->nodename(); + if ($this->options->hasName()) { + $name = $this->options->getName(); - if ($nodename !== '') { /** @psalm-var RedisClusterInfoType $info */ - $info = $resource->info($nodename); + $info = $resource->info($name); return $info; } diff --git a/test/integration/Laminas/RedisClusterStorageCreationTrait.php b/test/integration/Laminas/RedisClusterStorageCreationTrait.php index 872ffb4..413bdc3 100644 --- a/test/integration/Laminas/RedisClusterStorageCreationTrait.php +++ b/test/integration/Laminas/RedisClusterStorageCreationTrait.php @@ -35,14 +35,14 @@ private function createRedisClusterStorage(int $serializerOption, bool $serializ return $this->storage; } - $node = $this->nodename(); + $node = $this->getClusterNameFromEnvironment(); if ($node === '') { - throw new RuntimeException('Could not find nodename environment configuration.'); + throw new RuntimeException('Could not find named config environment configuration.'); } $this->options = new RedisClusterOptions([ - 'nodename' => $node, + 'name' => $node, 'lib_options' => [ RedisClusterFromExtension::OPT_SERIALIZER => $serializerOption, ], diff --git a/test/integration/Laminas/RedisClusterTest.php b/test/integration/Laminas/RedisClusterTest.php index 007d4c0..d67612e 100644 --- a/test/integration/Laminas/RedisClusterTest.php +++ b/test/integration/Laminas/RedisClusterTest.php @@ -29,10 +29,11 @@ public function testWillProperlyFlush(): void public function testCanCreateResourceFromSeeds(): void { - $nodename = $this->nodename(); + $name = $this->getClusterNameFromEnvironment(); + self::assertNotEmpty($name, 'Missing cluster name environment configuration.'); $optionsFromIni = new RedisClusterOptionsFromIni(); $options = new RedisClusterOptions([ - 'seeds' => $optionsFromIni->seeds($nodename), + 'seeds' => $optionsFromIni->seeds($name), ]); $storage = new RedisCluster($options); diff --git a/test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php b/test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php index 8564e29..dce8ac9 100644 --- a/test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php +++ b/test/integration/Laminas/RedisConfigurationFromEnvironmentTrait.php @@ -48,13 +48,13 @@ private function password(): string return $password; } - private function nodename(): string + private function getClusterNameFromEnvironment(): string { - $nodename = getenv('TESTS_LAMINAS_CACHE_REDIS_CLUSTER_NODENAME'); - if ($nodename === false) { + $name = getenv('TESTS_LAMINAS_CACHE_REDIS_CLUSTER_NAME'); + if ($name === false) { return ''; } - return $nodename; + return $name; } } diff --git a/test/unit/RedisClusterOptionsFromIniTest.php b/test/unit/RedisClusterOptionsFromIniTest.php index 378a97c..83fbdcc 100644 --- a/test/unit/RedisClusterOptionsFromIniTest.php +++ b/test/unit/RedisClusterOptionsFromIniTest.php @@ -23,17 +23,18 @@ public function testWillThrowExceptionOnMissingSeedsConfiguration(): void } /** - * @dataProvider seedsByNodenameProvider + * @psalm-param non-empty-string $name + * @dataProvider seedsByNameProvider */ - public function testWillDetectSeedsByNodename(string $nodename, string $config, array $expected): void + public function testWillDetectSeedsByName(string $name, string $config, array $expected): void { ini_set('redis.clusters.seeds', $config); $options = new RedisClusterOptionsFromIni(); - $seeds = $options->seeds($nodename); + $seeds = $options->getSeeds($name); $this->assertEquals($expected, $seeds); } - public function testWillThrowExceptionOnMissingNodenameInSeeds(): void + public function testWillThrowExceptionOnMissingNameInSeeds(): void { ini_set('redis.clusters.seeds', 'foo[]=bar:123'); $options = new RedisClusterOptionsFromIni(); @@ -44,7 +45,7 @@ public function testWillThrowExceptionOnMissingNodenameInSeeds(): void /** * @psalm-return non-empty-array}> */ - public function seedsByNodenameProvider(): array + public function seedsByNameProvider(): array { return [ 'simple' => [ diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php index f3bc7f9..614de06 100644 --- a/test/unit/RedisClusterOptionsTest.php +++ b/test/unit/RedisClusterOptionsTest.php @@ -14,14 +14,14 @@ final class RedisClusterOptionsTest extends TestCase public function testCanHandleOptionsWithNodename(): void { $options = new RedisClusterOptions([ - 'nodename' => 'foo', + 'name' => 'foo', 'timeout' => 1.0, 'read_timeout' => 2.0, 'persistent' => false, 'redis_version' => '1.0', ]); - $this->assertEquals($options->nodename(), 'foo'); + $this->assertEquals($options->getName(), 'foo'); $this->assertEquals($options->timeout(), 1.0); $this->assertEquals($options->readTimeout(), 2.0); $this->assertEquals($options->persistent(), false); @@ -48,10 +48,10 @@ public function testCanHandleOptionsWithSeeds(): void public function testWillDetectSeedsAndNodenameConfiguration(): void { $this->expectException(InvalidRedisClusterConfigurationException::class); - $this->expectExceptionMessage('Please provide either `nodename` or `seeds` configuration, not both.'); + $this->expectExceptionMessage('Please provide either `name` or `seeds` configuration, not both.'); new RedisClusterOptions([ - 'seeds' => ['localhost:1234'], - 'nodename' => 'foo', + 'seeds' => ['localhost:1234'], + 'name' => 'foo', ]); } @@ -74,7 +74,7 @@ public function testWillValidateEmptyVersion(): void public function testWillDetectMissingRequiredValues(): void { $this->expectException(InvalidRedisClusterConfigurationException::class); - $this->expectExceptionMessage('Missing either `nodename` or `seeds`.'); + $this->expectExceptionMessage('Missing either `name` or `seeds`.'); new RedisClusterOptions(); } } diff --git a/test/unit/RedisClusterResourceManagerTest.php b/test/unit/RedisClusterResourceManagerTest.php index 2a9098d..6195011 100644 --- a/test/unit/RedisClusterResourceManagerTest.php +++ b/test/unit/RedisClusterResourceManagerTest.php @@ -44,7 +44,7 @@ public function testCanDetectSerializationSupportFromSerializerPlugin(): void ->willReturn(true); $manager = new RedisClusterResourceManager(new RedisClusterOptions([ - 'nodename' => uniqid('', true), + 'name' => uniqid('', true), ])); $adapter = $this->createMock(AbstractAdapter::class); $adapter @@ -58,7 +58,7 @@ public function testCanDetectSerializationSupportFromSerializerPlugin(): void public function testWillReturnVersionFromOptions(): void { $manager = new RedisClusterResourceManager(new RedisClusterOptions([ - 'nodename' => uniqid('', true), + 'name' => uniqid('', true), 'redis_version' => '1.0.0', ])); @@ -74,7 +74,7 @@ public function serializationSupportOptionsProvider(): array return [ 'php-serialize' => [ new RedisClusterOptions([ - 'nodename' => uniqid('', true), + 'name' => uniqid('', true), 'lib_options' => [ RedisCluster::OPT_SERIALIZER => RedisCluster::SERIALIZER_PHP, ], @@ -82,7 +82,7 @@ public function serializationSupportOptionsProvider(): array ], 'igbinary-serialize' => [ new RedisClusterOptions([ - 'nodename' => uniqid('', true), + 'name' => uniqid('', true), 'lib_options' => [ RedisCluster::OPT_SERIALIZER => RedisCluster::SERIALIZER_IGBINARY, ], diff --git a/test/unit/RedisClusterTest.php b/test/unit/RedisClusterTest.php index c47d2a6..1978d92 100644 --- a/test/unit/RedisClusterTest.php +++ b/test/unit/RedisClusterTest.php @@ -15,7 +15,7 @@ public function testCanDetectCapabilitiesWithSerializationSupport(): void $resourceManager = $this->createMock(RedisClusterResourceManagerInterface::class); $adapter = new RedisCluster([ - 'nodename' => 'bar', + 'name' => 'bar', ]); $adapter->getOptions()->setResourceManager($resourceManager); @@ -49,7 +49,7 @@ public function testCanDetectCapabilitiesWithoutSerializationSupport(): void $resourceManager = $this->createMock(RedisClusterResourceManagerInterface::class); $adapter = new RedisCluster([ - 'nodename' => 'bar', + 'name' => 'bar', ]); $adapter->getOptions()->setResourceManager($resourceManager); From 685d825a129a29c6465d77c37d6b1d00b3432fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 20:57:23 +0200 Subject: [PATCH 12/21] qa: mark `RedisClusterOptions::setResourceManager` as internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- psalm.xml | 5 +++++ src/RedisClusterOptions.php | 3 +++ 2 files changed, 8 insertions(+) diff --git a/psalm.xml b/psalm.xml index cbfd14e..b5f70d0 100644 --- a/psalm.xml +++ b/psalm.xml @@ -20,6 +20,11 @@ + + + + + diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index a97f938..e2636eb 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -175,6 +175,9 @@ public function libOptions(): array return $this->libOptions; } + /** + * @internal This method should only be used within this library to have better test coverage! + */ public function setResourceManager(RedisClusterResourceManagerInterface $resourceManager): void { $this->resourceManager = $resourceManager; From 2b0c0cf553f13ab8df4b112b5120bfd020d6b1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 21:00:59 +0200 Subject: [PATCH 13/21] qa: convert methods to getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisClusterOptionsFromIni.php | 6 +++--- src/RedisClusterResourceManager.php | 6 +++--- test/integration/Laminas/RedisClusterTest.php | 2 +- test/unit/RedisClusterOptionsFromIniTest.php | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php index 5db196d..3659ab8 100644 --- a/src/RedisClusterOptionsFromIni.php +++ b/src/RedisClusterOptionsFromIni.php @@ -76,7 +76,7 @@ public function __construct() * @return array * @psalm-return list */ - public function seeds(string $name): array + public function getSeeds(string $name): array { $seeds = $this->seedsByName[$name] ?? []; if (! $seeds) { @@ -86,12 +86,12 @@ public function seeds(string $name): array return $seeds; } - public function timeout(string $name, float $fallback): float + public function getTimeout(string $name, float $fallback): float { return $this->timeoutByName[$name] ?? $fallback; } - public function readTimeout(string $name, float $fallback): float + public function getReadTimeout(string $name, float $fallback): float { return $this->readTimeoutByName[$name] ?? $fallback; } diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 24f068f..222e371 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -135,9 +135,9 @@ private function createRedisResourceFromName( bool $persistent ): RedisClusterFromExtension { $options = new RedisClusterOptionsFromIni(); - $seeds = $options->seeds($name); - $timeout = $options->timeout($name, $fallbackTimeout); - $readTimeout = $options->readTimeout($name, $fallbackReadTimeout); + $seeds = $options->getSeeds($name); + $timeout = $options->getTimeout($name, $fallbackTimeout); + $readTimeout = $options->getReadTimeout($name, $fallbackReadTimeout); return new RedisClusterFromExtension(null, $seeds, $timeout, $readTimeout, $persistent); } diff --git a/test/integration/Laminas/RedisClusterTest.php b/test/integration/Laminas/RedisClusterTest.php index d67612e..53d3b5a 100644 --- a/test/integration/Laminas/RedisClusterTest.php +++ b/test/integration/Laminas/RedisClusterTest.php @@ -33,7 +33,7 @@ public function testCanCreateResourceFromSeeds(): void self::assertNotEmpty($name, 'Missing cluster name environment configuration.'); $optionsFromIni = new RedisClusterOptionsFromIni(); $options = new RedisClusterOptions([ - 'seeds' => $optionsFromIni->seeds($name), + 'seeds' => $optionsFromIni->getSeeds($name), ]); $storage = new RedisCluster($options); diff --git a/test/unit/RedisClusterOptionsFromIniTest.php b/test/unit/RedisClusterOptionsFromIniTest.php index 83fbdcc..2630a12 100644 --- a/test/unit/RedisClusterOptionsFromIniTest.php +++ b/test/unit/RedisClusterOptionsFromIniTest.php @@ -39,7 +39,7 @@ public function testWillThrowExceptionOnMissingNameInSeeds(): void ini_set('redis.clusters.seeds', 'foo[]=bar:123'); $options = new RedisClusterOptionsFromIni(); $this->expectException(InvalidRedisClusterConfigurationException::class); - $options->seeds('bar'); + $options->getSeeds('bar'); } /** From be6116b92120280518147c0c4f16eea99088d514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 21:05:51 +0200 Subject: [PATCH 14/21] qa: use `non-empty-string` for named configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisClusterOptions.php | 12 +++++++++++- src/RedisClusterOptionsFromIni.php | 7 +++++++ src/RedisClusterResourceManager.php | 5 ++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index e2636eb..63ba87e 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -4,6 +4,7 @@ namespace Laminas\Cache\Storage\Adapter; +use Laminas\Cache\Exception\RuntimeException; use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use Traversable; @@ -97,9 +98,18 @@ public function hasName(): bool return $this->name !== ''; } + /** + * @psalm-return non-empty-string + * @throws RuntimeException If method is called but `name` was not provided via configuration. + */ public function getName(): string { - return $this->name; + $name = $this->name; + if ($name === '') { + throw new RuntimeException('`name` is not provided via configuration.'); + } + + return $name; } /** diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php index 3659ab8..94a9491 100644 --- a/src/RedisClusterOptionsFromIni.php +++ b/src/RedisClusterOptionsFromIni.php @@ -73,6 +73,7 @@ public function __construct() } /** + * @psalm-param non-empty-string $name * @return array * @psalm-return list */ @@ -86,11 +87,17 @@ public function getSeeds(string $name): array return $seeds; } + /** + * @psalm-param non-empty-string $name + */ public function getTimeout(string $name, float $fallback): float { return $this->timeoutByName[$name] ?? $fallback; } + /** + * @psalm-param non-empty-string $name + */ public function getReadTimeout(string $name, float $fallback): float { return $this->readTimeoutByName[$name] ?? $fallback; diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 222e371..79bc90c 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -128,6 +128,9 @@ private function createRedisResource(RedisClusterOptions $options): RedisCluster ); } + /** + * @psalm-param non-empty-string $name + */ private function createRedisResourceFromName( string $name, float $fallbackTimeout, @@ -139,7 +142,7 @@ private function createRedisResourceFromName( $timeout = $options->getTimeout($name, $fallbackTimeout); $readTimeout = $options->getReadTimeout($name, $fallbackReadTimeout); - return new RedisClusterFromExtension(null, $seeds, $timeout, $readTimeout, $persistent); + return new RedisClusterFromExtension($name, $seeds, $timeout, $readTimeout, $persistent); } /** From b867d729f99eacc20e8bf50630339180d9579787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 21:08:53 +0200 Subject: [PATCH 15/21] qa: change some methods to reflect `getter` methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisClusterOptions.php | 14 +++++++------- src/RedisClusterResourceManager.php | 22 +++++++++++----------- test/unit/RedisClusterOptionsTest.php | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index 63ba87e..90e7542 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -50,7 +50,7 @@ public function __construct($options = null) /** @psalm-suppress InvalidArgument */ parent::__construct($options); $hasName = $this->hasName(); - $hasSeeds = $this->seeds() !== []; + $hasSeeds = $this->getSeeds() !== []; if (! $hasName && ! $hasSeeds) { throw InvalidRedisClusterConfigurationException::fromMissingRequiredValues(); @@ -121,17 +121,17 @@ public function setName(string $name): void $this->triggerOptionEvent('name', $name); } - public function timeout(): float + public function getTimeout(): float { return $this->timeout; } - public function readTimeout(): float + public function getReadTimeout(): float { return $this->readTimeout; } - public function persistent(): bool + public function isPersistent(): bool { return $this->persistent; } @@ -140,7 +140,7 @@ public function persistent(): bool * @return array * @psalm-return list */ - public function seeds(): array + public function getSeeds(): array { return $this->seeds; } @@ -164,7 +164,7 @@ public function setRedisVersion(string $version): void $this->version = $version; } - public function redisVersion(): string + public function getRedisVersion(): string { return $this->version; } @@ -180,7 +180,7 @@ public function setLibOptions(array $options): void /** * @psalm-return array */ - public function libOptions(): array + public function getLibOptions(): array { return $this->libOptions; } diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 79bc90c..2ce068c 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -69,7 +69,7 @@ private static function getRedisClusterOptions(): array public function getVersion(): string { - $versionFromOptions = $this->options->redisVersion(); + $versionFromOptions = $this->options->getRedisVersion(); if ($versionFromOptions) { return $versionFromOptions; } @@ -96,7 +96,7 @@ public function getResource(): RedisClusterFromExtension throw RedisRuntimeException::fromFailedConnection($exception); } - $libraryOptions = $this->options->libOptions(); + $libraryOptions = $this->options->getLibOptions(); try { $resource = $this->applyLibraryOptions($resource, $libraryOptions); @@ -113,18 +113,18 @@ private function createRedisResource(RedisClusterOptions $options): RedisCluster if ($options->hasName()) { return $this->createRedisResourceFromName( $options->getName(), - $options->timeout(), - $options->readTimeout(), - $options->persistent() + $options->getTimeout(), + $options->getReadTimeout(), + $options->isPersistent() ); } return new RedisClusterFromExtension( null, - $options->seeds(), - $options->timeout(), - $options->readTimeout(), - $options->persistent() + $options->getSeeds(), + $options->getTimeout(), + $options->getReadTimeout(), + $options->isPersistent() ); } @@ -199,7 +199,7 @@ public function getLibOption(int $option) public function hasSerializationSupport(PluginCapableInterface $adapter): bool { $options = $this->options; - $libraryOptions = $options->libOptions(); + $libraryOptions = $options->getLibOptions(); $serializer = $libraryOptions[RedisClusterFromExtension::OPT_SERIALIZER] ?? RedisClusterFromExtension::SERIALIZER_NONE; @@ -233,7 +233,7 @@ private function info(RedisClusterFromExtension $resource): array return $info; } - $seeds = $this->options->seeds(); + $seeds = $this->options->getSeeds(); if ($seeds === []) { throw new RuntimeException('Neither the node name nor any seed is configured.'); } diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php index 614de06..2ef50a9 100644 --- a/test/unit/RedisClusterOptionsTest.php +++ b/test/unit/RedisClusterOptionsTest.php @@ -22,10 +22,10 @@ public function testCanHandleOptionsWithNodename(): void ]); $this->assertEquals($options->getName(), 'foo'); - $this->assertEquals($options->timeout(), 1.0); - $this->assertEquals($options->readTimeout(), 2.0); - $this->assertEquals($options->persistent(), false); - $this->assertEquals($options->redisVersion(), '1.0'); + $this->assertEquals($options->getTimeout(), 1.0); + $this->assertEquals($options->getReadTimeout(), 2.0); + $this->assertEquals($options->isPersistent(), false); + $this->assertEquals($options->getRedisVersion(), '1.0'); } public function testCanHandleOptionsWithSeeds(): void @@ -38,11 +38,11 @@ public function testCanHandleOptionsWithSeeds(): void 'redis_version' => '1.0', ]); - $this->assertEquals($options->seeds(), ['localhost:1234']); - $this->assertEquals($options->timeout(), 1.0); - $this->assertEquals($options->readTimeout(), 2.0); - $this->assertEquals($options->persistent(), false); - $this->assertEquals($options->redisVersion(), '1.0'); + $this->assertEquals($options->getSeeds(), ['localhost:1234']); + $this->assertEquals($options->getTimeout(), 1.0); + $this->assertEquals($options->getReadTimeout(), 2.0); + $this->assertEquals($options->isPersistent(), false); + $this->assertEquals($options->getRedisVersion(), '1.0'); } public function testWillDetectSeedsAndNodenameConfiguration(): void From 007c7d11b8612fa1965a4902267f19f3ae7b96df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 21:11:24 +0200 Subject: [PATCH 16/21] qa: rename some methods to better reflect what they are doing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisCluster.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/RedisCluster.php b/src/RedisCluster.php index e6a7192..46ea13e 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -188,7 +188,7 @@ protected function internalGetItems(array &$normalizedKeys): array $namespacedKeys = []; foreach ($normalizedKeys as $normalizedKey) { /** @psalm-suppress RedundantCast */ - $namespacedKeys[] = $this->key((string) $normalizedKey); + $namespacedKeys[] = $this->createNamespacedKey((string) $normalizedKey); } $redis = $this->getRedisResource(); @@ -205,7 +205,7 @@ protected function internalGetItems(array &$normalizedKeys): array foreach ($resultsByIndex as $keyIndex => $value) { $normalizedKey = $normalizedKeys[$keyIndex]; $namespacedKey = $namespacedKeys[$keyIndex]; - if ($value === false && ! $this->falseReturnValueIsIntended($redis, $namespacedKey)) { + if ($value === false && ! $this->isFalseReturnValuePersisted($redis, $namespacedKey)) { continue; } @@ -216,7 +216,7 @@ protected function internalGetItems(array &$normalizedKeys): array return $result; } - private function key(string $key): string + private function createNamespacedKey(string $key): string { if ($this->namespacePrefix !== null) { return $this->namespacePrefix . $key; @@ -242,7 +242,7 @@ protected function internalSetItem(&$normalizedKey, &$value): bool $options = $this->getOptions(); $ttl = (int) $options->getTtl(); - $namespacedKey = $this->key($normalizedKey); + $namespacedKey = $this->createNamespacedKey($normalizedKey); try { if ($ttl) { /** @@ -270,7 +270,7 @@ protected function internalRemoveItem(&$normalizedKey): bool $redis = $this->getRedisResource(); try { - return $redis->del($this->key($normalizedKey)) === 1; + return $redis->del($this->createNamespacedKey($normalizedKey)) === 1; } catch (RedisClusterException $exception) { throw $this->clusterException($exception, $redis); } @@ -285,7 +285,7 @@ protected function internalHasItem(&$normalizedKey): bool try { /** @psalm-var 0|1 $exists */ - $exists = $redis->exists($this->key($normalizedKey)); + $exists = $redis->exists($this->createNamespacedKey($normalizedKey)); return (bool) $exists; } catch (RedisClusterException $exception) { throw $this->clusterException($exception, $redis); @@ -300,7 +300,7 @@ protected function internalSetItems(array &$normalizedKeyValuePairs): array $namespacedKeyValuePairs = []; /** @psalm-suppress MixedAssignment */ foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { - $namespacedKeyValuePairs[$this->key((string) $normalizedKey)] = $value; + $namespacedKeyValuePairs[$this->createNamespacedKey((string) $normalizedKey)] = $value; } $successByKey = []; @@ -356,7 +356,7 @@ protected function internalGetCapabilities(): Capabilities $this, $this->capabilityMarker, [ - 'supportedDatatypes' => $this->supportedDatatypes($serializer), + 'supportedDatatypes' => $this->getSupportedDatatypes($serializer), 'supportedMetadata' => $supportedMetadata, 'minTtl' => $minTtl, 'maxTtl' => 0, @@ -374,7 +374,7 @@ protected function internalGetCapabilities(): Capabilities /** * @psalm-return array */ - private function supportedDatatypes(bool $serializer): array + private function getSupportedDatatypes(bool $serializer): array { if ($serializer) { return [ @@ -439,7 +439,7 @@ private function clusterException( * {@see RedisClusterFromExtension::mget} is `false` because the key does not exist or because the keys value * is `false` at type-level. */ - private function falseReturnValueIsIntended(RedisClusterFromExtension $redis, string $key): bool + private function isFalseReturnValuePersisted(RedisClusterFromExtension $redis, string $key): bool { /** @psalm-suppress MixedAssignment */ $serializer = $this->getLibOption(RedisClusterFromExtension::OPT_SERIALIZER); @@ -466,7 +466,7 @@ private function falseReturnValueIsIntended(RedisClusterFromExtension $redis, st protected function internalGetMetadata(&$normalizedKey) { /** @psalm-suppress RedundantCastGivenDocblockType */ - $namespacedKey = $this->key((string) $normalizedKey); + $namespacedKey = $this->createNamespacedKey((string) $normalizedKey); $redis = $this->getRedisResource(); $metadata = []; $capabilities = $this->internalGetCapabilities(); From b34fb06ceb427a8ca7be93920d9266495a43f8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 24 May 2021 22:57:41 +0200 Subject: [PATCH 17/21] feat: introduce support for `RedisCluster` authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisClusterOptions.php | 16 +++++++++++++ src/RedisClusterOptionsFromIni.php | 22 ++++++++++++++++++ src/RedisClusterResourceManager.php | 24 ++++++++++++++++---- test/unit/RedisClusterOptionsFromIniTest.php | 14 ++++++++++++ test/unit/RedisClusterOptionsTest.php | 24 ++++++++++++-------- 5 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index 90e7542..62d5ce4 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -37,6 +37,9 @@ final class RedisClusterOptions extends AdapterOptions /** @var RedisClusterResourceManagerInterface|null */ private $resourceManager; + /** @var string */ + private $password = ''; + /** * @param array|Traversable|null|AdapterOptions $options * @psalm-param array|Traversable|null|AdapterOptions $options @@ -201,4 +204,17 @@ public function getResourceManager(): RedisClusterResourceManagerInterface return $this->resourceManager = new RedisClusterResourceManager($this); } + + public function getPassword(): string + { + return $this->password; + } + + /** + * @psalm-param non-empty-string $password + */ + public function setPassword(string $password): void + { + $this->password = $password; + } } diff --git a/src/RedisClusterOptionsFromIni.php b/src/RedisClusterOptionsFromIni.php index 94a9491..1c5ae20 100644 --- a/src/RedisClusterOptionsFromIni.php +++ b/src/RedisClusterOptionsFromIni.php @@ -26,6 +26,9 @@ final class RedisClusterOptionsFromIni /** @psalm-var array */ private $readTimeoutByName; + /** @psalm-var array */ + private $authenticationByName; + public function __construct() { $seedsConfiguration = ini_get('redis.clusters.seeds'); @@ -70,6 +73,17 @@ public function __construct() /** @psalm-var array $readTimeoutByName */ $this->readTimeoutByName = $readTimeoutByName; + + $authenticationConfiguration = ini_get('redis.clusters.auth'); + if (! is_string($authenticationConfiguration)) { + $authenticationConfiguration = ''; + } + + $authenticationByName = []; + parse_str($authenticationConfiguration, $authenticationByName); + /** @psalm-var array $authenticationByName */ + + $this->authenticationByName = $authenticationByName; } /** @@ -102,4 +116,12 @@ public function getReadTimeout(string $name, float $fallback): float { return $this->readTimeoutByName[$name] ?? $fallback; } + + /** + * @psalm-param non-empty-string $name + */ + public function getPasswordByName(string $name, string $fallback): string + { + return $this->authenticationByName[$name] ?? $fallback; + } } diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 2ce068c..a7a43b0 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -115,16 +115,23 @@ private function createRedisResource(RedisClusterOptions $options): RedisCluster $options->getName(), $options->getTimeout(), $options->getReadTimeout(), - $options->isPersistent() + $options->isPersistent(), + $options->getPassword() ); } + $password = $options->getPassword(); + if ($password === '') { + $password = null; + } + return new RedisClusterFromExtension( null, $options->getSeeds(), $options->getTimeout(), $options->getReadTimeout(), - $options->isPersistent() + $options->isPersistent(), + $password ); } @@ -135,14 +142,23 @@ private function createRedisResourceFromName( string $name, float $fallbackTimeout, float $fallbackReadTimeout, - bool $persistent + bool $persistent, + string $fallbackPassword ): RedisClusterFromExtension { $options = new RedisClusterOptionsFromIni(); $seeds = $options->getSeeds($name); $timeout = $options->getTimeout($name, $fallbackTimeout); $readTimeout = $options->getReadTimeout($name, $fallbackReadTimeout); + $password = $options->getPasswordByName($name, $fallbackPassword); - return new RedisClusterFromExtension($name, $seeds, $timeout, $readTimeout, $persistent); + return new RedisClusterFromExtension( + null, + $seeds, + $timeout, + $readTimeout, + $persistent, + $password + ); } /** diff --git a/test/unit/RedisClusterOptionsFromIniTest.php b/test/unit/RedisClusterOptionsFromIniTest.php index 2630a12..936700c 100644 --- a/test/unit/RedisClusterOptionsFromIniTest.php +++ b/test/unit/RedisClusterOptionsFromIniTest.php @@ -66,6 +66,20 @@ public function seedsByNameProvider(): array ]; } + public function testCanParseAllConfigurationsForName(): void + { + ini_set('redis.clusters.seeds', 'foo[]=bar'); + ini_set('redis.clusters.timeout', 'foo=1.0'); + ini_set('redis.clusters.read_timeout', 'foo=2.0'); + ini_set('redis.clusters.auth', 'foo=secret'); + $options = new RedisClusterOptionsFromIni(); + + $this->assertEquals(['bar'], $options->getSeeds('foo')); + $this->assertEquals(1.0, $options->getTimeout('foo', 0.0)); + $this->assertEquals(2.0, $options->getReadTimeout('foo', 0.0)); + $this->assertEquals('secret', $options->getPasswordByName('foo', '')); + } + protected function setUp(): void { parent::setUp(); diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php index 2ef50a9..2dd6cd0 100644 --- a/test/unit/RedisClusterOptionsTest.php +++ b/test/unit/RedisClusterOptionsTest.php @@ -19,13 +19,15 @@ public function testCanHandleOptionsWithNodename(): void 'read_timeout' => 2.0, 'persistent' => false, 'redis_version' => '1.0', + 'password' => 'secret', ]); - $this->assertEquals($options->getName(), 'foo'); - $this->assertEquals($options->getTimeout(), 1.0); - $this->assertEquals($options->getReadTimeout(), 2.0); - $this->assertEquals($options->isPersistent(), false); - $this->assertEquals($options->getRedisVersion(), '1.0'); + $this->assertEquals('foo', $options->getName()); + $this->assertEquals(1.0, $options->getTimeout()); + $this->assertEquals(2.0, $options->getReadTimeout()); + $this->assertEquals(false, $options->isPersistent()); + $this->assertEquals('1.0', $options->getRedisVersion()); + $this->assertEquals('secret', $options->getPassword()); } public function testCanHandleOptionsWithSeeds(): void @@ -36,13 +38,15 @@ public function testCanHandleOptionsWithSeeds(): void 'read_timeout' => 2.0, 'persistent' => false, 'redis_version' => '1.0', + 'password' => 'secret', ]); - $this->assertEquals($options->getSeeds(), ['localhost:1234']); - $this->assertEquals($options->getTimeout(), 1.0); - $this->assertEquals($options->getReadTimeout(), 2.0); - $this->assertEquals($options->isPersistent(), false); - $this->assertEquals($options->getRedisVersion(), '1.0'); + $this->assertEquals(['localhost:1234'], $options->getSeeds()); + $this->assertEquals(1.0, $options->getTimeout()); + $this->assertEquals(2.0, $options->getReadTimeout()); + $this->assertEquals(false, $options->isPersistent()); + $this->assertEquals('1.0', $options->getRedisVersion()); + $this->assertEquals('secret', $options->getPassword()); } public function testWillDetectSeedsAndNodenameConfiguration(): void From 94a99a7b9fe86c2540e107e1bcd24eb65d5422f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Thu, 3 Jun 2021 13:56:37 +0200 Subject: [PATCH 18/21] feature: introduce `RedisClusterOptions::OPT_*` and `RedisClusterOptions::LIBRARY_OPTIONS` constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Having these constants available gives us some more flexibility and avoids runtime checks. Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisCluster.php | 41 ++++++--- src/RedisClusterOptions.php | 50 +++++----- src/RedisClusterResourceManager.php | 53 +++-------- src/RedisClusterResourceManagerInterface.php | 1 + test/unit/RedisClusterOptionsTest.php | 96 ++++++++++++++++++++ test/unit/RedisClusterTest.php | 6 +- 6 files changed, 171 insertions(+), 76 deletions(-) diff --git a/src/RedisCluster.php b/src/RedisCluster.php index 46ea13e..866b190 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -39,6 +39,9 @@ final class RedisCluster extends AbstractAdapter implements /** @var string|null */ private $namespacePrefix; + /** @var RedisClusterResourceManagerInterface|null */ + private $resourceManager; + /** * @param null|array|Traversable|RedisClusterOptions $options * @psalm-param array|RedisClusterOptions|Traversable $options @@ -68,15 +71,15 @@ public function setOptions($options) $options = new RedisClusterOptions($options); } - $options->setAdapter($this); - parent::setOptions($options); return $this; } /** - * In RedisCluster, it is totally okay if just one master is being flushed. If one master is not reachable, it will - * re-sync if that master is coming back online. + * In RedisCluster, it is totally okay if just one primary server is being flushed. + * If one or more primaries are not reachable, they will re-sync if they're coming back online. + * + * One has to connect to the primaries directly using {@see Redis::connect}. */ public function flush(): bool { @@ -109,8 +112,7 @@ private function getRedisResource(): RedisClusterFromExtension return $this->resource; } - $options = $this->getOptions(); - $resourceManager = $options->getResourceManager(); + $resourceManager = $this->getResourceManager(); try { return $this->resource = $resourceManager->getResource(); @@ -402,12 +404,12 @@ private function getSupportedDatatypes(bool $serializer): array } /** + * @psalm-param RedisClusterOptions::OPT_* $option * @return mixed */ private function getLibOption(int $option) { - $options = $this->getOptions(); - $resourceManager = $options->getResourceManager(); + $resourceManager = $this->getResourceManager(); return $resourceManager->getLibOption($option); } @@ -541,15 +543,30 @@ private function detectTtlForKey(RedisClusterFromExtension $redis, string $names private function getRedisVersion(): string { - $options = $this->getOptions(); - $resourceManager = $options->getResourceManager(); + $resourceManager = $this->getResourceManager(); return $resourceManager->getVersion(); } private function hasSerializationSupport(): bool { - $options = $this->getOptions(); - $resourceManager = $options->getResourceManager(); + $resourceManager = $this->getResourceManager(); return $resourceManager->hasSerializationSupport($this); } + + private function getResourceManager(): RedisClusterResourceManagerInterface + { + if ($this->resourceManager !== null) { + return $this->resourceManager; + } + + return $this->resourceManager = new RedisClusterResourceManager($this->getOptions()); + } + + /** + * @internal This is only used for unit testing. There should be no need to use this method in upstream projects. + */ + public function setResourceManager(RedisClusterResourceManagerInterface $resourceManager): void + { + $this->resourceManager = $resourceManager; + } } diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index 62d5ce4..57bcc7a 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -10,6 +10,30 @@ final class RedisClusterOptions extends AdapterOptions { + public const LIBRARY_OPTIONS = [ + self::OPT_SERIALIZER, + self::OPT_PREFIX, + self::OPT_READ_TIMEOUT, + self::OPT_SCAN, + self::OPT_SLAVE_FAILOVER, + self::OPT_TCP_KEEPALIVE, + self::OPT_COMPRESSION, + self::OPT_REPLY_LITERAL, + self::OPT_COMPRESSION_LEVEL, + self::OPT_NULL_MULTIBULK_AS_NULL, + ]; + + public const OPT_SERIALIZER = 1; + public const OPT_PREFIX = 2; + public const OPT_READ_TIMEOUT = 3; + public const OPT_SCAN = 4; + public const OPT_SLAVE_FAILOVER = 5; + public const OPT_TCP_KEEPALIVE = 6; + public const OPT_COMPRESSION = 7; + public const OPT_REPLY_LITERAL = 8; + public const OPT_COMPRESSION_LEVEL = 9; + public const OPT_NULL_MULTIBULK_AS_NULL = 10; + /** @var string */ protected $namespaceSeparator = ':'; @@ -31,12 +55,9 @@ final class RedisClusterOptions extends AdapterOptions /** @var string */ private $version = ''; - /** @psalm-var array */ + /** @psalm-var array */ private $libOptions = []; - /** @var RedisClusterResourceManagerInterface|null */ - private $resourceManager; - /** @var string */ private $password = ''; @@ -173,7 +194,7 @@ public function getRedisVersion(): string } /** - * @psalm-param array $options + * @psalm-param array $options */ public function setLibOptions(array $options): void { @@ -181,30 +202,13 @@ public function setLibOptions(array $options): void } /** - * @psalm-return array + * @psalm-return array */ public function getLibOptions(): array { return $this->libOptions; } - /** - * @internal This method should only be used within this library to have better test coverage! - */ - public function setResourceManager(RedisClusterResourceManagerInterface $resourceManager): void - { - $this->resourceManager = $resourceManager; - } - - public function getResourceManager(): RedisClusterResourceManagerInterface - { - if ($this->resourceManager) { - return $this->resourceManager; - } - - return $this->resourceManager = new RedisClusterResourceManager($this); - } - public function getPassword(): string { return $this->password; diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index a7a43b0..2ab7f30 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -12,26 +12,20 @@ use Laminas\Cache\Storage\PluginCapableInterface; use RedisCluster as RedisClusterFromExtension; use RedisClusterException; -use ReflectionClass; use function array_key_exists; use function assert; use function extension_loaded; -use function is_int; -use function strpos; /** * @psalm-type RedisClusterInfoType = array&array{redis_version:string} */ final class RedisClusterResourceManager implements RedisClusterResourceManagerInterface { - /** @var array|null */ - private static $clusterOptionsCache; - /** @var RedisClusterOptions */ private $options; - /** @psalm-var array */ + /** @psalm-var array */ private $libraryOptions = []; public function __construct(RedisClusterOptions $options) @@ -42,31 +36,6 @@ public function __construct(RedisClusterOptions $options) } } - /** - * @return array - */ - private static function getRedisClusterOptions(): array - { - if (self::$clusterOptionsCache !== null) { - return self::$clusterOptionsCache; - } - - $reflection = new ReflectionClass(RedisClusterFromExtension::class); - - $options = []; - foreach ($reflection->getConstants() as $constant => $constantValue) { - if (strpos($constant, 'OPT_') !== 0) { - continue; - } - assert($constant !== ''); - assert(is_int($constantValue)); - - $options[$constant] = $constantValue; - } - - return self::$clusterOptionsCache = $options; - } - public function getVersion(): string { $versionFromOptions = $this->options->getRedisVersion(); @@ -162,7 +131,7 @@ private function createRedisResourceFromName( } /** - * @param array $options + * @psalm-param array $options */ private function applyLibraryOptions( RedisClusterFromExtension $resource, @@ -170,7 +139,12 @@ private function applyLibraryOptions( ): RedisClusterFromExtension { /** @psalm-suppress MixedAssignment */ foreach ($options as $option => $value) { - /** @psalm-suppress InvalidArgument,MixedArgument */ + /** + * @see https://github.com/phpredis/phpredis#setoption + * + * @psalm-suppress InvalidArgument + * @psalm-suppress MixedArgument + */ $resource->setOption($option, $value); } @@ -178,13 +152,13 @@ private function applyLibraryOptions( } /** - * @param array $options - * @return array + * @psalm-param array $options + * @psalm-return array */ private function mergeLibraryOptionsFromCluster(array $options, RedisClusterFromExtension $resource): array { - foreach (self::getRedisClusterOptions() as $constantValue) { - if (array_key_exists($constantValue, $options)) { + foreach (RedisClusterOptions::LIBRARY_OPTIONS as $option) { + if (array_key_exists($option, $options)) { continue; } @@ -193,13 +167,14 @@ private function mergeLibraryOptionsFromCluster(array $options, RedisClusterFrom * * @psalm-suppress InvalidArgument */ - $options[$constantValue] = $resource->getOption($constantValue); + $options[$option] = $resource->getOption($option); } return $options; } /** + * @psalm-param RedisClusterOptions::OPT_* $option * @return mixed */ public function getLibOption(int $option) diff --git a/src/RedisClusterResourceManagerInterface.php b/src/RedisClusterResourceManagerInterface.php index 964a8e3..438e3db 100644 --- a/src/RedisClusterResourceManagerInterface.php +++ b/src/RedisClusterResourceManagerInterface.php @@ -14,6 +14,7 @@ public function getVersion(): string; public function getResource(): RedisClusterFromExtension; /** + * @psalm-param RedisClusterOptions::OPT_* $option * @return mixed */ public function getLibOption(int $option); diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php index 2dd6cd0..3c27da3 100644 --- a/test/unit/RedisClusterOptionsTest.php +++ b/test/unit/RedisClusterOptionsTest.php @@ -4,10 +4,20 @@ namespace LaminasTest\Cache\Storage\Adapter; +use Generator; use InvalidArgumentException; use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use PHPUnit\Framework\TestCase; +use RedisCluster as RedisClusterFromExtension; +use ReflectionClass; + +use function assert; +use function constant; +use function defined; +use function is_int; +use function sprintf; +use function strpos; final class RedisClusterOptionsTest extends TestCase { @@ -81,4 +91,90 @@ public function testWillDetectMissingRequiredValues(): void $this->expectExceptionMessage('Missing either `name` or `seeds`.'); new RedisClusterOptions(); } + + /** + * @psalm-param non-empty-string $constant + * @psalm-param positive-int $constantValue + * @dataProvider redisClusterOptionConstants + */ + public function testOptionConstantsMatchingExtensionImplementation(string $constant, int $constantValue): void + { + $constantInOptions = sprintf('%s::%s', RedisClusterOptions::class, $constant); + + if (! defined($constantInOptions)) { + self::markTestSkipped(sprintf( + 'Constant "%s" with value "%d" is not defined.', + $constantInOptions, + $constantValue + )); + } + + /** @psalm-suppress MixedAssignment */ + $constantValueInOptions = constant($constantInOptions); + self::assertIsInt($constantValueInOptions); + self::assertEquals( + $constantValue, + $constantValueInOptions, + sprintf( + 'Constant "%s" diverged from ext-redis. Expected "%d", "%d" declared.', + $constant, + $constantValue, + $constantValueInOptions + ) + ); + } + + /** + * @psalm-return Generator + */ + public function redisClusterOptionConstants(): Generator + { + $reflection = new ReflectionClass(RedisClusterFromExtension::class); + + foreach ($reflection->getConstants() as $constant => $constantValue) { + if (strpos($constant, 'OPT_') !== 0) { + continue; + } + + assert($constant !== ''); + assert(is_int($constantValue) && $constantValue > 0); + yield $constant => [$constant, $constantValue]; + } + } + + /** + * @psalm-param non-empty-string $constant + * @psalm-param positive-int $constantValue + * @dataProvider declaredLibraryOptionConstants + */ + public function testLibraryOptionsConstantContainsAllDeclaredConstants(string $constant, int $constantValue): void + { + self::assertContains( + $constantValue, + RedisClusterOptions::LIBRARY_OPTIONS, + sprintf( + 'Missing constant "%s" in %s::LIBRARY_OPTIONS', + $constant, + RedisClusterOptions::class + ) + ); + } + + /** + * @psalm-return Generator + */ + public function declaredLibraryOptionConstants(): Generator + { + $reflection = new ReflectionClass(RedisClusterOptions::class); + + foreach ($reflection->getConstants() as $constant => $constantValue) { + if (strpos($constant, 'OPT_') !== 0) { + continue; + } + + assert($constant !== ''); + assert(is_int($constantValue) && $constantValue > 0); + yield $constant => [$constant, $constantValue]; + } + } } diff --git a/test/unit/RedisClusterTest.php b/test/unit/RedisClusterTest.php index 1978d92..f1296e1 100644 --- a/test/unit/RedisClusterTest.php +++ b/test/unit/RedisClusterTest.php @@ -17,7 +17,8 @@ public function testCanDetectCapabilitiesWithSerializationSupport(): void $adapter = new RedisCluster([ 'name' => 'bar', ]); - $adapter->getOptions()->setResourceManager($resourceManager); + /** @psalm-suppress InternalMethod */ + $adapter->setResourceManager($resourceManager); $resourceManager ->expects($this->once()) @@ -51,7 +52,8 @@ public function testCanDetectCapabilitiesWithoutSerializationSupport(): void $adapter = new RedisCluster([ 'name' => 'bar', ]); - $adapter->getOptions()->setResourceManager($resourceManager); + /** @psalm-suppress InternalMethod */ + $adapter->setResourceManager($resourceManager); $resourceManager ->expects($this->once()) From b11b418dd666bd45c5d45e7200c7f47d877d407f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Thu, 3 Jun 2021 14:08:24 +0200 Subject: [PATCH 19/21] qa: optimize some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisClusterOptions.php | 10 ++++++++++ src/RedisClusterResourceManager.php | 22 +++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index 57bcc7a..1542dd1 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -209,6 +209,16 @@ public function getLibOptions(): array return $this->libOptions; } + /** + * @psalm-param RedisClusterOptions::OPT_* $option + * @param mixed $default + * @return mixed + */ + public function getLibOption(int $option, $default = null) + { + return $this->libOptions[$option] ?? $default; + } + public function getPassword(): string { return $this->password; diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 2ab7f30..96c008e 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -179,20 +179,32 @@ private function mergeLibraryOptionsFromCluster(array $options, RedisClusterFrom */ public function getLibOption(int $option) { + if (array_key_exists($option, $this->libraryOptions)) { + return $this->libraryOptions[$option]; + } + /** * @see https://github.com/phpredis/phpredis#getoption * * @psalm-suppress InvalidArgument */ - return $this->libraryOptions[$option] ?? $this->getResource()->getOption($option); + return $this->libraryOptions[$option] = $this->getResource()->getOption($option); } public function hasSerializationSupport(PluginCapableInterface $adapter): bool { - $options = $this->options; - $libraryOptions = $options->getLibOptions(); - $serializer = $libraryOptions[RedisClusterFromExtension::OPT_SERIALIZER] ?? - RedisClusterFromExtension::SERIALIZER_NONE; + /** + * NOTE: we are not using {@see RedisClusterResourceManager::getLibOption} here + * as this would create a connection to redis even tho it wont be needed. + * Theoretically, it would be possible for upstream projects to receive the resource directly from the + * resource manager and then apply changes to it. As this is not the common use-case, this is not + * considered in this check. + */ + $options = $this->options; + $serializer = $options->getLibOption( + RedisClusterFromExtension::OPT_SERIALIZER, + RedisClusterFromExtension::SERIALIZER_NONE + ); if ($serializer !== RedisClusterFromExtension::SERIALIZER_NONE) { return true; From fe876d071f27e3153037ee4220811dd39b504e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:34:58 +0200 Subject: [PATCH 20/21] qa: add proper maximum key length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since redis 3.0 is available via docker, I've verified that at least since v3, keys with 512 MB work. See https://redis.io/topics/data-types-intro#redis-keys for more details Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisCluster.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RedisCluster.php b/src/RedisCluster.php index 866b190..db116d0 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -351,6 +351,7 @@ protected function internalGetCapabilities(): Capabilities $redisVersion = $this->getRedisVersion(); $serializer = $this->hasSerializationSupport(); $redisVersionLessThanV2 = version_compare($redisVersion, '2.0', '<'); + $redisVersionLessThanV3 = version_compare($redisVersion, '3.0', '<'); $minTtl = $redisVersionLessThanV2 ? 0 : 1; $supportedMetadata = ! $redisVersionLessThanV2 ? ['ttl'] : []; @@ -365,7 +366,7 @@ protected function internalGetCapabilities(): Capabilities 'staticTtl' => true, 'ttlPrecision' => 1, 'useRequestTime' => false, - 'maxKeyLength' => 255, + 'maxKeyLength' => $redisVersionLessThanV3 ? 255 : 512000000, 'namespaceIsPrefix' => true, ] ); From 3606b0773c5b8781b38a68649ed7a957bee02350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:40:25 +0200 Subject: [PATCH 21/21] qa: ensure redis extension is loaded until v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/RedisCluster.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/RedisCluster.php b/src/RedisCluster.php index db116d0..aa9b9be 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -21,6 +21,7 @@ use function array_key_exists; use function array_values; use function count; +use function extension_loaded; use function in_array; use function sprintf; use function version_compare; @@ -48,6 +49,10 @@ final class RedisCluster extends AbstractAdapter implements */ public function __construct($options = null) { + if (! extension_loaded('redis')) { + throw new Exception\ExtensionNotLoadedException("Redis extension is not loaded"); + } + /** @psalm-suppress PossiblyInvalidArgument */ parent::__construct($options); $eventManager = $this->getEventManager();