Predis to v2.2.2

This commit is contained in:
the-djmaze 2024-04-08 16:29:55 +02:00
parent 9510347d3a
commit 969dca5f7e
449 changed files with 22013 additions and 2 deletions

View file

@ -0,0 +1,553 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Connection\Replication;
use InvalidArgumentException;
use Predis\ClientException;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\Replication\MissingMasterException;
use Predis\Replication\ReplicationStrategy;
use Predis\Response\ErrorInterface as ResponseErrorInterface;
/**
* Aggregate connection handling replication of Redis nodes configured in a
* single master / multiple slaves setup.
*/
class MasterSlaveReplication implements ReplicationInterface
{
/**
* @var ReplicationStrategy
*/
protected $strategy;
/**
* @var NodeConnectionInterface
*/
protected $master;
/**
* @var NodeConnectionInterface[]
*/
protected $slaves = [];
/**
* @var NodeConnectionInterface[]
*/
protected $pool = [];
/**
* @var NodeConnectionInterface[]
*/
protected $aliases = [];
/**
* @var NodeConnectionInterface
*/
protected $current;
/**
* @var bool
*/
protected $autoDiscovery = false;
/**
* @var FactoryInterface
*/
protected $connectionFactory;
/**
* {@inheritdoc}
*/
public function __construct(ReplicationStrategy $strategy = null)
{
$this->strategy = $strategy ?: new ReplicationStrategy();
}
/**
* Configures the automatic discovery of the replication configuration on failure.
*
* @param bool $value Enable or disable auto discovery.
*/
public function setAutoDiscovery($value)
{
if (!$this->connectionFactory) {
throw new ClientException('Automatic discovery requires a connection factory');
}
$this->autoDiscovery = (bool) $value;
}
/**
* Sets the connection factory used to create the connections by the auto
* discovery procedure.
*
* @param FactoryInterface $connectionFactory Connection factory instance.
*/
public function setConnectionFactory(FactoryInterface $connectionFactory)
{
$this->connectionFactory = $connectionFactory;
}
/**
* Resets the connection state.
*/
protected function reset()
{
$this->current = null;
}
/**
* {@inheritdoc}
*/
public function add(NodeConnectionInterface $connection)
{
$parameters = $connection->getParameters();
if ('master' === $parameters->role) {
$this->master = $connection;
} else {
// everything else is considered a slvave.
$this->slaves[] = $connection;
}
if (isset($parameters->alias)) {
$this->aliases[$parameters->alias] = $connection;
}
$this->pool[(string) $connection] = $connection;
$this->reset();
}
/**
* {@inheritdoc}
*/
public function remove(NodeConnectionInterface $connection)
{
if ($connection === $this->master) {
$this->master = null;
} elseif (false !== $id = array_search($connection, $this->slaves, true)) {
unset($this->slaves[$id]);
} else {
return false;
}
unset($this->pool[(string) $connection]);
if ($this->aliases && $alias = $connection->getParameters()->alias) {
unset($this->aliases[$alias]);
}
$this->reset();
return true;
}
/**
* {@inheritdoc}
*/
public function getConnectionByCommand(CommandInterface $command)
{
if (!$this->current) {
if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) {
$this->current = $slave;
} else {
$this->current = $this->getMasterOrDie();
}
return $this->current;
}
if ($this->current === $master = $this->getMasterOrDie()) {
return $master;
}
if (!$this->strategy->isReadOperation($command) || !$this->slaves) {
$this->current = $master;
}
return $this->current;
}
/**
* {@inheritdoc}
*/
public function getConnectionById($id)
{
return $this->pool[$id] ?? null;
}
/**
* Returns a connection instance by its alias.
*
* @param string $alias Connection alias.
*
* @return NodeConnectionInterface|null
*/
public function getConnectionByAlias($alias)
{
return $this->aliases[$alias] ?? null;
}
/**
* Returns a connection by its role.
*
* @param string $role Connection role (`master` or `slave`)
*
* @return NodeConnectionInterface|null
*/
public function getConnectionByRole($role)
{
if ($role === 'master') {
return $this->getMaster();
} elseif ($role === 'slave') {
return $this->pickSlave();
}
return null;
}
/**
* Switches the internal connection in use by the backend.
*
* @param NodeConnectionInterface $connection Connection instance in the pool.
*/
public function switchTo(NodeConnectionInterface $connection)
{
if ($connection && $connection === $this->current) {
return;
}
if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
throw new InvalidArgumentException('Invalid connection or connection not found.');
}
$this->current = $connection;
}
/**
* {@inheritdoc}
*/
public function switchToMaster()
{
if (!$connection = $this->getConnectionByRole('master')) {
throw new InvalidArgumentException('Invalid connection or connection not found.');
}
$this->switchTo($connection);
}
/**
* {@inheritdoc}
*/
public function switchToSlave()
{
if (!$connection = $this->getConnectionByRole('slave')) {
throw new InvalidArgumentException('Invalid connection or connection not found.');
}
$this->switchTo($connection);
}
/**
* {@inheritdoc}
*/
public function getCurrent()
{
return $this->current;
}
/**
* {@inheritdoc}
*/
public function getMaster()
{
return $this->master;
}
/**
* Returns the connection associated to the master server.
*
* @return NodeConnectionInterface
*/
private function getMasterOrDie()
{
if (!$connection = $this->getMaster()) {
throw new MissingMasterException('No master server available for replication');
}
return $connection;
}
/**
* {@inheritdoc}
*/
public function getSlaves()
{
return $this->slaves;
}
/**
* Returns the underlying replication strategy.
*
* @return ReplicationStrategy
*/
public function getReplicationStrategy()
{
return $this->strategy;
}
/**
* Returns a random slave.
*
* @return NodeConnectionInterface|null
*/
protected function pickSlave()
{
if (!$this->slaves) {
return null;
}
return $this->slaves[array_rand($this->slaves)];
}
/**
* {@inheritdoc}
*/
public function isConnected()
{
return $this->current ? $this->current->isConnected() : false;
}
/**
* {@inheritdoc}
*/
public function connect()
{
if (!$this->current) {
if (!$this->current = $this->pickSlave()) {
if (!$this->current = $this->getMaster()) {
throw new ClientException('No available connection for replication');
}
}
}
$this->current->connect();
}
/**
* {@inheritdoc}
*/
public function disconnect()
{
foreach ($this->pool as $connection) {
$connection->disconnect();
}
}
/**
* Handles response from INFO.
*
* @param string $response
*
* @return array
*/
private function handleInfoResponse($response)
{
$info = [];
foreach (preg_split('/\r?\n/', $response) as $row) {
if (strpos($row, ':') === false) {
continue;
}
[$k, $v] = explode(':', $row, 2);
$info[$k] = $v;
}
return $info;
}
/**
* Fetches the replication configuration from one of the servers.
*/
public function discover()
{
if (!$this->connectionFactory) {
throw new ClientException('Discovery requires a connection factory');
}
while (true) {
try {
if ($connection = $this->getMaster()) {
$this->discoverFromMaster($connection, $this->connectionFactory);
break;
} elseif ($connection = $this->pickSlave()) {
$this->discoverFromSlave($connection, $this->connectionFactory);
break;
} else {
throw new ClientException('No connection available for discovery');
}
} catch (ConnectionException $exception) {
$this->remove($connection);
}
}
}
/**
* Discovers the replication configuration by contacting the master node.
*
* @param NodeConnectionInterface $connection Connection to the master node.
* @param FactoryInterface $connectionFactory Connection factory instance.
*/
protected function discoverFromMaster(NodeConnectionInterface $connection, FactoryInterface $connectionFactory)
{
$response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION'));
$replication = $this->handleInfoResponse($response);
if ($replication['role'] !== 'master') {
throw new ClientException("Role mismatch (expected master, got slave) [$connection]");
}
$this->slaves = [];
foreach ($replication as $k => $v) {
$parameters = null;
if (strpos($k, 'slave') === 0 && preg_match('/ip=(?P<host>.*),port=(?P<port>\d+)/', $v, $parameters)) {
$slaveConnection = $connectionFactory->create([
'host' => $parameters['host'],
'port' => $parameters['port'],
'role' => 'slave',
]);
$this->add($slaveConnection);
}
}
}
/**
* Discovers the replication configuration by contacting one of the slaves.
*
* @param NodeConnectionInterface $connection Connection to one of the slaves.
* @param FactoryInterface $connectionFactory Connection factory instance.
*/
protected function discoverFromSlave(NodeConnectionInterface $connection, FactoryInterface $connectionFactory)
{
$response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION'));
$replication = $this->handleInfoResponse($response);
if ($replication['role'] !== 'slave') {
throw new ClientException("Role mismatch (expected slave, got master) [$connection]");
}
$masterConnection = $connectionFactory->create([
'host' => $replication['master_host'],
'port' => $replication['master_port'],
'role' => 'master',
]);
$this->add($masterConnection);
$this->discoverFromMaster($masterConnection, $connectionFactory);
}
/**
* Retries the execution of a command upon slave failure.
*
* @param CommandInterface $command Command instance.
* @param string $method Actual method.
*
* @return mixed
*/
private function retryCommandOnFailure(CommandInterface $command, $method)
{
while (true) {
try {
$connection = $this->getConnectionByCommand($command);
$response = $connection->$method($command);
if ($response instanceof ResponseErrorInterface && $response->getErrorType() === 'LOADING') {
throw new ConnectionException($connection, "Redis is loading the dataset in memory [$connection]");
}
break;
} catch (ConnectionException $exception) {
$connection = $exception->getConnection();
$connection->disconnect();
if ($connection === $this->master && !$this->autoDiscovery) {
// Throw immediately when master connection is failing, even
// when the command represents a read-only operation, unless
// automatic discovery has been enabled.
throw $exception;
} else {
// Otherwise remove the failing slave and attempt to execute
// the command again on one of the remaining slaves...
$this->remove($connection);
}
// ... that is, unless we have no more connections to use.
if (!$this->slaves && !$this->master) {
throw $exception;
} elseif ($this->autoDiscovery) {
$this->discover();
}
} catch (MissingMasterException $exception) {
if ($this->autoDiscovery) {
$this->discover();
} else {
throw $exception;
}
}
}
return $response;
}
/**
* {@inheritdoc}
*/
public function writeRequest(CommandInterface $command)
{
$this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
* {@inheritdoc}
*/
public function readResponse(CommandInterface $command)
{
return $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
* {@inheritdoc}
*/
public function executeCommand(CommandInterface $command)
{
return $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return ['master', 'slaves', 'pool', 'aliases', 'strategy'];
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Connection\Replication;
use Predis\Connection\AggregateConnectionInterface;
use Predis\Connection\NodeConnectionInterface;
/**
* Defines a group of Redis nodes in a master / slave replication setup.
*/
interface ReplicationInterface extends AggregateConnectionInterface
{
/**
* Switches the internal connection in use to the master server.
*/
public function switchToMaster();
/**
* Switches the internal connection in use to a random slave server.
*/
public function switchToSlave();
/**
* Returns the connection in use by the replication backend.
*
* @return NodeConnectionInterface
*/
public function getCurrent();
/**
* Returns the connection to the master server.
*
* @return NodeConnectionInterface
*/
public function getMaster();
/**
* Returns a list of connections to slave servers.
*
* @return NodeConnectionInterface[]
*/
public function getSlaves();
}

View file

@ -0,0 +1,775 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Connection\Replication;
use InvalidArgumentException;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\CommunicationException;
use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface as ConnectionFactoryInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\Connection\Parameters;
use Predis\Replication\ReplicationStrategy;
use Predis\Replication\RoleException;
use Predis\Response\Error;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ServerException;
/**
* @author Daniele Alessandri <suppakilla@gmail.com>
* @author Ville Mattila <ville@eventio.fi>
*/
class SentinelReplication implements ReplicationInterface
{
/**
* @var NodeConnectionInterface
*/
protected $master;
/**
* @var NodeConnectionInterface[]
*/
protected $slaves = [];
/**
* @var NodeConnectionInterface[]
*/
protected $pool = [];
/**
* @var NodeConnectionInterface
*/
protected $current;
/**
* @var string
*/
protected $service;
/**
* @var ConnectionFactoryInterface
*/
protected $connectionFactory;
/**
* @var ReplicationStrategy
*/
protected $strategy;
/**
* @var NodeConnectionInterface[]
*/
protected $sentinels = [];
/**
* @var int
*/
protected $sentinelIndex = 0;
/**
* @var NodeConnectionInterface
*/
protected $sentinelConnection;
/**
* @var float
*/
protected $sentinelTimeout = 0.100;
/**
* Max number of automatic retries of commands upon server failure.
*
* -1 = unlimited retry attempts
* 0 = no retry attempts (fails immediately)
* n = fail only after n retry attempts
*
* @var int
*/
protected $retryLimit = 20;
/**
* Time to wait in milliseconds before fetching a new configuration from one
* of the sentinel servers.
*
* @var int
*/
protected $retryWait = 1000;
/**
* Flag for automatic fetching of available sentinels.
*
* @var bool
*/
protected $updateSentinels = false;
/**
* @param string $service Name of the service for autodiscovery.
* @param array $sentinels Sentinel servers connection parameters.
* @param ConnectionFactoryInterface $connectionFactory Connection factory instance.
* @param ReplicationStrategy $strategy Replication strategy instance.
*/
public function __construct(
$service,
array $sentinels,
ConnectionFactoryInterface $connectionFactory,
ReplicationStrategy $strategy = null
) {
$this->sentinels = $sentinels;
$this->service = $service;
$this->connectionFactory = $connectionFactory;
$this->strategy = $strategy ?: new ReplicationStrategy();
}
/**
* Sets a default timeout for connections to sentinels.
*
* When "timeout" is present in the connection parameters of sentinels, its
* value overrides the default sentinel timeout.
*
* @param float $timeout Timeout value.
*/
public function setSentinelTimeout($timeout)
{
$this->sentinelTimeout = (float) $timeout;
}
/**
* Sets the maximum number of retries for commands upon server failure.
*
* -1 = unlimited retry attempts
* 0 = no retry attempts (fails immediately)
* n = fail only after n retry attempts
*
* @param int $retry Number of retry attempts.
*/
public function setRetryLimit($retry)
{
$this->retryLimit = (int) $retry;
}
/**
* Sets the time to wait (in milliseconds) before fetching a new configuration
* from one of the sentinels.
*
* @param float $milliseconds Time to wait before the next attempt.
*/
public function setRetryWait($milliseconds)
{
$this->retryWait = (float) $milliseconds;
}
/**
* Set automatic fetching of available sentinels.
*
* @param bool $update Enable or disable automatic updates.
*/
public function setUpdateSentinels($update)
{
$this->updateSentinels = (bool) $update;
}
/**
* Resets the current connection.
*/
protected function reset()
{
$this->current = null;
}
/**
* Wipes the current list of master and slaves nodes.
*/
protected function wipeServerList()
{
$this->reset();
$this->master = null;
$this->slaves = [];
$this->pool = [];
}
/**
* {@inheritdoc}
*/
public function add(NodeConnectionInterface $connection)
{
$parameters = $connection->getParameters();
$role = $parameters->role;
if ('master' === $role) {
$this->master = $connection;
} elseif ('sentinel' === $role) {
$this->sentinels[] = $connection;
// sentinels are not considered part of the pool.
return;
} else {
// everything else is considered a slave.
$this->slaves[] = $connection;
}
$this->pool[(string) $connection] = $connection;
$this->reset();
}
/**
* {@inheritdoc}
*/
public function remove(NodeConnectionInterface $connection)
{
if ($connection === $this->master) {
$this->master = null;
} elseif (false !== $id = array_search($connection, $this->slaves, true)) {
unset($this->slaves[$id]);
} elseif (false !== $id = array_search($connection, $this->sentinels, true)) {
unset($this->sentinels[$id]);
return true;
} else {
return false;
}
unset($this->pool[(string) $connection]);
$this->reset();
return true;
}
/**
* Creates a new connection to a sentinel server.
*
* @return NodeConnectionInterface
*/
protected function createSentinelConnection($parameters)
{
if ($parameters instanceof NodeConnectionInterface) {
return $parameters;
}
if (is_string($parameters)) {
$parameters = Parameters::parse($parameters);
}
if (is_array($parameters)) {
// NOTE: sentinels do not accept AUTH and SELECT commands so we must
// explicitly set them to NULL to avoid problems when using default
// parameters set via client options. Actually AUTH is supported for
// sentinels starting with Redis 5 but we have to differentiate from
// sentinels passwords and nodes passwords, this will be implemented
// in a later release.
$parameters['database'] = null;
$parameters['username'] = null;
// don't leak password from between configurations
// https://github.com/predis/predis/pull/807/#discussion_r985764770
if (!isset($parameters['password'])) {
$parameters['password'] = null;
}
if (!isset($parameters['timeout'])) {
$parameters['timeout'] = $this->sentinelTimeout;
}
}
return $this->connectionFactory->create($parameters);
}
/**
* Returns the current sentinel connection.
*
* If there is no active sentinel connection, a new connection is created.
*
* @return NodeConnectionInterface
*/
public function getSentinelConnection()
{
if (!$this->sentinelConnection) {
if ($this->sentinelIndex >= count($this->sentinels)) {
$this->sentinelIndex = 0;
throw new \Predis\ClientException('No sentinel server available for autodiscovery.');
}
$sentinel = $this->sentinels[$this->sentinelIndex];
++$this->sentinelIndex;
$this->sentinelConnection = $this->createSentinelConnection($sentinel);
}
return $this->sentinelConnection;
}
/**
* Fetches an updated list of sentinels from a sentinel.
*/
public function updateSentinels()
{
SENTINEL_QUERY: {
$sentinel = $this->getSentinelConnection();
try {
$payload = $sentinel->executeCommand(
RawCommand::create('SENTINEL', 'sentinels', $this->service)
);
$this->sentinels = [];
$this->sentinelIndex = 0;
// NOTE: sentinel server does not return itself, so we add it back.
$this->sentinels[] = $sentinel->getParameters()->toArray();
foreach ($payload as $sentinel) {
$this->sentinels[] = [
'host' => $sentinel[3],
'port' => $sentinel[5],
'role' => 'sentinel',
];
}
} catch (ConnectionException $exception) {
$this->sentinelConnection = null;
goto SENTINEL_QUERY;
}
}
}
/**
* Fetches the details for the master and slave servers from a sentinel.
*/
public function querySentinel()
{
$this->wipeServerList();
$this->updateSentinels();
$this->getMaster();
$this->getSlaves();
}
/**
* Handles error responses returned by redis-sentinel.
*
* @param NodeConnectionInterface $sentinel Connection to a sentinel server.
* @param ErrorResponseInterface $error Error response.
*/
private function handleSentinelErrorResponse(NodeConnectionInterface $sentinel, ErrorResponseInterface $error)
{
if ($error->getErrorType() === 'IDONTKNOW') {
throw new ConnectionException($sentinel, $error->getMessage());
} else {
throw new ServerException($error->getMessage());
}
}
/**
* Fetches the details for the master server from a sentinel.
*
* @param NodeConnectionInterface $sentinel Connection to a sentinel server.
* @param string $service Name of the service.
*
* @return array
*/
protected function querySentinelForMaster(NodeConnectionInterface $sentinel, $service)
{
$payload = $sentinel->executeCommand(
RawCommand::create('SENTINEL', 'get-master-addr-by-name', $service)
);
if ($payload === null) {
throw new ServerException('ERR No such master with that name');
}
if ($payload instanceof ErrorResponseInterface) {
$this->handleSentinelErrorResponse($sentinel, $payload);
}
return [
'host' => $payload[0],
'port' => $payload[1],
'role' => 'master',
];
}
/**
* Fetches the details for the slave servers from a sentinel.
*
* @param NodeConnectionInterface $sentinel Connection to a sentinel server.
* @param string $service Name of the service.
*
* @return array
*/
protected function querySentinelForSlaves(NodeConnectionInterface $sentinel, $service)
{
$slaves = [];
$payload = $sentinel->executeCommand(
RawCommand::create('SENTINEL', 'slaves', $service)
);
if ($payload instanceof ErrorResponseInterface) {
$this->handleSentinelErrorResponse($sentinel, $payload);
}
foreach ($payload as $slave) {
$flags = explode(',', $slave[9]);
if (array_intersect($flags, ['s_down', 'o_down', 'disconnected'])) {
continue;
}
$slaves[] = [
'host' => $slave[3],
'port' => $slave[5],
'role' => 'slave',
];
}
return $slaves;
}
/**
* {@inheritdoc}
*/
public function getCurrent()
{
return $this->current;
}
/**
* {@inheritdoc}
*/
public function getMaster()
{
if ($this->master) {
return $this->master;
}
if ($this->updateSentinels) {
$this->updateSentinels();
}
SENTINEL_QUERY: {
$sentinel = $this->getSentinelConnection();
try {
$masterParameters = $this->querySentinelForMaster($sentinel, $this->service);
$masterConnection = $this->connectionFactory->create($masterParameters);
$this->add($masterConnection);
} catch (ConnectionException $exception) {
$this->sentinelConnection = null;
goto SENTINEL_QUERY;
}
}
return $masterConnection;
}
/**
* {@inheritdoc}
*/
public function getSlaves()
{
if ($this->slaves) {
return array_values($this->slaves);
}
if ($this->updateSentinels) {
$this->updateSentinels();
}
SENTINEL_QUERY: {
$sentinel = $this->getSentinelConnection();
try {
$slavesParameters = $this->querySentinelForSlaves($sentinel, $this->service);
foreach ($slavesParameters as $slaveParameters) {
$this->add($this->connectionFactory->create($slaveParameters));
}
} catch (ConnectionException $exception) {
$this->sentinelConnection = null;
goto SENTINEL_QUERY;
}
}
return array_values($this->slaves);
}
/**
* Returns a random slave.
*
* @return NodeConnectionInterface|null
*/
protected function pickSlave()
{
$slaves = $this->getSlaves();
return $slaves
? $slaves[rand(1, count($slaves)) - 1]
: null;
}
/**
* Returns the connection instance in charge for the given command.
*
* @param CommandInterface $command Command instance.
*
* @return NodeConnectionInterface
*/
private function getConnectionInternal(CommandInterface $command)
{
if (!$this->current) {
if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) {
$this->current = $slave;
} else {
$this->current = $this->getMaster();
}
return $this->current;
}
if ($this->current === $this->master) {
return $this->current;
}
if (!$this->strategy->isReadOperation($command)) {
$this->current = $this->getMaster();
}
return $this->current;
}
/**
* Asserts that the specified connection matches an expected role.
*
* @param NodeConnectionInterface $connection Connection to a redis server.
* @param string $role Expected role of the server ("master", "slave" or "sentinel").
*
* @throws RoleException|ConnectionException
*/
protected function assertConnectionRole(NodeConnectionInterface $connection, $role)
{
$role = strtolower($role);
$actualRole = $connection->executeCommand(RawCommand::create('ROLE'));
if ($actualRole instanceof Error) {
throw new ConnectionException($connection, $actualRole->getMessage());
}
if ($role !== $actualRole[0]) {
throw new RoleException($connection, "Expected $role but got $actualRole[0] [$connection]");
}
}
/**
* {@inheritdoc}
*/
public function getConnectionByCommand(CommandInterface $command)
{
$connection = $this->getConnectionInternal($command);
if (!$connection->isConnected()) {
// When we do not have any available slave in the pool we can expect
// read-only operations to hit the master server.
$expectedRole = $this->strategy->isReadOperation($command) && $this->slaves ? 'slave' : 'master';
$this->assertConnectionRole($connection, $expectedRole);
}
return $connection;
}
/**
* {@inheritdoc}
*/
public function getConnectionById($id)
{
return $this->pool[$id] ?? null;
}
/**
* Returns a connection by its role.
*
* @param string $role Connection role (`master`, `slave` or `sentinel`)
*
* @return NodeConnectionInterface|null
*/
public function getConnectionByRole($role)
{
if ($role === 'master') {
return $this->getMaster();
} elseif ($role === 'slave') {
return $this->pickSlave();
} elseif ($role === 'sentinel') {
return $this->getSentinelConnection();
} else {
return null;
}
}
/**
* Switches the internal connection in use by the backend.
*
* Sentinel connections are not considered as part of the pool, meaning that
* trying to switch to a sentinel will throw an exception.
*
* @param NodeConnectionInterface $connection Connection instance in the pool.
*/
public function switchTo(NodeConnectionInterface $connection)
{
if ($connection && $connection === $this->current) {
return;
}
if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
throw new InvalidArgumentException('Invalid connection or connection not found.');
}
$connection->connect();
if ($this->current) {
$this->current->disconnect();
}
$this->current = $connection;
}
/**
* {@inheritdoc}
*/
public function switchToMaster()
{
$connection = $this->getConnectionByRole('master');
$this->switchTo($connection);
}
/**
* {@inheritdoc}
*/
public function switchToSlave()
{
$connection = $this->getConnectionByRole('slave');
$this->switchTo($connection);
}
/**
* {@inheritdoc}
*/
public function isConnected()
{
return $this->current ? $this->current->isConnected() : false;
}
/**
* {@inheritdoc}
*/
public function connect()
{
if (!$this->current) {
if (!$this->current = $this->pickSlave()) {
$this->current = $this->getMaster();
}
}
$this->current->connect();
}
/**
* {@inheritdoc}
*/
public function disconnect()
{
foreach ($this->pool as $connection) {
$connection->disconnect();
}
}
/**
* Retries the execution of a command upon server failure after asking a new
* configuration to one of the sentinels.
*
* @param CommandInterface $command Command instance.
* @param string $method Actual method.
*
* @return mixed
*/
private function retryCommandOnFailure(CommandInterface $command, $method)
{
$retries = 0;
while ($retries <= $this->retryLimit) {
try {
$response = $this->getConnectionByCommand($command)->$method($command);
break;
} catch (CommunicationException $exception) {
$this->wipeServerList();
$exception->getConnection()->disconnect();
if ($retries === $this->retryLimit) {
throw $exception;
}
usleep($this->retryWait * 1000);
++$retries;
}
}
return $response;
}
/**
* {@inheritdoc}
*/
public function writeRequest(CommandInterface $command)
{
$this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
* {@inheritdoc}
*/
public function readResponse(CommandInterface $command)
{
return $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
* {@inheritdoc}
*/
public function executeCommand(CommandInterface $command)
{
return $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
* Returns the underlying replication strategy.
*
* @return ReplicationStrategy
*/
public function getReplicationStrategy()
{
return $this->strategy;
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return [
'master', 'slaves', 'pool', 'service', 'sentinels', 'connectionFactory', 'strategy',
];
}
}