diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index e6a6064b..c350814f 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -13,6 +13,7 @@ namespace Predis; use Predis\Command\Argument\Geospatial\ByInterface; use Predis\Command\Argument\Geospatial\FromInterface; +use Predis\Command\Argument\Server\To; use Predis\Command\CommandInterface; /** @@ -48,6 +49,7 @@ use Predis\Command\CommandInterface; * @method $this bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1) * @method $this decr($key) * @method $this decrby($key, $decrement) + * @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1) * @method $this get($key) * @method $this getbit($key, $offset) * @method $this getex(string $key, $modifier = '', $value = false) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 3acd5fa5..4e98251c 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -13,6 +13,7 @@ namespace Predis; use Predis\Command\Argument\Geospatial\ByInterface; use Predis\Command\Argument\Geospatial\FromInterface; +use Predis\Command\Argument\Server\To; use Predis\Command\CommandInterface; use Predis\Command\FactoryInterface; use Predis\Configuration\OptionsInterface; @@ -57,6 +58,7 @@ use Predis\Response\Status; * @method array bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 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) * @method string|null get(string $key) * @method int getbit(string $key, $offset) * @method int|null getex(string $key, $modifier = '', $value = false) diff --git a/src/Command/Argument/Server/To.php b/src/Command/Argument/Server/To.php new file mode 100644 index 00000000..083b63df --- /dev/null +++ b/src/Command/Argument/Server/To.php @@ -0,0 +1,47 @@ +host = $host; + $this->port = $port; + $this->isForce = $isForce; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $arguments = [self::KEYWORD, $this->host, $this->port]; + + if ($this->isForce) { + $arguments[] = self::FORCE_KEYWORD; + } + + return $arguments; + } +} diff --git a/src/Command/Redis/FAILOVER.php b/src/Command/Redis/FAILOVER.php new file mode 100644 index 00000000..98c30fed --- /dev/null +++ b/src/Command/Redis/FAILOVER.php @@ -0,0 +1,38 @@ +setTimeout($arguments); + $arguments = $this->getArguments(); + + $this->setTo($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/Traits/Timeout.php b/src/Command/Traits/Timeout.php new file mode 100644 index 00000000..2a529c59 --- /dev/null +++ b/src/Command/Traits/Timeout.php @@ -0,0 +1,41 @@ += $argumentsLength) { + parent::setArguments($arguments); + return; + } + + if ($arguments[static::$timeoutArgumentPositionOffset] === -1) { + array_splice($arguments, static::$timeoutArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + return; + } + + if ($arguments[static::$timeoutArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong timeout argument value or position offset'); + } + + $argument = $arguments[static::$timeoutArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$timeoutArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$timeoutArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$timeoutModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/To/ServerTo.php b/src/Command/Traits/To/ServerTo.php new file mode 100644 index 00000000..1ea93305 --- /dev/null +++ b/src/Command/Traits/To/ServerTo.php @@ -0,0 +1,36 @@ += $argumentsLength) { + parent::setArguments($arguments); + return; + } + + /** @var To|null $toArgument */ + $toArgument = $arguments[static::$toArgumentPositionOffset]; + + if (null === $toArgument) { + array_splice($arguments, static::$toArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + return; + } + + $argumentsBefore = array_slice($arguments, 0, static::$toArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$toArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $toArgument->toArray(), + $argumentsAfter + )); + } +} diff --git a/tests/Predis/Command/Redis/FAILOVER_Test.php b/tests/Predis/Command/Redis/FAILOVER_Test.php new file mode 100644 index 00000000..d6267ecf --- /dev/null +++ b/tests/Predis/Command/Redis/FAILOVER_Test.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command\Redis; + +use Predis\Command\Argument\Server\To; +use UnexpectedValueException; + +/** + * @group commands + * @group realm-server + */ +class FAILOVER_Test extends PredisCommandTestCase +{ + /** + * {@inheritdoc} + */ + protected function getExpectedCommand(): string + { + return FAILOVER::class; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedId(): string + { + return 'FAILOVER'; + } + + /** + * @group disconnected + * @dataProvider argumentsProvider + */ + public function testFilterArguments(array $actualArguments, array $expectedArguments): void + { + $command = $this->getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse(): void + { + $this->assertSame(1, $this->getCommand()->parseResponse(1)); + } + + /** + * @group connected + * @return void + * @requiresRedisVersion >= 6.2.0 + */ + public function testPerformFailoverOfConnectedReplica(): void + { + $this->markTestSkipped('Test requires configured replica node connected to master'); + } + + /** + * @group connected + * @return void + * @requiresRedisVersion >= 6.2.0 + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $redis = $this->getClient(); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong timeout argument value or position offset'); + + $redis->failover(null, false, 0); + } + + public function argumentsProvider(): array + { + return [ + 'without optional arguments - no arguments' => [ + [], + [] + ], + 'without optional arguments - default arguments' => [ + [null, false, -1], + [] + ], + 'with TO argument - no FORCE' => [ + [new To('test', 9999)], + ['TO', 'test', 9999] + ], + 'with TO argument - with FORCE' => [ + [new To('test', 9999, true)], + ['TO', 'test', 9999, 'FORCE'] + ], + 'with ABORT modifier' => [ + [null, true], + ['ABORT'] + ], + 'with TIMEOUT argument' => [ + [null, false, 10], + ['TIMEOUT', 10] + ], + 'with all arguments' => [ + [new To('test', 9999, true), true, 10], + ['TO', 'test', 9999, 'FORCE', 'ABORT', 'TIMEOUT', 10] + ] + ]; + } +} diff --git a/tests/Predis/Command/Traits/TimeoutTest.php b/tests/Predis/Command/Traits/TimeoutTest.php new file mode 100644 index 00000000..dd3b2b93 --- /dev/null +++ b/tests/Predis/Command/Traits/TimeoutTest.php @@ -0,0 +1,76 @@ +testClass = new class extends RedisCommand { + use Timeout; + + public static $timeoutArgumentPositionOffset = 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::$timeoutArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong timeout argument value or position offset'); + + $this->testClass->setArguments([0]); + } + + public function argumentsProvider(): array + { + return [ + 'with TIMEOUT argument' => [ + 0, + [10], + ['TIMEOUT', 10] + ], + 'with wrong offset given' => [ + 1, + [], + [] + ], + 'with default value' => [ + 0, + [-1], + [false] + ] + ]; + } +} diff --git a/tests/Predis/Command/Traits/To/ServerToTest.php b/tests/Predis/Command/Traits/To/ServerToTest.php new file mode 100644 index 00000000..62696824 --- /dev/null +++ b/tests/Predis/Command/Traits/To/ServerToTest.php @@ -0,0 +1,70 @@ +testClass = new class extends RedisCommand { + use ServerTo; + + public static $toArgumentPositionOffset = 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::$toArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + public function argumentsProvider(): array + { + return [ + 'with TO argument - no FORCE' => [ + 0, + [new To('host', 9999)], + ['TO', 'host', 9999] + ], + 'with TO argument - with FORCE' => [ + 0, + [new To('host', 9999, true)], + ['TO', 'host', 9999, 'FORCE'] + ], + 'with wrong offset given' => [ + 1, + [], + [] + ], + 'with default value' => [ + 0, + [null], + [false] + ] + ]; + } +}