diff --git a/CHANGELOG.md b/CHANGELOG.md index a62980ee..9e4ead6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## Changelog ## Unreleased +### Changed +- Handle and retry `LOADING` errors from Sentinel replicas (#1536) + ### Fixed - Fixed PHP 8.4 deprecated call to `stream_context_set_option()` (#1545) - Fixed return type for `ZCOUNT` to be `int` (#1547) diff --git a/src/Connection/Replication/SentinelReplication.php b/src/Connection/Replication/SentinelReplication.php index 38867e22..d2ce92d2 100644 --- a/src/Connection/Replication/SentinelReplication.php +++ b/src/Connection/Replication/SentinelReplication.php @@ -715,6 +715,9 @@ class SentinelReplication extends AbstractAggregateConnection implements Replica while ($retries <= $this->retryLimit) { try { $response = $this->getConnectionByCommand($command)->$method($command); + if ($response instanceof Error && $response->getErrorType() === 'LOADING') { + throw new ConnectionException($this->current, $response->getMessage()); + } break; } catch (CommunicationException $exception) { $this->wipeServerList(); diff --git a/tests/Predis/Connection/Replication/SentinelReplicationTest.php b/tests/Predis/Connection/Replication/SentinelReplicationTest.php index da672a30..85908b7e 100644 --- a/tests/Predis/Connection/Replication/SentinelReplicationTest.php +++ b/tests/Predis/Connection/Replication/SentinelReplicationTest.php @@ -1599,6 +1599,91 @@ class SentinelReplicationTest extends PredisTestCase ]; } + /** + * @group disconnected + */ + public function testDiscardsSlaveWhenRespondsLOADINGAndExecutesReadOnlyCommandOnNextSlave(): void + { + $sentinel1 = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel'); + $sentinel1 + ->expects($this->any()) + ->method('executeCommand') + ->with($this->isRedisCommand( + 'SENTINEL', ['slaves', 'svc'] + )) + ->willReturn( + [ + [ + 'name', '127.0.0.1:6383', + 'ip', '127.0.0.1', + 'port', '6383', + 'runid', '1c0bf1291797fbc5608c07a17da394147dc62817', + 'flags', 'slave', + 'master-host', '127.0.0.1', + 'master-port', '6381', + ], + ] + ); + + $master = $this->getMockConnection('tcp://127.0.0.1:6381?role=master'); + $master + ->expects($this->any()) + ->method('isConnected') + ->willReturn(true); + + $slave1 = $this->getMockConnection('tcp://127.0.0.1:6382?role=slave'); + $slave1 + ->expects($this->any()) + ->method('isConnected') + ->willReturn(true); + $slave1 + ->expects($this->once()) + ->method('executeCommand') + ->with( + $this->isRedisCommand('GET', ['key']) + ) + ->willReturn( + new Response\Error('LOADING') + ); + + $slave2 = $this->getMockConnection('tcp://127.0.0.1:6383?role=slave'); + $slave2 + ->expects($this->any()) + ->method('isConnected') + ->willReturn(true); + $slave2 + ->expects($this->once()) + ->method('executeCommand') + ->withConsecutive( + [$this->isRedisCommand('GET', ['key'])] + ) + ->willReturnOnConsecutiveCalls( + 'value' + ); + + /** @var Connection\FactoryInterface|MockObject */ + $factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock(); + $factory + ->expects($this->once()) + ->method('create') + ->with([ + 'host' => '127.0.0.1', + 'port' => '6383', + 'role' => 'slave', + ]) + ->willReturn($slave2); + + $replication = $this->getReplicationConnection('svc', [$sentinel1], $factory); + + $replication->add($master); + $replication->add($slave1); + + $this->assertSame('value', $replication->executeCommand( + Command\RawCommand::create('get', 'key') + )); + $this->assertSame($slave2, $replication->getCurrent()); + } + // ******************************************************************** // // ---- HELPER METHODS ------------------------------------------------ // // ******************************************************************** //