diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e2e1c..ab362c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - 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` diff --git a/src/Pipeline/ConnectionErrorProof.php b/src/Pipeline/ConnectionErrorProof.php index 7059ac9..3b873c4 100644 --- a/src/Pipeline/ConnectionErrorProof.php +++ b/src/Pipeline/ConnectionErrorProof.php @@ -101,10 +101,10 @@ class ConnectionErrorProof extends Pipeline $command = $commands->dequeue(); $cmdConnection = $connection->getConnectionByCommand($command); - $connectionHash = spl_object_hash($cmdConnection); + $connectionId = spl_object_id($cmdConnection); - if (isset($exceptions[$connectionHash])) { - $responses[$i] = $exceptions[$connectionHash]; + if (isset($exceptions[$connectionId])) { + $responses[$i] = $exceptions[$connectionId]; continue; } @@ -112,7 +112,7 @@ class ConnectionErrorProof extends Pipeline $responses[$i] = $cmdConnection->readResponse($command); } catch (CommunicationException $exception) { $responses[$i] = $exception; - $exceptions[$connectionHash] = $exception; + $exceptions[$connectionId] = $exception; } } 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; + } +}