commit 7c87a97e6768d291cb73c97aa82d6c0d47e7a09e Author: upstream Date: Thu Oct 8 01:25:58 2026 +0200 Upstream predis/predis state of the touched files before 33fb123441 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab362c8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,460 @@ +## Changelog + +## Unreleased +### Added +- Added optional `CELL_SIZE` argument for `CMS.INITBY(DIM|PROB)` (#1729) +- Added `BLESS *` command family (#1733) +- Added testing with a new HNSW compression (#1737) + +### Fixed +- Fixed client iteration with master/slave and Sentinel replication (#923) +- Fixed `*StreamConnection` looping forever when closed in the middle of a bulk reply (#1731) +- Fixed cluster CAS transactions returning transaction objects for reads and sending `MULTI` twice +- Fixed `MultiExec::multi()` being ignored as the first call of a CAS transaction +- Fixed cleanup of empty transactions and transaction callbacks that throw PHP errors +- Fixed `spl_object_hash()` deprecation on PHP 8.6 in `ConnectionErrorProof` pipeline + +### Security +- Fixed aborted cluster transactions leaving node connections in `MULTI` + +## v3.6.1 (2026-09-17) +### Fixed +- Fixed RESP3 double parsing returning positive `INF` for `-inf` payloads (#1716) +- Fixed `client_info` connection parameter being ignored (#1722) +- Fixed `Stream::write()` and `read()` leaving a dead connection when a host error handler throws exception (#1725) + +### Security +- Deprecated `CommandInterface::deserializeCommand()` (CVE GHSA-w6f5-v2h6-g786) +- Fixed CRLF command smuggling and node misrouting in `AbstractAggregateConnection::write()` and `CommandInterface::deserializeCommand()` (CVE GHSA-w6f5-v2h6-g786) + +## v3.6.0 (2026-08-14) +### Added +- Added support for new TS commands + Indonesian language support integration test (#1695) +- Added support for new COLLECT reducer for FT.AGGREGATE (#1699) +- Added support for `SDIFFCARD` and `SUNIONCARD` command +- Added support for `MAXCOUNT` and `MAXSIZE` arguments for `XREAD` and `XREADGROUP` +- Added support for `TS.READ` command +- Added support for `EXCLUDEEMPTY` argument for `TS.MRANGE` and `TS.MREVRANGE` commands +- Added explicit testing for FT.SEARCH timeout policies +- Added support for `TS.QUERYLABELS` command +- Added support for `LMOVEM` and `BLMOVEM` commands +- Added support for `FT.ALIASLIST` command +- Added stream commands to ClusterStrategy +- Added vector sets commands to ClusterStrategy +- Added experimental support for `HIMPORT` bulk hash import feature (API may change in a future release) + +### Changed +- Added `OBJECT` and hash field expiration commands to `ClusterStrategy` +- Make `ZMSCORE` command prefixable and add to `ClusterStrategy` (#1692) + +### Fixed +- Fixed Sentinel does not wipe servers on exception caused (#1694) +- Fixed `@method cmsincrby()` annotation + +## v3.5.1 (2026-06-11) +### Added +- Expose `pipeline()` API via `ClientInterface` (#1686) + +### Fixed +- Allow `UNLINK` to accept an array of keys (#1687) +- Fixed `Client::(un)pack()` return types + +## v3.5.0 (2026-06-02) +### Added +- Added support for `XNACK` command (#1666) +- Added support for `INCREX` command (#1674) +- Added support for `UNLINK` command (#1680) +- Added support for `AR*` array commands (#1672) +- Handle Redis Cluster `-READONLY` responses failover events (#1656) +- Added `FPHA` argument for `JSON.SET` command (#1661) +- Added new `COUNT` aggregator for Sorted Set commands (#1668) +- Added support for multiple aggregators for `TS.range` commands (#1670) + +### Changed +- Include command name in unsupported container command error messages (#1653) + +### Fixed +- Fixed handling of gap slots in `SlotMap::offsetUnset()` (#1660) +- Fixed `ZRANGE` to include 6.2 arguments (#1662) +- Fixed Sentinel retry to narrow retryable exceptions to `CommunicationException` (#1665) +- Fixed `SENTINEL SLAVES` RESP3 incompatible response (#1676) + +## v3.4.2 (2026-03-09) +### Changed +- Switch to `static` closures + +### Fixed +- Fixed Sentinel `getParameters()` executed on string configuration (#1649) +- Fixed Sentinel discovery methods not catching `StreamInitException` on connection failure (#1650) + +## v3.4.1 (2026-02-23) +### Added +- Added `upstream_driver` option for CLIENT SETINFO upstream reporting (#1645) +- Made `H(P)TTL` commands prefixable (#1639) +- Made `(B)LMPOP` commands prefixable (#1643) + +### Fixed +- Fixed Sentinel `getParameter()` call on array error (#2423) +- Removed deprecated `static` from callables (#1642) + +## v3.4.0 (2026-02-11) +### Added +- Added optional retry support (#1616) +- Added support for `VRANGE` command (#1623) +- Added support for idempotent stream API (#1632) +- Added support for `HOTKEYS` container command (#1630) + +### Fixed +- Fixed `[L|R]PUSHX` variadic arguments normalization (#1633) +- Fixed wrong `@param` annotation in `Parameters` (#1614) +- Made `ZRANDMEMBER` prefixable (#1621) +- Improve connection handshake by pipelining commands (#1622) + +## v3.3.0 (2025-11-24) +### Added +- Added cluster support for `XADD`, `XDEL` and `XRANGE` (#1587) +- Added prefixable interface for `HEXPIRE` and `HEXPIRETIME` (#1592) +- Added new experimental CAS/CAD functionality (#1609) +- Added temporary `XREADGROUP_CLAIM` command (#1608) +- Added support for `MSET` command (#1610) +- Added experimental support for `FT.HYBRID` (#1607) + +### Changed +- Improved compatiblity with Relay (#1597) +- Refactor pipeline data writing depends on connection type (#1586) + +## v3.2.0 (2025-08-05) +### Added +- Added support for `XDELEX` and `XACKDEL` (#1580) +- Added missing VSIM argument (#1582) + +### Changed +- Extended `XTRIM` and `XADD` commands with new parameters (#1580) + +### Maintenance +- Updated Redis 8.2 test image (#1583) +- Added test coverage for updated Vamana (#1584) + +## v3.1.0 (2025-07-22) +### Added +- Add experimental support for vector sets commands (#1550) +- Added support for `XACK` command (#1555) +- Added support for `XCLAIM` command (#1557) +- Added support for `XPENDING` command (#1558) +- Added support for `XSETID` command (#1559) +- Added validation and support for the new `BITOP` command operations (#1566) + +### Changed +- Handle and retry `LOADING` errors from Sentinel replicas (#1536) +- Retry all exceptions from Sentinel replicas (#1577) + +### Fixed +- Fixed PHP 8.4 deprecated call to `stream_context_set_option()` (#1545) +- Fixed return type for `ZCOUNT` to be `int` (#1547) +- Fixed throwing `CommunicationException` when stream is EOF (#1548) +- Removed automatic `conn_uid` parameter assignment (#1552) +- Fixed wrong command API call on prefix processing (#1554) +- Fixed `XREAD` response parsing while read null (#1563) +- Fixed `XINFO` command responses parsing (#1560) +- Marked missing commands as Prefixable (#1576) + +## v3.0.1 (2025-05-16) +### Fixed +- Send `default` user if only password is used for authentication (#1535) + +## v3.0.0 (2025-05-02) +### Added +- ⚠️ Added support for transactions for clustered connections (#1497) +- Added support for RESP3 communication protocol (#1047) +- Added support for RESP3 push notifications (#1316) +- Added support for Sharded Pub/Sub (#1303) +- Added support for `XGROUP` commands (#1324) +- Added support for `XREADGROUP` command (#1327) +- Added support for `XAUTOCLAIM` command (#1328) +- Added support for `XINFO` commands (#1331) +- Added support for Redis 7.4 `XREAD` and `HSCAN` interface changes (#1452) + +### Changed +- ⚠️ Refactored Relay integration (#1423) +- ⚠️ Changed interface of `CLIENT` command (#1337) +- ⚠️ Changed interfaces for commands to support Redis 6.2 — 7.0 arguments (#1330) +- ⚠️ Changed default search `DIALECT` to `2` (#1516) +- ⚠️ Implemented PSR-7 compatible stream abstraction (#1450) +- ⚠️ Improved pipeline abstractions (#1438) +- ⚠️ Improved connection handshake (#1431) + +### Fixed +- Fixed single persistent connection using the same resource (#1512) + +### Maintenance +- Revisit incompatible tests against Relay (#1537) + +## v2.4.0 (2025-04-30) +### Added +- Added new hash-field expiration commands (#1520) +- Added missing `FT._LIST` and `BITFIELD_RO` commands (#1521) + +### Changed +- Update `WATCH` command to accept `string|string[]` (#1476) +- Optimize cluster slotmap with compact slot range object (#1493) + +### Fixed +- Fixed `EVAL_RO` cluster support (#1449) +- Fixed PHP 8.4 compatibility with `stream_context_set_option()` (#1503) +- Prevent named arguments runtime failure (#1509) +- Mark `GEOSEARCH` as read-only to ensure execution on replica (#1481) +- Fixed protocol loss during redis cluster `MOVED` / `ASK` (#1530) + +### Maintenance +- Added CI testing with Redis 8.0 (#1510) +- Added test coverage for compatibility with Redis 8.0 (#1513) +- Use parallel on PHP-CS-Fixer (#1489) + +## v2.3.0 (2024-11-21) +### Added +- Added `GeoShapeField` field (#1467) +- Added hash expiration commands (#1456) +- Added support for time series `IGNORE` filter (#1458) +- Added `XREAD` commands (#1459) +- Added `NOVALUES` argument support to `HSCAN` (#1459) +- Added support for search `INDEXMISSING` and `INDEXEMPTY` arguments (#1464) + +### Changed +- Explicitly mark nullable parameters as nullable (#1448) +- Filter out available replicas based on link status flag (#1440) +- Respect `prefix` for `ZPOPMIN`, `ZPOPMAX`, `ZMSCORE`, `LMOVE`, `BLMOVE`, `SMISMEMBER` and `GEOSEARCH` (#1451, #1453, #1455, #1468) +- Updated test infrastructure (#1510) +- +## v2.2.0 (2023-06-14) + +Predis v2.2.0 introduces official support for [Redis Stack](https://redis.io/docs/stack/) as well as a [Relay](https://github.com/cachewerk/relay) integration for substantially [faster read performance](https://github.com/predis/predis/wiki/Using-Relay). + +### Added +- Added support for [Relay](https://github.com/predis/predis/wiki/Using-Relay) (#1263) +- Added support for `FCALL_RO` command (#1191) +- Added support for Redis `JSON`, `Bloom`, `Search` and `TimeSeries` module (#1253) +- Added support for `ACL SETUSER, GETUSER, DRYRUN` commands (#1193) + +### Changed +- Minor code style and type-hint changes (#1311) + +### Fixed +- Fixed prefixes for `XTRIM` and `XREVRANGE` commands (#1230) +- Fixed `fclose()` being called on invalid stream resource (#1199) +- Fixed `BitByte` and `ExpireOptions` traits skip processing on null values (#1169) +- Fixed missing `@return` annotations (#1265) +- Fixed `GETDEL` prefixing (#1306) + +## v2.1.2 (2023-03-02) + +### Added +- Added stream commands to `KeyPrefixProcessor` (#1051) +- Added `ReplicationStrategy::$loadBalancing` option to disable replica reads (#1168) +- Added support for `FCALL` and `FUNCTIONS` commands (#1049) +- Added support for `PEXPIRETIME` command (#1031) +- Added support for `EXPIRETIME` command (#1029) +- Added support for `EVAL_RO` command (#1032) +- Added support for `LCS` command (#1035) +- Added support for `SORT_RO` command (#1044) +- Added support for `SINTERCARD` command (#1027) +- Added support for `EVALSHA_RO` command (#1034) +- Added support for new arguments for `BITPOS` and `BITCOUNT` commands (#1045) +- Added support for new arguments for `EXPIRE` and `EXPIREAT` commands (#1046) + +### Bug Fixes +- Fixed deprecated function call syntax + +### Deprecated +- Further deprecated phpiredis and webdis integration (#1179) + +### Maintenance +- Applied coding standards +- Pass PHPStan level 2 + +## v2.1.1 (2023-01-17) + +### Bug Fixes +- Fix `@template` in `Predis\Client` (#1017) +- Fix support options array in `ZINTERSTORE` and `ZUNIONSTORE` (#1018) + +### Deprecated +- Deprecated phpiredis and webdis connections + +## v2.1.0 (2023-01-16) + +### New Features +- Implemented `GETEX` command (#872) +- Implemented `GETDEL` command (#869) +- Implemented `COPY` command (#866) +- Implemented `FAILOVER` command (#875) +- Implemented `LMOVE` command (#863) +- Implemented `LMPOP` command (#1013) +- Implemented `HRANDFIELD` command (#870) +- Implemented `SMISMEMBER` command (#871) +- Implemented `ZMPOP` command (#831) +- Implemented `BLMOVE` command (#865) +- Implemented `BLMPOP` command (#1015) +- Implemented `BZMPOP` command (#833) +- Implemented `BZPOPMIN` command (#862) +- Implemented `BZPOPMAX` command (#864) +- Implemented `ZUNION` command (#860) +- Implemented `ZINTER` command (#859) +- Implemented `ZINTERCARD` command (#861) +- Implemented `ZRANGESTORE` command (#829) +- Implemented `ZDIFFSTORE` command (#828) +- Implemented `ZDIFF` command (#826) +- Implemented `ZRANDMEMBER` command (#825) +- Implemented `ZMSCORE` (#823) +- Implemented `GEOSEARCH` command (#867) +- Implemented `GEOSEARCHSTORE` command (#873) + +### Bug Fixes +- Added annotations to suppress PHP 8.1 return type deprecation warning (#810) + +### Maintenance +- Added mixin annotations for traits (#835) + +## v2.0.3 (2022-10-11) + +- Improved PHP 8.2 support +- Call `is_resource()` before reading/writing +- Added partial support for Redis Stream commands +- Fixed Sentinel authentication issue + +## v2.0.2 (2022-09-06) + +- Fixed PHP 8.2 deprecation notice: Use of "static" in callables + +## v2.0.1 (2022-09-04) + +- Added retry interval to `RedisCluster` with a default of `10ms` +- Avoid PHP 8.2 warning in `Connection\Parameters` +- Fixed Sentinel reconnect issue in long-running PHP processes + +## v2.0.0 (2022-06-08) + +- Dropped support for PHP 7.1 and older + +- Accepted values for some client options have changed, this is the new list of + accepted values: + + - `aggregate`: callable returning an aggregate connection. + - `cluster`: string value (`predis`, `redis`), callable returning an aggregate + connection. + - `replication`: string value (`predis`, `sentinel`), callable returning an + aggregate connection. + - `commands`: command factory, named array mapping command IDs to PHP classes, + callable returning a command factory or a named array. + - `connections`: connection factory, callable object returning a connection + factory, named array mapping URI schemes to PHP classes, string identifying + a supported combination of configurations for the connection factory. + - `prefix`: string value, command processor, callable. + - `exceptions`: boolean. + + Note that both the `cluster` and `replication` options now return a closure + acting as initializer instead of an aggregate connection instance. + +- The `connections` client option now accepts certain string values identifying + certain combinations of configurations for the connection factory. Currently + this is used to provide a short way to configure Predis to load our phpiredis + based connection backends simply, accepted values are: + + - `phpiredis-stream` maps `Phpiredis\Connection\PhpiredisStreamConnection` to + `tcp`, `redis`, `unix` URI schemes. + - `phpiredis-socket` maps `Phpiredis\Connection\PhpiredisSocketConnection` to + `tcp`, `redis`, `unix` URI schemes. + - `phpiredis-stream` is simply an alias of `phpiredis-stream`. + +- Added the new `Predis\Cluster\Hash\PhpiredisCRC16` class using ext-phpiredis + to speed-up the generation of the CRC16 hash of keys for redis-cluster. Predis + automatically uses this class when ext-phpiredis is loaded, but it is possible + to configure the hash generator using the new `crc16` client option (accepted + values `predis`, `phpiredis` or an hash generator instance). + +- Replication backends now use the `role` parameter instead of `alias` in order + to distinguish the role of a connection. Accepted values are `master`, `slave` + and, for redis-sentinel, `sentinel`. This led to a redesign of how connections + can be retrieved from replication backends: the method getConnectionById() now + retrieves a connection only by its ID (ip:port pair), to get a connection by + its alias there is the new method getConnectionByAlias(). This method is not + supported by the redis-sentinel backend due to its dynamic nature (connections + are retrieved and initialized at runtime from sentinels) but it is possible to + get a single connection from the pool by using its ID. It is also possible to + retrieve a connection by role using the method getConnectionByRole(). + +- The concept of connection ID (ip:port pair) and connection alias (the `alias` + parameter) in `Predis\Connection\Cluster\PredisCluster` has been separated. + This change does not affect distribution and it is safe for existing clusters. + +- Client option classes now live in the `Predis\Configuration\Option` namespace. + +- Classes for Redis commands have been moved into the new `Predis\Command\Redis` + namespace and each class name mirrors the respective Redis command ID. + +- The concept of server profiles is gone, the library now uses a single command + factory to create instances of commands classes. The `profile` option has been + replaced by the `commands` option accepting `Predis\Command\FactoryInterface` + to customize the underlying command factory. The default command factory class + used by Predis is `Predis\Command\RedisFactory` and it still allows developers + to define or override commands with their own implementations. In addition to + that, `Predis\Command\RedisFactory` relies on a convention-over-configuration + approach by looking for a suitable class with the same name as the command ID + in the `Predis\Command\Redis` when the internal class map does not contain a + class associated. + +- The method `Predis\Client::getClientFor($connectionID)` has been replaced by + `getClientBy($selector, $value, $callable = null)` which is more flexible as + it is not limited to picking a connection from the underlying replication or + cluster backend by ID, but allows users to specify a `$selector` that can be + either `id` (the old behavior), `key`, `slot` or `command`. The client uses + duck-typing instead of type-checking to verify that the underlying connection + implements a method that matches the specified selector which means that some + selectors may not be available to all kinds of connection backends. + +- The method `Predis\Client::getConnectionById($connectionID)` has been removed. + +- Changed the signature for the constructor of `Predis\Command\RawCommand`. + +- The `Predis\Connection\Aggregate` namespace has been split into two separate + namespaces for cluster backends (`Predis\Connection\Cluster`) and replication + backends (`Predis\Connection\Replication`). + +- The method `Predis\Connection\AggregateConnectionInterface::getConnection()` + has been renamed to `getConnectionByCommand()`. + +- The methods `switchToMaster()` and `switchToSlave()` have been promoted to be + part of `Predis\Connection\Replication\ReplicationInterface` while the method + `switchTo($connection)` has been removed from it. + +- The method `Predis\Connection\Cluster\PredisCluster::executeCommandOnNodes()` + has been removed as it is possible to achieve the same by iterating over the + connection or, even better, over the client instance in order to execute the + same command against all of the registered connections. + +- The class `Predis\CommunicationException` now uses the correct default types + for the `$message` (string) and `$code` (integer) parameters. + +- The method `onConnectionError()` in `Predis\Connection\AbstractConnection` + class now passes the second argument as an integer value `0` as its default + value instead of `null`. + +- Support Pub/Sub and Pipelines when using replication + +- The class `Predis\Transaction\AbortedMultiExecException` now uses the correct + default types for the `$code` (integer) parameter. + +- __FIX__: using `strval` in `getScanOptions()` method, part of + `Predis\Collection\Iterator\CursorBasedIterator` to make sure we retrieve the + string value of `$this->match` and not passing `null` to `strlen()` function. + +- __FIX__: the value returned from `getArgument()` in `isReadOperation()` method, + part of `Predis\Replication\ReplicationStrategy` class, is checked to not pass + `null` to `sha1` function. + +- __FIX__: the value returned from `getArgument()` in `parseResponse()`method, + part of `Predis\Command\Redis\SENTINEL` class, is checked to not pass `null` + to `strtolower()` function. + +## v2.0.0-beta.1 (2022-05-26) + +Same as v2.0.0 diff --git a/src/Pipeline/ConnectionErrorProof.php b/src/Pipeline/ConnectionErrorProof.php new file mode 100644 index 0000000..3b873c4 --- /dev/null +++ b/src/Pipeline/ConnectionErrorProof.php @@ -0,0 +1,121 @@ +getClient()->getConnection(); + } + + /** + * {@inheritdoc} + */ + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) + { + if ($connection instanceof NodeConnectionInterface) { + return $this->executeSingleNode($connection, $commands); + } elseif ($connection instanceof ClusterInterface) { + return $this->executeCluster($connection, $commands); + } + $class = get_class($connection); + + throw new NotSupportedException("The connection class '$class' is not supported."); + } + + /** + * {@inheritdoc} + */ + protected function executeSingleNode(NodeConnectionInterface $connection, SplQueue $commands) + { + $responses = []; + $sizeOfPipe = count($commands); + $buffer = ''; + + foreach ($commands as $command) { + $buffer .= $command->serializeCommand(); + } + + try { + $connection->write($buffer); + } catch (CommunicationException $exception) { + return array_fill(0, $sizeOfPipe, $exception); + } + + for ($i = 0; $i < $sizeOfPipe; ++$i) { + $command = $commands->dequeue(); + + try { + $responses[$i] = $connection->readResponse($command); + } catch (CommunicationException $exception) { + $add = count($commands) - count($responses); + $responses = array_merge($responses, array_fill(0, $add, $exception)); + + break; + } + } + + return $responses; + } + + /** + * {@inheritdoc} + */ + protected function executeCluster(ClusterInterface $connection, SplQueue $commands) + { + $responses = []; + $sizeOfPipe = count($commands); + $exceptions = []; + + foreach ($commands as $command) { + $nodeConnection = $connection->getConnectionByCommand($command); + $nodeConnection->write($command->serializeCommand()); + } + + for ($i = 0; $i < $sizeOfPipe; ++$i) { + $command = $commands->dequeue(); + + $cmdConnection = $connection->getConnectionByCommand($command); + $connectionId = spl_object_id($cmdConnection); + + if (isset($exceptions[$connectionId])) { + $responses[$i] = $exceptions[$connectionId]; + continue; + } + + try { + $responses[$i] = $cmdConnection->readResponse($command); + } catch (CommunicationException $exception) { + $responses[$i] = $exception; + $exceptions[$connectionId] = $exception; + } + } + + return $responses; + } +} diff --git a/tests/Predis/Pipeline/ConnectionErrorProofTest.php b/tests/Predis/Pipeline/ConnectionErrorProofTest.php new file mode 100644 index 0000000..52bc4c1 --- /dev/null +++ b/tests/Predis/Pipeline/ConnectionErrorProofTest.php @@ -0,0 +1,160 @@ +serializeCommand() . (new PING())->serializeCommand() . (new PING())->serializeCommand(); + + $connection = $this->getMockConnection('tcp://127.0.0.1:7001'); + $exception = new ConnectionException($connection, 'Connection failed'); + + $connection + ->expects($this->once()) + ->method('write') + ->with($buffer) + ->willThrowException($exception); + $connection + ->expects($this->never()) + ->method('readResponse'); + + $pipeline = new ConnectionErrorProof(new Client($connection)); + + $pipeline->ping(); + $pipeline->ping(); + $pipeline->ping(); + + $this->assertSame([$exception, $exception, $exception], $pipeline->execute()); + } + + /** + * @group disconnected + */ + public function testReturnsExceptionOfEachFailedConnectionWithClusterConnection(): void + { + $connection1 = $this->getMockConnection('tcp://127.0.0.1:7001'); + $exception1 = new ConnectionException($connection1, 'Connection to node 1 failed'); + $connection1 + ->expects($this->exactly(2)) + ->method('write'); + $connection1 + ->expects($this->once()) + ->method('readResponse') + ->willThrowException($exception1); + + $connection2 = $this->getMockConnection('tcp://127.0.0.1:7002'); + $exception2 = new ConnectionException($connection2, 'Connection to node 2 failed'); + $connection2 + ->expects($this->exactly(2)) + ->method('write'); + $connection2 + ->expects($this->once()) + ->method('readResponse') + ->willThrowException($exception2); + + $cluster = $this->getMockClusterConnection(['foo' => $connection1, 'bar' => $connection2]); + + $pipeline = new ConnectionErrorProof(new Client($cluster)); + + $pipeline->get('foo'); + $pipeline->get('bar'); + $pipeline->get('foo'); + $pipeline->get('bar'); + + $this->assertSame([$exception1, $exception2, $exception1, $exception2], $pipeline->execute()); + } + + /** + * @group disconnected + */ + public function testReadsResponsesFromHealthyConnectionsWhenOneFailsWithClusterConnection(): void + { + $connection1 = $this->getMockConnection('tcp://127.0.0.1:7001'); + $exception = new ConnectionException($connection1, 'Connection to node 1 failed'); + $connection1 + ->expects($this->exactly(2)) + ->method('write'); + $connection1 + ->expects($this->once()) + ->method('readResponse') + ->willThrowException($exception); + + $connection2 = $this->getMockConnection('tcp://127.0.0.1:7002'); + $connection2 + ->expects($this->exactly(2)) + ->method('write'); + $connection2 + ->expects($this->exactly(2)) + ->method('readResponse') + ->willReturnOnConsecutiveCalls('value1', 'value2'); + + $cluster = $this->getMockClusterConnection(['foo' => $connection1, 'bar' => $connection2]); + + $pipeline = new ConnectionErrorProof(new Client($cluster)); + + $pipeline->get('foo'); + $pipeline->get('bar'); + $pipeline->get('foo'); + $pipeline->get('bar'); + + $this->assertSame([$exception, 'value1', $exception, 'value2'], $pipeline->execute()); + } + + /** + * @group disconnected + */ + public function testThrowsExceptionOnUnsupportedConnection(): void + { + $this->expectException(NotSupportedException::class); + $this->expectExceptionMessageMatches('/^The connection class \'.+\' is not supported\.$/'); + + $connection = $this->getMockBuilder('Predis\Connection\Replication\ReplicationInterface')->getMock(); + + $pipeline = new ConnectionErrorProof(new Client($connection)); + + $pipeline->ping(); + $pipeline->execute(); + } + + /** + * Returns a mocked cluster connection routing commands by their first key. + * + * @param array $connectionsByKey Node connections indexed by key + * + * @return \PHPUnit\Framework\MockObject\MockObject|\Predis\Connection\Cluster\ClusterInterface + */ + private function getMockClusterConnection(array $connectionsByKey) + { + $cluster = $this->getMockBuilder('Predis\Connection\Cluster\ClusterInterface')->getMock(); + $cluster + ->expects($this->atLeastOnce()) + ->method('getConnectionByCommand') + ->willReturnCallback(static function (CommandInterface $command) use ($connectionsByKey) { + return $connectionsByKey[$command->getArgument(0)]; + }); + + return $cluster; + } +}