From 7eb464a0dde0b72ae80fe0bf52e8b3009a34f405 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:46:37 +0200 Subject: [PATCH] Extended CuckooFilters support by implementing CF.RESERVE command (#1086) * Added support for CF.RESERVE command * Codestyle fixes --------- Co-authored-by: Vladyslav Vildanov --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Command/Redis/CuckooFilter/CFRESERVE.php | 52 ++++++ .../Traits/BloomFilters/BucketSize.php | 57 +++++++ .../Traits/BloomFilters/MaxIterations.php | 57 +++++++ .../Redis/CuckooFilter/CFRESERVE_Test.php | 160 ++++++++++++++++++ .../Traits/BloomFilters/BucketSizeTest.php | 86 ++++++++++ .../Traits/BloomFilters/MaxIterationsTest.php | 86 ++++++++++ 8 files changed, 500 insertions(+) create mode 100644 src/Command/Redis/CuckooFilter/CFRESERVE.php create mode 100644 src/Command/Traits/BloomFilters/BucketSize.php create mode 100644 src/Command/Traits/BloomFilters/MaxIterations.php create mode 100644 tests/Predis/Command/Redis/CuckooFilter/CFRESERVE_Test.php create mode 100644 tests/Predis/Command/Traits/BloomFilters/BucketSizeTest.php create mode 100644 tests/Predis/Command/Traits/BloomFilters/MaxIterationsTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 088050bd..d78750c4 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -67,6 +67,7 @@ use Predis\Command\CommandInterface; * @method $this cfmexists(string $key, ...$item) * @method $this cfinfo(string $key) * @method $this cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) + * @method $this cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1) * @method $this decr($key) * @method $this decrby($key, $decrement) * @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index b1a19c54..66e2df8e 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -76,6 +76,7 @@ use Predis\Response\Status; * @method int cfmexists(string $key, ...$item) * @method array cfinfo(string $key) * @method array cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item) + * @method Status cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1) * @method int decr(string $key) * @method int decrby(string $key, int $decrement) * @method Status failover(?To $to = null, bool $abort = false, int $timeout = -1) diff --git a/src/Command/Redis/CuckooFilter/CFRESERVE.php b/src/Command/Redis/CuckooFilter/CFRESERVE.php new file mode 100644 index 00000000..bfdb93aa --- /dev/null +++ b/src/Command/Redis/CuckooFilter/CFRESERVE.php @@ -0,0 +1,52 @@ +setExpansion($arguments); + $arguments = $this->getArguments(); + + $this->setMaxIterations($arguments); + $arguments = $this->getArguments(); + + $this->setBucketSize($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/Traits/BloomFilters/BucketSize.php b/src/Command/Traits/BloomFilters/BucketSize.php new file mode 100644 index 00000000..99e22be0 --- /dev/null +++ b/src/Command/Traits/BloomFilters/BucketSize.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$bucketSizeArgumentPositionOffset] === -1) { + array_splice($arguments, static::$bucketSizeArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$bucketSizeArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong bucket size argument value or position offset'); + } + + $argument = $arguments[static::$bucketSizeArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$bucketSizeArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$bucketSizeArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$bucketSizeModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/BloomFilters/MaxIterations.php b/src/Command/Traits/BloomFilters/MaxIterations.php new file mode 100644 index 00000000..fb307e6d --- /dev/null +++ b/src/Command/Traits/BloomFilters/MaxIterations.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$maxIterationsArgumentPositionOffset] === -1) { + array_splice($arguments, static::$maxIterationsArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$maxIterationsArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong max iterations argument value or position offset'); + } + + $argument = $arguments[static::$maxIterationsArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$maxIterationsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$maxIterationsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$maxIterationsModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/tests/Predis/Command/Redis/CuckooFilter/CFRESERVE_Test.php b/tests/Predis/Command/Redis/CuckooFilter/CFRESERVE_Test.php new file mode 100644 index 00000000..5e9bf589 --- /dev/null +++ b/tests/Predis/Command/Redis/CuckooFilter/CFRESERVE_Test.php @@ -0,0 +1,160 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group connected + * @dataProvider filtersProvider + * @param array $filterArguments + * @param int $expectedCapacity + * @param int $expectedBucketSize + * @param int $expectedMaxIterations + * @param int $expectedExpansion + * @return void + * @requiresRedisBfVersion >= 1.0.0 + */ + public function testReserveCreatesCuckooFilterWithCorrectConfiguration( + array $filterArguments, + int $expectedCapacity, + int $expectedBucketSize, + int $expectedMaxIterations, + int $expectedExpansion + ): void { + $redis = $this->getClient(); + + $actualResponse = $redis->cfreserve(...$filterArguments); + $this->assertEquals('OK', $actualResponse); + + $info = $redis->cfinfo('key'); + + $this->assertSame($expectedCapacity, $info['Size']); + $this->assertSame($expectedBucketSize, $info['Bucket size']); + $this->assertSame($expectedMaxIterations, $info['Max iterations']); + $this->assertSame($expectedExpansion, $info['Expansion rate']); + } + + /** + * @group connected + * @requiresRedisBfVersion >= 1.0.0 + */ + public function testThrowsExceptionOnWrongType(): void + { + $this->expectException(ServerException::class); + $this->expectExceptionMessage('Operation against a key holding the wrong kind of value'); + + $redis = $this->getClient(); + + $redis->set('cfreserve_foo', 'bar'); + $redis->cfreserve('cfreserve_foo', 500); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + ['key', 500], + ['key', 500], + ], + 'with BUCKETSIZE argument' => [ + ['key', 500, 2], + ['key', 500, 'BUCKETSIZE', 2], + ], + 'with MAXITERATIONS argument' => [ + ['key', 500, -1, 15], + ['key', 500, 'MAXITERATIONS', 15], + ], + 'with EXPANSION argument' => [ + ['key', 500, -1, -1, 3], + ['key', 500, 'EXPANSION', 3], + ], + 'with all arguments' => [ + ['key', 500, 2, 15, 3], + ['key', 500, 'BUCKETSIZE', 2, 'MAXITERATIONS', 15, 'EXPANSION', 3], + ], + ]; + } + + public function filtersProvider(): array + { + return [ + 'with default arguments' => [ + ['key', 500, -1, -1, -1], + 568, + 2, + 20, + 1, + ], + 'with modified bucket size' => [ + ['key', 1000, 3, -1, -1], + 1592, + 3, + 20, + 1, + ], + 'with modified max iterations' => [ + ['key', 1000, -1, 15, -1], + 1080, + 2, + 15, + 1, + ], + 'with modified expansion' => [ + ['key', 1000, -1, -1, 3], + 1080, + 2, + 20, + 4, + ], + 'with all arguments' => [ + ['key', 1000, 3, 15, 3], + 1592, + 3, + 15, + 4, + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/BloomFilters/BucketSizeTest.php b/tests/Predis/Command/Traits/BloomFilters/BucketSizeTest.php new file mode 100644 index 00000000..17cecf47 --- /dev/null +++ b/tests/Predis/Command/Traits/BloomFilters/BucketSizeTest.php @@ -0,0 +1,86 @@ +testClass = new class() extends RedisCommand { + use BucketSize; + + public static $bucketSizeArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $arguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $arguments, array $expectedResponse): void + { + $this->testClass::$bucketSizeArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsErrorOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong bucket size argument value or position offset'); + + $this->testClass->setArguments([-5]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + [], + [], + ], + 'with default argument' => [ + 0, + [-1], + [false], + ], + 'with non-default argument' => [ + 0, + [10], + ['BUCKETSIZE', 10], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/BloomFilters/MaxIterationsTest.php b/tests/Predis/Command/Traits/BloomFilters/MaxIterationsTest.php new file mode 100644 index 00000000..b2b04707 --- /dev/null +++ b/tests/Predis/Command/Traits/BloomFilters/MaxIterationsTest.php @@ -0,0 +1,86 @@ +testClass = new class() extends RedisCommand { + use MaxIterations; + + public static $maxIterationsArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $arguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $arguments, array $expectedResponse): void + { + $this->testClass::$maxIterationsArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsErrorOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong max iterations argument value or position offset'); + + $this->testClass->setArguments([-5]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + [], + [], + ], + 'with default argument' => [ + 0, + [-1], + [false], + ], + 'with non-default argument' => [ + 0, + [10], + ['MAXITERATIONS', 10], + ], + ]; + } +}