Handle -READONLY as a redirection signal for Redis Cluster (AWS ElastiCache support) (#1656)

* feature: Added support for READONLY responses from AWS Elastic Cache

* chore: improve -READONLY handler comment and add changelog entry (#1656)

* tests: add unit test demonstrating ServerException behavior with READONLY error
This commit is contained in:
Kevin Rojas
2026-03-23 04:00:26 -03:00
committed by GitHub
parent 1c4219cf88
commit bb37322a24
3 changed files with 67 additions and 0 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unreleased
### Added
- Handle `-READONLY` responses in Redis Cluster for AWS ElastiCache Redis OSS failover events
### Changed
- Include command name in unsupported container command error messages (#1653)
### Fixed
+27
View File
@@ -465,6 +465,9 @@ class RedisCluster extends AbstractAggregateConnection implements ClusterInterfa
$details = explode(' ', $error->getMessage(), 2);
switch ($details[0]) {
case 'READONLY':
return $this->onReadOnlyResponse($command);
case 'MOVED':
return $this->onMovedResponse($command, $details[1]);
@@ -476,6 +479,30 @@ class RedisCluster extends AbstractAggregateConnection implements ClusterInterfa
}
}
/**
* Handles -READONLY responses by disconnecting the current node's connection
* and refreshing the slots map (when cluster slots are enabled), then
* re-executing the command so it is routed to the updated primary node.
*
* This is a workaround for AWS ElastiCache Redis OSS, which may return
* -READONLY errors during failover events. Standard Redis clusters do not
* exhibit this behavior.
*
* @param CommandInterface $command Command that generated the -READONLY response.
*
* @return mixed
*/
protected function onReadOnlyResponse(CommandInterface $command)
{
if ($this->useClusterSlots) {
$connection = $this->getConnectionByCommand($command);
$connection->disconnect();
$this->askSlotMap();
}
return $this->executeCommand($command);
}
/**
* Handles -MOVED responses by executing again the command against the node
* indicated by the Redis response.
@@ -738,6 +738,45 @@ class RedisClusterTest extends PredisTestCase
));
}
/**
* @group disconnected
*/
public function testRetriesExecutingCommandOnReadonlyServerExceptionDoesNotUpdateSlotMap(): void
{
$serverException = new Response\ServerException('READONLY You can\'t write against a read only replica.');
$command = $this->getCommandFactory()->create('get', ['node:1001']);
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6381?slots=0-5460');
$connection1
->expects($this->exactly(2))
->method('executeCommand')
->with($this->isRedisCommand($command))
->willThrowException($serverException);
$connection1
->expects($this->never())
->method('disconnect');
/** @var FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->never())
->method('create');
$cluster = new RedisCluster($factory, new Parameters());
$cluster->useClusterSlots(true);
$cluster->setRetryInterval(0);
$cluster->setRetryLimit(1);
$cluster->add($connection1);
$cluster->getSlotMap()->setSlots(0, 5460, '127.0.0.1:6381');
$this->expectException('Predis\Response\ServerException');
$this->expectExceptionMessage('READONLY You can\'t write against a read only replica.');
$cluster->executeCommand($command);
}
/**
* @group disconnected
* @group slow