Improved pipeline abstractions (#1438)

* Added pipelining for on-connection commands

* Added server version condition for tests

* Fixed static analysis and codestyle errors

* Improved handshake session

* Updated HELLO retry logic

* Changed incorrect variable

* Added return statement

* Added more test coverage, codestyle fixes

* Improved pipelining abstractions

* Fixed unit tests

* Fixed typo

* Fixed another typo

* Fixed another typo

* Updated deserializeCommand signature

* Codestyle fixes

* Codestyle fixes

* Changed pipelined sequence

* Removed redundant method

* Fixed classname
This commit is contained in:
Vladyslav Vildanov
2024-03-07 15:38:04 +02:00
committed by GitHub
parent 8b464f2a60
commit 40d72d716d
23 changed files with 739 additions and 84 deletions
+45
View File
@@ -12,6 +12,9 @@
namespace Predis\Command;
use Predis\ClientConfiguration;
use UnexpectedValueException;
/**
* Base class for Redis commands.
*/
@@ -152,4 +155,46 @@ abstract class Command implements CommandInterface
return $buffer;
}
/**
* {@inheritDoc}
*/
public static function deserializeCommand(string $serializedCommand): CommandInterface
{
if ($serializedCommand[0] !== '*') {
throw new UnexpectedValueException('Invalid serializing format');
}
$commandArray = explode("\r\n", $serializedCommand);
$commandId = $commandArray[2];
$classPath = __NAMESPACE__ . '\Redis\\';
// Check if given command is a module command.
if (count($commandIdArray = explode('.', $commandId)) > 1) {
// Fetch module configuration to resolve namespace.
$moduleConfiguration = array_filter(
ClientConfiguration::getModules(),
static function ($module) use ($commandIdArray) {
return $module['commandPrefix'] === $commandIdArray[0];
}
);
$commandClass = strtoupper($commandIdArray[0] . $commandIdArray[1]);
$classPath .= array_shift($moduleConfiguration)['name'] . '\\' . $commandClass;
} else {
$classPath .= $commandIdArray[0];
}
$command = new $classPath();
$arguments = [];
for ($i = 4, $iMax = count($commandArray); $i < $iMax; $i++) {
$arguments[] = $commandArray[$i];
++$i;
}
$command->setArguments($arguments);
return $command;
}
}
+8
View File
@@ -92,4 +92,12 @@ interface CommandInterface
* @return string
*/
public function serializeCommand(): string;
/**
* Creates command object from given serialized representation.
*
* @param string $serializedCommand
* @return static
*/
public static function deserializeCommand(string $serializedCommand): CommandInterface;
}
+42
View File
@@ -12,6 +12,9 @@
namespace Predis\Command;
use Predis\ClientConfiguration;
use UnexpectedValueException;
/**
* Class representing a generic Redis command.
*
@@ -149,4 +152,43 @@ final class RawCommand implements CommandInterface
return $buffer;
}
public static function deserializeCommand(string $serializedCommand): CommandInterface
{
if ($serializedCommand[0] !== '*') {
throw new UnexpectedValueException('Invalid serializing format');
}
$commandArray = explode("\r\n", $serializedCommand);
$commandId = $commandArray[2];
$classPath = __NAMESPACE__ . '\Redis\\';
// Check if given command is a module command.
if (count($commandIdArray = explode('.', $commandId)) > 1) {
// Fetch module configuration to resolve namespace.
$moduleConfiguration = array_filter(
ClientConfiguration::getModules(),
static function ($module) use ($commandIdArray) {
return $module['commandPrefix'] === $commandIdArray[0];
}
);
$commandClass = strtoupper($commandIdArray[0] . $commandIdArray[1]);
$classPath .= array_shift($moduleConfiguration)['name'] . '\\' . $commandClass;
} else {
$classPath .= $commandIdArray[0];
}
$command = new $classPath();
$arguments = [];
for ($i = 4, $iMax = count($commandArray); $i < $iMax; $i++) {
$arguments[] = $commandArray[$i];
++$i;
}
$command->setArguments($arguments);
return $command;
}
}
@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Connection;
use Predis\Command\Command;
use Predis\Command\CommandInterface;
abstract class AbstractAggregateConnection implements AggregateConnectionInterface
{
/**
* {@inheritDoc}
*/
abstract public function add(NodeConnectionInterface $connection);
/**
* {@inheritDoc}
*/
abstract public function remove(NodeConnectionInterface $connection);
/**
* {@inheritDoc}
*/
abstract public function getConnectionByCommand(CommandInterface $command);
/**
* {@inheritDoc}
*/
abstract public function getConnectionById($connectionID);
/**
* {@inheritDoc}
*/
abstract public function connect();
/**
* {@inheritDoc}
*/
abstract public function disconnect();
/**
* {@inheritDoc}
*/
abstract public function isConnected();
/**
* {@inheritDoc}
*/
abstract public function writeRequest(CommandInterface $command);
/**
* {@inheritDoc}
*/
abstract public function readResponse(CommandInterface $command);
/**
* {@inheritDoc}
*/
abstract public function executeCommand(CommandInterface $command);
/**
* {@inheritDoc}
*/
abstract public function getParameters();
/**
* {@inheritDoc}
*/
public function write(string $buffer): void
{
$rawCommands = [];
$explodedBuffer = explode("\r\n", trim($buffer));
while (!empty($explodedBuffer)) {
$argsLen = (int) explode('*', $explodedBuffer[0])[1];
$cmdLen = ($argsLen * 2) + 1;
$rawCommands[] = array_splice($explodedBuffer, 0, $cmdLen);
}
foreach ($rawCommands as $command) {
$command = implode("\r\n", $command) . "\r\n";
$commandObj = Command::deserializeCommand($command);
$this->getConnectionByCommand($commandObj)->write($command);
}
}
}
+3 -1
View File
@@ -17,7 +17,9 @@ use Countable;
use IteratorAggregate;
use Predis\Cluster\PredisStrategy;
use Predis\Cluster\StrategyInterface;
use Predis\Command\Command;
use Predis\Command\CommandInterface;
use Predis\Connection\AbstractAggregateConnection;
use Predis\Connection\NodeConnectionInterface;
use Predis\Connection\ParametersInterface;
use Predis\NotSupportedException;
@@ -28,7 +30,7 @@ use Traversable;
* Abstraction for a cluster of aggregate connections to various Redis servers
* implementing client-side sharding based on pluggable distribution strategies.
*/
class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
class PredisCluster extends AbstractAggregateConnection implements ClusterInterface, IteratorAggregate, Countable
{
/**
* @var NodeConnectionInterface[]
+3 -1
View File
@@ -20,8 +20,10 @@ use Predis\ClientException;
use Predis\Cluster\RedisStrategy as RedisClusterStrategy;
use Predis\Cluster\SlotMap;
use Predis\Cluster\StrategyInterface;
use Predis\Command\Command;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\Connection\AbstractAggregateConnection;
use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface;
use Predis\Connection\NodeConnectionInterface;
@@ -54,7 +56,7 @@ use Traversable;
* Asking for the cluster configuration to Redis is actually done by issuing a
* CLUSTER SLOTS command to a random node in the pool.
*/
class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
class RedisCluster extends AbstractAggregateConnection implements ClusterInterface, IteratorAggregate, Countable
{
private $useClusterSlots = true;
+9
View File
@@ -53,6 +53,15 @@ interface ConnectionInterface
*/
public function readResponse(CommandInterface $command);
/**
* Performs a write operation over the stream of the buffer containing a
* command serialized with the Redis wire protocol.
*
* @param string $buffer
* @return void
*/
public function write(string $buffer): void;
/**
* Writes a request for the given command over the connection and reads back
* the response returned by Redis.
@@ -14,8 +14,10 @@ namespace Predis\Connection\Replication;
use InvalidArgumentException;
use Predis\ClientException;
use Predis\Command\Command;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\Connection\AbstractAggregateConnection;
use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface;
use Predis\Connection\NodeConnectionInterface;
@@ -28,7 +30,7 @@ use Predis\Response\ErrorInterface as ResponseErrorInterface;
* Aggregate connection handling replication of Redis nodes configured in a
* single master / multiple slaves setup.
*/
class MasterSlaveReplication implements ReplicationInterface
class MasterSlaveReplication extends AbstractAggregateConnection implements ReplicationInterface
{
/**
* @var ReplicationStrategy
@@ -13,9 +13,11 @@
namespace Predis\Connection\Replication;
use InvalidArgumentException;
use Predis\Command\Command;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\CommunicationException;
use Predis\Connection\AbstractAggregateConnection;
use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface as ConnectionFactoryInterface;
use Predis\Connection\NodeConnectionInterface;
@@ -31,7 +33,7 @@ use Predis\Response\ServerException;
* @author Daniele Alessandri <suppakilla@gmail.com>
* @author Ville Mattila <ville@eventio.fi>
*/
class SentinelReplication implements ReplicationInterface
class SentinelReplication extends AbstractAggregateConnection implements ReplicationInterface
{
/**
* @var NodeConnectionInterface
+4 -1
View File
@@ -63,11 +63,14 @@ class Atomic extends Pipeline
{
$commandFactory = $this->getClient()->getCommandFactory();
$connection->executeCommand($commandFactory->create('multi'));
$buffer = '';
foreach ($commands as $command) {
$connection->writeRequest($command);
$buffer .= $command->serializeCommand();
}
$connection->write($buffer);
foreach ($commands as $command) {
$response = $connection->readResponse($command);
+12 -16
View File
@@ -56,13 +56,16 @@ class ConnectionErrorProof extends Pipeline
{
$responses = [];
$sizeOfPipe = count($commands);
$buffer = '';
foreach ($commands as $command) {
try {
$connection->writeRequest($command);
} catch (CommunicationException $exception) {
return array_fill(0, $sizeOfPipe, $exception);
}
$buffer .= $command->serializeCommand();
}
try {
$connection->write($buffer);
} catch (CommunicationException $exception) {
return array_fill(0, $sizeOfPipe, $exception);
}
for ($i = 0; $i < $sizeOfPipe; ++$i) {
@@ -89,21 +92,14 @@ class ConnectionErrorProof extends Pipeline
$responses = [];
$sizeOfPipe = count($commands);
$exceptions = [];
$buffer = '';
foreach ($commands as $command) {
$cmdConnection = $connection->getConnectionByCommand($command);
if (isset($exceptions[spl_object_hash($cmdConnection)])) {
continue;
}
try {
$cmdConnection->writeRequest($command);
} catch (CommunicationException $exception) {
$exceptions[spl_object_hash($cmdConnection)] = $exception;
}
$buffer .= $command->serializeCommand();
}
$connection->write($buffer);
for ($i = 0; $i < $sizeOfPipe; ++$i) {
$command = $commands->dequeue();
+4 -1
View File
@@ -25,10 +25,13 @@ class FireAndForget extends Pipeline
*/
protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
{
$buffer = '';
while (!$commands->isEmpty()) {
$connection->writeRequest($commands->dequeue());
$buffer .= $commands->dequeue()->serializeCommand();
}
$connection->write($buffer);
$connection->disconnect();
return [];
+5 -1
View File
@@ -131,10 +131,14 @@ class Pipeline implements ClientContextInterface
*/
protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
{
$buffer = '';
foreach ($commands as $command) {
$connection->writeRequest($command);
$buffer .= $command->serializeCommand();
}
$connection->write($buffer);
$responses = [];
$exceptions = $this->throwServerExceptions();
$protocolVersion = (int) $connection->getParameters()->protocol;
+1 -1
View File
@@ -22,7 +22,7 @@ abstract class PredisCommandTestCase extends PredisTestCase
/**
* Returns the expected command for tests.
*
* @return Command\CommandInterface|string Instance or FQCN of the expected command
* @return CommandInterface|string Instance or FQCN of the expected command
*/
abstract protected function getExpectedCommand(): string;
+89
View File
@@ -12,8 +12,20 @@
namespace Predis\Command;
use Predis\Command\Redis\BloomFilter\BFADD;
use Predis\Command\Redis\CountMinSketch\CMSINFO;
use Predis\Command\Redis\CuckooFilter\CFADD;
use Predis\Command\Redis\GET;
use Predis\Command\Redis\Json\JSONSET;
use Predis\Command\Redis\MGET;
use Predis\Command\Redis\Search\FTSEARCH;
use Predis\Command\Redis\TDigest\TDIGESTADD;
use Predis\Command\Redis\TimeSeries\TSGET;
use Predis\Command\Redis\TopK\TOPKQUERY;
use Predis\Command\Redis\ZADD;
use PredisTestCase;
use stdClass;
use UnexpectedValueException;
class CommandTest extends PredisTestCase
{
@@ -179,4 +191,81 @@ class CommandTest extends PredisTestCase
$command->serializeCommand()
);
}
/**
* @dataProvider deserializeCommandProvider
* @group disconnected
*/
public function testDeserializeCommand(string $class, array $arguments): void
{
$command = new $class();
$command->setArguments($arguments);
$deserializedCommand = Command::deserializeCommand($command->serializeCommand());
$this->assertInstanceOf($class, $deserializedCommand);
$this->assertSame($command->getArguments(), $deserializedCommand->getArguments());
}
/**
* @group disconnected
* @return void
*/
public function testDeserializeCommandThrowsException(): void
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Invalid serializing format');
Command::deserializeCommand('foobar');
}
public function deserializeCommandProvider(): array
{
return [
'GET' => [
GET::class,
['arg1'],
],
'MGET' => [
MGET::class,
['arg1', 'arg2', 'arg3'],
],
'ZADD' => [
ZADD::class,
['key', 'value', 'key1', 'value1'],
],
'JSONSET' => [
JSONSET::class,
['key', '$', '{"key":"value"}'],
],
'BFADD' => [
BFADD::class,
['key', 'value'],
],
'CFADD' => [
CFADD::class,
['key', 'value'],
],
'CMSINFO' => [
CMSINFO::class,
['key', 'value'],
],
'FTSEARCH' => [
FTSEARCH::class,
['key', 'value'],
],
'TDIGESTADD' => [
TDIGESTADD::class,
['key', 'value'],
],
'TSGET' => [
TSGET::class,
['key'],
],
'TOPKQUERY' => [
TOPKQUERY::class,
['key'],
],
];
}
}
+89
View File
@@ -12,7 +12,19 @@
namespace Predis\Command;
use Predis\Command\Redis\BloomFilter\BFADD;
use Predis\Command\Redis\CountMinSketch\CMSINFO;
use Predis\Command\Redis\CuckooFilter\CFADD;
use Predis\Command\Redis\GET;
use Predis\Command\Redis\Json\JSONSET;
use Predis\Command\Redis\MGET;
use Predis\Command\Redis\Search\FTSEARCH;
use Predis\Command\Redis\TDigest\TDIGESTADD;
use Predis\Command\Redis\TimeSeries\TSGET;
use Predis\Command\Redis\TopK\TOPKQUERY;
use Predis\Command\Redis\ZADD;
use PredisTestCase;
use UnexpectedValueException;
class RawCommandTest extends PredisTestCase
{
@@ -151,4 +163,81 @@ class RawCommandTest extends PredisTestCase
$command->serializeCommand()
);
}
/**
* @dataProvider deserializeCommandProvider
* @group disconnected
*/
public function testDeserializeCommand(string $class, array $arguments): void
{
$command = new $class();
$command->setArguments($arguments);
$deserializedCommand = RawCommand::deserializeCommand($command->serializeCommand());
$this->assertInstanceOf($class, $deserializedCommand);
$this->assertSame($command->getArguments(), $deserializedCommand->getArguments());
}
/**
* @group disconnected
* @return void
*/
public function testDeserializeCommandThrowsException(): void
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Invalid serializing format');
RawCommand::deserializeCommand('foobar');
}
public function deserializeCommandProvider(): array
{
return [
'GET' => [
GET::class,
['arg1'],
],
'MGET' => [
MGET::class,
['arg1', 'arg2', 'arg3'],
],
'ZADD' => [
ZADD::class,
['key', 'value', 'key1', 'value1'],
],
'JSONSET' => [
JSONSET::class,
['key', '$', '{"key":"value"}'],
],
'BFADD' => [
BFADD::class,
['key', 'value'],
],
'CFADD' => [
CFADD::class,
['key', 'value'],
],
'CMSINFO' => [
CMSINFO::class,
['key', 'value'],
],
'FTSEARCH' => [
FTSEARCH::class,
['key', 'value'],
],
'TDIGESTADD' => [
TDIGESTADD::class,
['key', 'value'],
],
'TSGET' => [
TSGET::class,
['key'],
],
'TOPKQUERY' => [
TOPKQUERY::class,
['key'],
],
];
}
}
@@ -13,6 +13,7 @@
namespace Predis\Connection\Cluster;
use Predis\Command\CommandInterface;
use Predis\Command\Redis\GET;
use Predis\Connection\Parameters;
use PredisTestCase;
@@ -471,4 +472,48 @@ class PredisClusterTest extends PredisTestCase
$this->assertEquals(['response1', 'response2', 'response3'], $cluster->executeCommandOnEachNode($mockCommand));
}
/**
* @group disconnected
*/
public function testWrite(): void
{
$command1 = new GET();
$command1->setArguments(['arg1']);
$command2 = new GET();
$command2->setArguments(['arg2']);
$command3 = new GET();
$command3->setArguments(['arg3']);
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
$connection1
->expects($this->exactly(3))
->method('write')
->withConsecutive(
[$command1->serializeCommand()],
[$command2->serializeCommand()],
[$command3->serializeCommand()]
);
$connection2
->expects($this->never())
->method('write');
$connection3
->expects($this->never())
->method('write');
$cluster = new PredisCluster(new Parameters());
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
}
}
@@ -331,7 +331,7 @@ class RedisClusterTest extends PredisTestCase
);
// TODO: I'm not sure about mocking a protected method, but it'll do for now
/** @var Connection\Cluster\RedisCluster|MockObject */
/** @var RedisCluster|MockObject */
$cluster = $this->getMockBuilder('Predis\Connection\Cluster\RedisCluster')
->onlyMethods(['getRandomConnection'])
->setConstructorArgs([$factory, new Parameters()])
@@ -633,7 +633,7 @@ class RedisClusterTest extends PredisTestCase
'value:5001'
);
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
@@ -711,7 +711,7 @@ class RedisClusterTest extends PredisTestCase
->willReturn($connection4);
// TODO: I'm not sure about mocking a protected method, but it'll do for now
/** @var Connection\Cluster\RedisCluster|MockObject */
/** @var RedisCluster|MockObject */
$cluster = $this->getMockBuilder('Predis\Connection\Cluster\RedisCluster')
->onlyMethods(['getRandomConnection'])
->setConstructorArgs([$factory, new Parameters()])
@@ -740,7 +740,7 @@ class RedisClusterTest extends PredisTestCase
$this->expectException('Predis\ClientException');
$this->expectExceptionMessage('No connections available in the pool');
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->never())
@@ -758,7 +758,7 @@ class RedisClusterTest extends PredisTestCase
*/
public function testAskSlotMapReturnEmptyArrayOnEmptyConnectionsPool(): void
{
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->never())
@@ -818,7 +818,7 @@ class RedisClusterTest extends PredisTestCase
->method('create');
// TODO: I'm not sure about mocking a protected method, but it'll do for now
/** @var Connection\Cluster\RedisCluster|MockObject */
/** @var RedisCluster|MockObject */
$cluster = $this->getMockBuilder('Predis\Connection\Cluster\RedisCluster')
->onlyMethods(['getRandomConnection'])
->setConstructorArgs([$factory, new Parameters()])
@@ -884,7 +884,7 @@ class RedisClusterTest extends PredisTestCase
->method('create');
// TODO: I'm not sure about mocking a protected method, but it'll do for now
/** @var Connection\Cluster\RedisCluster|MockObject */
/** @var RedisCluster|MockObject */
$cluster = $this->getMockBuilder('Predis\Connection\Cluster\RedisCluster')
->onlyMethods(['getRandomConnection'])
->setConstructorArgs([$factory, new Parameters()])
@@ -958,7 +958,7 @@ class RedisClusterTest extends PredisTestCase
'foobar'
);
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->never())
@@ -1009,7 +1009,7 @@ class RedisClusterTest extends PredisTestCase
'foobar'
);
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
@@ -1054,7 +1054,7 @@ class RedisClusterTest extends PredisTestCase
->with($command)
->willReturnOnConsecutiveCalls('foobar', 'foobar');
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory->expects($this->never())->method('create');
@@ -1097,7 +1097,7 @@ class RedisClusterTest extends PredisTestCase
->with($command)
->willReturnOnConsecutiveCalls('foobar', 'foobar');
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
@@ -1142,7 +1142,7 @@ class RedisClusterTest extends PredisTestCase
->with($command)
->willReturn('foobar');
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
@@ -1238,7 +1238,7 @@ class RedisClusterTest extends PredisTestCase
'foobar'
);
/** @var Connection\FactoryInterface|MockObject */
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
@@ -1407,7 +1407,7 @@ class RedisClusterTest extends PredisTestCase
->method('create');
// TODO: I'm not sure about mocking a protected method, but it'll do for now
/** @var Connection\Cluster\RedisCluster|MockObject */
/** @var RedisCluster|MockObject */
$cluster = $this->getMockBuilder('Predis\Connection\Cluster\RedisCluster')
->onlyMethods(['getRandomConnection'])
->setConstructorArgs([$factory, new Parameters()])
@@ -1518,4 +1518,48 @@ class RedisClusterTest extends PredisTestCase
$this->assertInstanceOf(Connection\RelayConnection::class, $cluster->getConnectionBySlot(9999));
}
/**
* @group disconnected
*/
public function testWrite(): void
{
$command1 = new Command\Redis\GET();
$command1->setArguments(['arg1']);
$command2 = new Command\Redis\GET();
$command2->setArguments(['arg2']);
$command3 = new Command\Redis\GET();
$command3->setArguments(['arg3']);
$factory = $this->getMockBuilder(FactoryInterface::class)->getMock();
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
$connection1
->expects($this->once())
->method('write')
->with($command3->serializeCommand());
$connection2
->expects($this->once())
->method('write')
->with($command2->serializeCommand());
$connection3
->expects($this->once())
->method('write')
->with($command1->serializeCommand());
$cluster = new RedisCluster($factory, new Parameters());
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
}
}
@@ -1451,6 +1451,44 @@ repl_backlog_histlen:12978
$this->assertSame($connection->getParameters(), $replication->getParameters());
}
/**
* @group disconnected
*/
public function testWrite(): void
{
$command1 = new Command\Redis\Json\JSONGET();
$command1->setArguments(['arg1']);
$command2 = new Command\Redis\Json\JSONGET();
$command2->setArguments(['arg2']);
$command3 = new Command\Redis\Json\JSONGET();
$command3->setArguments(['arg3']);
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
$slave1 = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
$slave1
->expects($this->never())
->method('write');
$master
->expects($this->exactly(3))
->method('write')
->withConsecutive(
[$command1->serializeCommand()],
[$command2->serializeCommand()],
[$command3->serializeCommand()]
);
$replication = new MasterSlaveReplication();
$replication->add($master);
$replication->add($slave1);
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
}
public function connectionsProvider(): array
{
return [
@@ -1545,6 +1545,52 @@ class SentinelReplicationTest extends PredisTestCase
$this->assertSame($sentinel->getParameters(), $replication->getParameters());
}
/**
* @group disconnected
*/
public function testWrite(): void
{
$command1 = new Command\Redis\Search\FTSEARCH();
$command1->setArguments(['arg1', '*']);
$command2 = new Command\Redis\Search\FTSEARCH();
$command2->setArguments(['arg2', '*']);
$command3 = new Command\Redis\Search\FTSEARCH();
$command3->setArguments(['arg3', '*']);
$sentinel = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel');
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
$slave = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
$strategy = new Replication\ReplicationStrategy();
$factory = new Connection\Factory();
$master
->expects($this->exactly(3))
->method('isConnected')
->willReturn(true);
$slave
->expects($this->never())
->method('write');
$master
->expects($this->exactly(3))
->method('write')
->withConsecutive(
[$command1->serializeCommand()],
[$command2->serializeCommand()],
[$command3->serializeCommand()]
);
$replication = new SentinelReplication('svc', [$sentinel], $factory, $strategy);
$replication->add($master);
$replication->add($slave);
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
}
public function connectionsProvider(): array
{
return [
+21 -34
View File
@@ -13,6 +13,7 @@
namespace Predis\Pipeline;
use Predis\Client;
use Predis\Command\Redis\PING;
use Predis\Connection\Parameters;
use Predis\Response;
use PredisTestCase;
@@ -26,6 +27,7 @@ class AtomicTest extends PredisTestCase
{
$pong = new Response\Status('PONG');
$queued = new Response\Status('QUEUED');
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
$connection
@@ -40,13 +42,9 @@ class AtomicTest extends PredisTestCase
[$pong, $pong, $pong]
);
$connection
->expects($this->exactly(3))
->method('writeRequest')
->withConsecutive(
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')]
);
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(3))
->method('readResponse')
@@ -75,6 +73,7 @@ class AtomicTest extends PredisTestCase
*/
public function testThrowsExceptionOnAbortedTransaction(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$this->expectException('Predis\ClientException');
$this->expectExceptionMessage('The underlying transaction has been aborted by the server');
@@ -93,13 +92,9 @@ class AtomicTest extends PredisTestCase
null
);
$connection
->expects($this->exactly(3))
->method('writeRequest')
->withConsecutive(
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')]
);
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(3))
->method('readResponse')
@@ -123,6 +118,7 @@ class AtomicTest extends PredisTestCase
*/
public function testPipelineWithErrorInTransaction(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$this->expectException('Predis\Response\ServerException');
$this->expectExceptionMessage('ERR Test error');
@@ -142,13 +138,9 @@ class AtomicTest extends PredisTestCase
new Response\Status('OK')
);
$connection
->expects($this->exactly(3))
->method('writeRequest')
->withConsecutive(
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')]
);
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(3))
->method('readResponse')
@@ -172,6 +164,7 @@ class AtomicTest extends PredisTestCase
*/
public function testThrowsServerExceptionOnResponseErrorByDefault(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand();
$this->expectException('Predis\Response\ServerException');
$this->expectExceptionMessage('ERR Test error');
@@ -188,12 +181,9 @@ class AtomicTest extends PredisTestCase
new Response\Status('OK')
);
$connection
->expects($this->exactly(2))
->method('writeRequest')
->withConsecutive(
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')]
);
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->once())
->method('readResponse')
@@ -214,6 +204,7 @@ class AtomicTest extends PredisTestCase
*/
public function testReturnsResponseErrorWithClientExceptionsSetToFalse(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$pong = new Response\Status('PONG');
$queued = new Response\Status('QUEUED');
$error = new Response\Error('ERR Test error');
@@ -231,13 +222,9 @@ class AtomicTest extends PredisTestCase
[$pong, $pong, $error]
);
$connection
->expects($this->exactly(3))
->method('writeRequest')
->withConsecutive(
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')],
[$this->isRedisCommand('PING')]
);
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(3))
->method('readResponse')
+28 -4
View File
@@ -13,6 +13,7 @@
namespace Predis\Pipeline;
use Predis\Client;
use Predis\Command\Redis\PING;
use PredisTestCase;
class FireAndForgetTest extends PredisTestCase
@@ -22,10 +23,12 @@ class FireAndForgetTest extends PredisTestCase
*/
public function testPipelineWithSingleConnection(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
$connection
->expects($this->exactly(3))
->method('writeRequest');
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->never())
->method('readResponse');
@@ -44,14 +47,16 @@ class FireAndForgetTest extends PredisTestCase
*/
public function testSwitchesToMasterWithReplicationConnection(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$connection = $this->getMockBuilder('Predis\Connection\Replication\ReplicationInterface')
->getMock();
$connection
->expects($this->once())
->method('switchToMaster');
$connection
->expects($this->exactly(3))
->method('writeRequest');
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->never())
->method('readResponse');
@@ -64,4 +69,23 @@ class FireAndForgetTest extends PredisTestCase
$this->assertEmpty($pipeline->execute());
}
/**
* @group connected
* @group cluster
* @requiresRedisVersion >= 6.2.0
*/
public function testClusterExecutePipeline(): void
{
$pipeline = new FireAndForget($this->createClient());
$pipeline->set('foo', 'bar');
$pipeline->get('foo');
$pipeline->set('bar', 'foo');
$pipeline->get('bar');
$pipeline->set('baz', 'baz');
$pipeline->get('baz');
$this->assertEmpty($pipeline->execute());
}
}
+88 -8
View File
@@ -18,6 +18,8 @@ use Predis\Client;
use Predis\ClientException;
use Predis\ClientInterface;
use Predis\Command\CommandInterface;
use Predis\Command\Redis\ECHO_;
use Predis\Command\Redis\PING;
use Predis\Connection\Parameters;
use Predis\Response;
use PredisTestCase;
@@ -215,10 +217,21 @@ class PipelineTest extends PredisTestCase
*/
public function testExecuteWithFilledBuffer(): void
{
$command1 = new ECHO_();
$command1->setArguments(['one']);
$command2 = new ECHO_();
$command2->setArguments(['two']);
$command3 = new ECHO_();
$command3->setArguments(['three']);
$buffer = $command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand();
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
$connection
->expects($this->exactly(3))
->method('writeRequest');
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(3))
->method('readResponse')
@@ -261,10 +274,27 @@ class PipelineTest extends PredisTestCase
*/
public function testFlushHandlesPartialBuffers(): void
{
$command1 = new ECHO_();
$command1->setArguments(['one']);
$command2 = new ECHO_();
$command2->setArguments(['two']);
$buffer1 = $command1->serializeCommand() . $command2->serializeCommand();
$command3 = new ECHO_();
$command3->setArguments(['three']);
$command4 = new ECHO_();
$command4->setArguments(['four']);
$buffer2 = $command3->serializeCommand() . $command4->serializeCommand();
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
$connection
->expects($this->exactly(4))
->method('writeRequest');
->expects($this->exactly(2))
->method('write')
->withConsecutive([$buffer1], [$buffer2]);
$connection
->expects($this->exactly(4))
->method('readResponse')
@@ -291,6 +321,7 @@ class PipelineTest extends PredisTestCase
*/
public function testSwitchesToMasterWithReplicationConnection(): void
{
$buffer = (new PING())->serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand();
$pong = new Response\Status('PONG');
$connection = $this->getMockBuilder('Predis\Connection\Replication\ReplicationInterface')->getMock();
@@ -298,8 +329,9 @@ class PipelineTest extends PredisTestCase
->expects($this->once())
->method('switchToMaster');
$connection
->expects($this->exactly(3))
->method('writeRequest');
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(3))
->method('readResponse')
@@ -366,10 +398,28 @@ class PipelineTest extends PredisTestCase
*/
public function testExecuteWithCallableArgumentRunsPipelineInCallable(): void
{
$command1 = new ECHO_();
$command1->setArguments(['one']);
$command2 = new ECHO_();
$command2->setArguments(['two']);
$command3 = new ECHO_();
$command3->setArguments(['three']);
$command4 = new ECHO_();
$command4->setArguments(['four']);
$buffer = $command1->serializeCommand()
. $command2->serializeCommand()
. $command3->serializeCommand()
. $command4->serializeCommand();
$connection = $this->getMockBuilder('Predis\Connection\NodeConnectionInterface')->getMock();
$connection
->expects($this->exactly(4))
->method('writeRequest');
->expects($this->once())
->method('write')
->with($buffer);
$connection
->expects($this->exactly(4))
->method('readResponse')
@@ -546,6 +596,36 @@ class PipelineTest extends PredisTestCase
$this->assertSame('bar', $results[2]);
}
/**
* @group connected
* @group cluster
* @requiresRedisVersion >= 6.2.0
*/
public function testClusterExecutePipeline(): void
{
$client = $this->getClient();
$results = $client->pipeline(function (Pipeline $pipe) {
$pipe->set('foo', 'bar');
$pipe->set('bar', 'foo');
$pipe->set('baz', 'baz');
$pipe->get('foo');
$pipe->get('bar');
$pipe->get('baz');
});
$expectedResults = [
new Response\Status('OK'),
new Response\Status('OK'),
new Response\Status('OK'),
'bar',
'foo',
'baz',
];
$this->assertSameValues($expectedResults, $results);
}
// ******************************************************************** //
// ---- HELPER METHODS ------------------------------------------------ //
// ******************************************************************** //