diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e60fb4..ba4a6e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - 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 diff --git a/src/Connection/Replication/SentinelReplication.php b/src/Connection/Replication/SentinelReplication.php index daf5a168..4a5099f8 100644 --- a/src/Connection/Replication/SentinelReplication.php +++ b/src/Connection/Replication/SentinelReplication.php @@ -16,6 +16,7 @@ use InvalidArgumentException; use Predis\Command\Command; use Predis\Command\CommandInterface; use Predis\Command\RawCommand; +use Predis\Command\Redis\Utils\CommandUtility; use Predis\CommunicationException; use Predis\Connection\AbstractAggregateConnection; use Predis\Connection\ConnectionException; @@ -430,20 +431,24 @@ class SentinelReplication extends AbstractAggregateConnection implements Replica } foreach ($payload as $slave) { - $flags = explode(',', $slave[9]); + if ($slave !== [] && !is_string(key($slave))) { + $slave = CommandUtility::arrayToDictionary($slave, null, false); + } + + $flags = explode(',', $slave['flags']); if (array_intersect($flags, ['s_down', 'o_down', 'disconnected'])) { continue; } // ensure `master-link-status` is ok - if (isset($slave[31]) && $slave[31] === 'err') { + if (isset($slave['master-link-status']) && $slave['master-link-status'] === 'err') { continue; } $slaves[] = [ - 'host' => $slave[3], - 'port' => $slave[5], + 'host' => $slave['ip'], + 'port' => $slave['port'], 'role' => 'slave', ]; } diff --git a/tests/Predis/ClientTest.php b/tests/Predis/ClientTest.php index 625c2a44..859b92fc 100644 --- a/tests/Predis/ClientTest.php +++ b/tests/Predis/ClientTest.php @@ -1718,6 +1718,35 @@ class ClientTest extends PredisTestCase $this->assertTrue($master->isConnected()); } + /** + * @group connected + * @group sentinel + * @group relay-incompatible + * @requiresRedisVersion >= 7.0.0 + */ + public function testSentinelReplicationConnectionWithResp3(): void + { + $client = $this->createClient(['protocol' => 3]); + $connection = $client->getConnection(); + + $this->assertInstanceOf(SentinelReplication::class, $connection); + + // SENTINEL slaves returns a map under RESP3 - calling getSlaves() + // exercises the parsing path that previously read fields by + // positional index. We do not assert on cardinality because the + // CI topology may not include replicas; we only require that the + // call completes without protocol-incompatibility errors. + $slaves = $connection->getSlaves(); + $this->assertIsArray($slaves); + + foreach ($slaves as $slave) { + $this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $slave); + } + + $client->set('sentinel_resp3_test_key', 'sentinel_resp3_test_value'); + $this->assertEquals('sentinel_resp3_test_value', $client->get('sentinel_resp3_test_key')); + } + // ******************************************************************** // // ---- HELPER METHODS ------------------------------------------------ // // ******************************************************************** // diff --git a/tests/Predis/Connection/Replication/SentinelReplicationTest.php b/tests/Predis/Connection/Replication/SentinelReplicationTest.php index 3b08e362..9aed6358 100644 --- a/tests/Predis/Connection/Replication/SentinelReplicationTest.php +++ b/tests/Predis/Connection/Replication/SentinelReplicationTest.php @@ -593,6 +593,61 @@ class SentinelReplicationTest extends PredisTestCase $this->assertSame('127.0.0.1:6383', (string) $slaves[1]); } + /** + * @group disconnected + */ + public function testMethodGetSlavesParsesResp3MapResponse(): void + { + $sentinel1 = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel'); + $sentinel1 + ->expects($this->once()) + ->method('executeCommand') + ->withConsecutive( + [$this->isRedisCommand('SENTINEL', ['slaves', 'svc'])] + ) + ->willReturnOnConsecutiveCalls( + [ + [ + 'name' => '127.0.0.1:6382', + 'ip' => '127.0.0.1', + 'port' => '6382', + 'runid' => '112cdebd22924a7d962be496f3a1c4c7c9bad93f', + 'flags' => 'slave', + 'master-host' => '127.0.0.1', + 'master-port' => '6381', + 'master-link-status' => 'ok', + ], + [ + 'name' => '127.0.0.1:6383', + 'ip' => '127.0.0.1', + 'port' => '6383', + 'runid' => '1c0bf1291797fbc5608c07a17da394147dc62817', + 'flags' => 's_down,slave', + 'master-host' => '127.0.0.1', + 'master-port' => '6381', + 'master-link-status' => 'ok', + ], + [ + 'name' => '127.0.0.1:6384', + 'ip' => '127.0.0.1', + 'port' => '6384', + 'runid' => '8c0bf1291797fbc5608c07a17da394147dc62818', + 'flags' => 'slave', + 'master-host' => '127.0.0.1', + 'master-port' => '6381', + 'master-link-status' => 'err', + ], + ] + ); + + $replication = $this->getReplicationConnection('svc', [$sentinel1]); + + $slaves = $replication->getSlaves(); + + $this->assertCount(1, $slaves); + $this->assertSame('127.0.0.1:6382', (string) $slaves[0]); + } + /** * @group disconnected */