Retwis ported to modern Redis data types.
The example (and related documentation at redis.io) was not updated for 5 years. Redis had no sorted sets and hashes when the original code was written (!). An update was really needed. We also use a modern PHP client now: Predis. A copy is shipped within this repository to make life easier to newcomers trying Redis for the first time via this example.
This commit is contained in:
commit
c58f935fdc
277 changed files with 18503 additions and 0 deletions
227
Predis/Connection/AbstractConnection.php
Normal file
227
Predis/Connection/AbstractConnection.php
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\ClientException;
|
||||
use Predis\CommunicationException;
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Protocol\ProtocolException;
|
||||
|
||||
/**
|
||||
* Base class with the common logic used by connection classes to communicate with Redis.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
abstract class AbstractConnection implements SingleConnectionInterface
|
||||
{
|
||||
private $resource;
|
||||
private $cachedId;
|
||||
|
||||
protected $parameters;
|
||||
protected $initCmds = array();
|
||||
|
||||
/**
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
*/
|
||||
public function __construct(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$this->parameters = $this->checkParameters($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the server and destroys the underlying resource when
|
||||
* PHP's garbage collector kicks in.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks some of the parameters used to initialize the connection.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Initialization parameters for the connection.
|
||||
* @return ConnectionParametersInterface
|
||||
*/
|
||||
protected function checkParameters(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
switch ($parameters->scheme) {
|
||||
case 'unix':
|
||||
if (!isset($parameters->path)) {
|
||||
throw new \InvalidArgumentException('Missing UNIX domain socket path');
|
||||
}
|
||||
|
||||
case 'tcp':
|
||||
return $parameters;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("Invalid scheme: {$parameters->scheme}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the underlying resource used to communicate with Redis.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function createResource();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isConnected()
|
||||
{
|
||||
return isset($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if ($this->isConnected()) {
|
||||
throw new ClientException('Connection already estabilished');
|
||||
}
|
||||
|
||||
$this->resource = $this->createResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
unset($this->resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function pushInitCommand(CommandInterface $command)
|
||||
{
|
||||
$this->initCmds[] = $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeCommand(CommandInterface $command)
|
||||
{
|
||||
$this->writeCommand($command);
|
||||
|
||||
return $this->readResponse($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readResponse(CommandInterface $command)
|
||||
{
|
||||
return $this->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle connection errors.
|
||||
*
|
||||
* @param string $message Error message.
|
||||
* @param int $code Error code.
|
||||
*/
|
||||
protected function onConnectionError($message, $code = null)
|
||||
{
|
||||
CommunicationException::handle(new ConnectionException($this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]", $code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle protocol errors.
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*/
|
||||
protected function onProtocolError($message)
|
||||
{
|
||||
CommunicationException::handle(new ProtocolException($this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle not supported connection parameters.
|
||||
*
|
||||
* @param string $option Name of the option.
|
||||
* @param mixed $parameters Parameters used to initialize the connection.
|
||||
*/
|
||||
protected function onInvalidOption($option, $parameters = null)
|
||||
{
|
||||
$class = get_called_class();
|
||||
$message = "Invalid option for connection $class: $option";
|
||||
|
||||
if (isset($parameters)) {
|
||||
$message .= sprintf(' [%s => %s]', $option, $parameters->{$option});
|
||||
}
|
||||
|
||||
throw new NotSupportedException($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
if (isset($this->resource)) {
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an identifier for the connection.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getIdentifier()
|
||||
{
|
||||
if ($this->parameters->scheme === 'unix') {
|
||||
return $this->parameters->path;
|
||||
}
|
||||
|
||||
return "{$this->parameters->host}:{$this->parameters->port}";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
if (!isset($this->cachedId)) {
|
||||
$this->cachedId = $this->getIdentifier();
|
||||
}
|
||||
|
||||
return $this->cachedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array('parameters', 'initCmds');
|
||||
}
|
||||
}
|
||||
55
Predis/Connection/AggregatedConnectionInterface.php
Normal file
55
Predis/Connection/AggregatedConnectionInterface.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Command\CommandInterface;
|
||||
|
||||
/**
|
||||
* Defines a virtual connection composed by multiple connection objects.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface AggregatedConnectionInterface extends ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Adds a connection instance to the aggregated connection.
|
||||
*
|
||||
* @param SingleConnectionInterface $connection Instance of a connection.
|
||||
*/
|
||||
public function add(SingleConnectionInterface $connection);
|
||||
|
||||
/**
|
||||
* Removes the specified connection instance from the aggregated
|
||||
* connection.
|
||||
*
|
||||
* @param SingleConnectionInterface $connection Instance of a connection.
|
||||
* @return bool Returns true if the connection was in the pool.
|
||||
*/
|
||||
public function remove(SingleConnectionInterface $connection);
|
||||
|
||||
/**
|
||||
* Gets the actual connection instance in charge of the specified command.
|
||||
*
|
||||
* @param CommandInterface $command Instance of a Redis command.
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getConnection(CommandInterface $command);
|
||||
|
||||
/**
|
||||
* Retrieves a connection instance from the aggregated connection
|
||||
* using an alias.
|
||||
*
|
||||
* @param string $connectionId Alias of a connection
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getConnectionById($connectionId);
|
||||
}
|
||||
22
Predis/Connection/ClusterConnectionInterface.php
Normal file
22
Predis/Connection/ClusterConnectionInterface.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
/**
|
||||
* Defines a cluster of Redis servers formed by aggregating multiple
|
||||
* connection objects.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface ClusterConnectionInterface extends AggregatedConnectionInterface
|
||||
{
|
||||
}
|
||||
57
Predis/Connection/ComposableConnectionInterface.php
Normal file
57
Predis/Connection/ComposableConnectionInterface.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Protocol\ProtocolInterface;
|
||||
|
||||
/**
|
||||
* Defines a connection object used to communicate with a single Redis server
|
||||
* that leverages an external protocol processor to handle pluggable protocol
|
||||
* handlers.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface ComposableConnectionInterface extends SingleConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Sets the protocol processor used by the connection.
|
||||
*
|
||||
* @param ProtocolInterface $protocol Protocol processor.
|
||||
*/
|
||||
public function setProtocol(ProtocolInterface $protocol);
|
||||
|
||||
/**
|
||||
* Gets the protocol processor used by the connection.
|
||||
*/
|
||||
public function getProtocol();
|
||||
|
||||
/**
|
||||
* Writes a buffer that contains a serialized Redis command.
|
||||
*
|
||||
* @param string $buffer Serialized Redis command.
|
||||
*/
|
||||
public function writeBytes($buffer);
|
||||
|
||||
/**
|
||||
* Reads a specified number of bytes from the connection.
|
||||
*
|
||||
* @param string
|
||||
*/
|
||||
public function readBytes($length);
|
||||
|
||||
/**
|
||||
* Reads a line from the connection.
|
||||
*
|
||||
* @param string
|
||||
*/
|
||||
public function readLine();
|
||||
}
|
||||
135
Predis/Connection/ComposableStreamConnection.php
Normal file
135
Predis/Connection/ComposableStreamConnection.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Protocol\ProtocolInterface;
|
||||
use Predis\Protocol\Text\TextProtocol;
|
||||
|
||||
/**
|
||||
* Connection abstraction to Redis servers based on PHP's stream that uses an
|
||||
* external protocol processor defining the protocol used for the communication.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class ComposableStreamConnection extends StreamConnection implements ComposableConnectionInterface
|
||||
{
|
||||
private $protocol;
|
||||
|
||||
/**
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @param ProtocolInterface $protocol A protocol processor.
|
||||
*/
|
||||
public function __construct(ConnectionParametersInterface $parameters, ProtocolInterface $protocol = null)
|
||||
{
|
||||
$this->parameters = $this->checkParameters($parameters);
|
||||
$this->protocol = $protocol ?: new TextProtocol();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setProtocol(ProtocolInterface $protocol)
|
||||
{
|
||||
if ($protocol === null) {
|
||||
throw new \InvalidArgumentException("The protocol instance cannot be a null value");
|
||||
}
|
||||
|
||||
$this->protocol = $protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getProtocol()
|
||||
{
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeBytes($buffer)
|
||||
{
|
||||
parent::writeBytes($buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readBytes($length)
|
||||
{
|
||||
if ($length <= 0) {
|
||||
throw new \InvalidArgumentException('Length parameter must be greater than 0');
|
||||
}
|
||||
|
||||
$value = '';
|
||||
$socket = $this->getResource();
|
||||
|
||||
do {
|
||||
$chunk = fread($socket, $length);
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->onConnectionError('Error while reading bytes from the server');
|
||||
}
|
||||
|
||||
$value .= $chunk;
|
||||
} while (($length -= strlen($chunk)) > 0);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readLine()
|
||||
{
|
||||
$value = '';
|
||||
$socket = $this->getResource();
|
||||
|
||||
do {
|
||||
$chunk = fgets($socket);
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->onConnectionError('Error while reading line from the server');
|
||||
}
|
||||
|
||||
$value .= $chunk;
|
||||
} while (substr($value, -2) !== "\r\n");
|
||||
|
||||
return substr($value, 0, -2);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$this->protocol->write($this, $command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
return $this->protocol->read($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array_diff(array_merge(parent::__sleep(), array('protocol')), array('mbiterable'));
|
||||
}
|
||||
}
|
||||
23
Predis/Connection/ConnectionException.php
Normal file
23
Predis/Connection/ConnectionException.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\CommunicationException;
|
||||
|
||||
/**
|
||||
* Exception class that identifies connection-related errors.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class ConnectionException extends CommunicationException
|
||||
{
|
||||
}
|
||||
180
Predis/Connection/ConnectionFactory.php
Normal file
180
Predis/Connection/ConnectionFactory.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Profile\ServerProfileInterface;
|
||||
|
||||
/**
|
||||
* Provides a default factory for Redis connections that maps URI schemes
|
||||
* to connection classes implementing Predis\Connection\SingleConnectionInterface.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class ConnectionFactory implements ConnectionFactoryInterface
|
||||
{
|
||||
protected $schemes;
|
||||
protected $profile;
|
||||
|
||||
/**
|
||||
* Initializes a new instance of the default connection factory class used by Predis.
|
||||
*
|
||||
* @param ServerProfileInterface $profile Server profile used to initialize new connections.
|
||||
*/
|
||||
public function __construct(ServerProfileInterface $profile = null)
|
||||
{
|
||||
$this->schemes = $this->getDefaultSchemes();
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a named array that maps URI schemes to connection classes.
|
||||
*
|
||||
* @return array Map of URI schemes and connection classes.
|
||||
*/
|
||||
protected function getDefaultSchemes()
|
||||
{
|
||||
return array(
|
||||
'tcp' => 'Predis\Connection\StreamConnection',
|
||||
'unix' => 'Predis\Connection\StreamConnection',
|
||||
'http' => 'Predis\Connection\WebdisConnection',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided argument represents a valid connection class
|
||||
* implementing Predis\Connection\SingleConnectionInterface. Optionally,
|
||||
* callable objects are used for lazy initialization of connection objects.
|
||||
*
|
||||
* @param mixed $initializer FQN of a connection class or a callable for lazy initialization.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function checkInitializer($initializer)
|
||||
{
|
||||
if (is_callable($initializer)) {
|
||||
return $initializer;
|
||||
}
|
||||
|
||||
$initializerReflection = new \ReflectionClass($initializer);
|
||||
|
||||
if (!$initializerReflection->isSubclassOf('Predis\Connection\SingleConnectionInterface')) {
|
||||
throw new \InvalidArgumentException(
|
||||
'A connection initializer must be a valid connection class or a callable object'
|
||||
);
|
||||
}
|
||||
|
||||
return $initializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function define($scheme, $initializer)
|
||||
{
|
||||
$this->schemes[$scheme] = $this->checkInitializer($initializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function undefine($scheme)
|
||||
{
|
||||
unset($this->schemes[$scheme]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function create($parameters)
|
||||
{
|
||||
if (!$parameters instanceof ConnectionParametersInterface) {
|
||||
$parameters = new ConnectionParameters($parameters ?: array());
|
||||
}
|
||||
|
||||
$scheme = $parameters->scheme;
|
||||
|
||||
if (!isset($this->schemes[$scheme])) {
|
||||
throw new \InvalidArgumentException("Unknown connection scheme: $scheme");
|
||||
}
|
||||
|
||||
$initializer = $this->schemes[$scheme];
|
||||
|
||||
if (is_callable($initializer)) {
|
||||
$connection = call_user_func($initializer, $parameters, $this);
|
||||
} else {
|
||||
$connection = new $initializer($parameters);
|
||||
$this->prepareConnection($connection);
|
||||
}
|
||||
|
||||
if (!$connection instanceof SingleConnectionInterface) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Objects returned by connection initializers must implement ' .
|
||||
'Predis\Connection\SingleConnectionInterface'
|
||||
);
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createAggregated(AggregatedConnectionInterface $connection, Array $parameters)
|
||||
{
|
||||
foreach ($parameters as $node) {
|
||||
$connection->add($node instanceof SingleConnectionInterface ? $node : $this->create($node));
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a connection object after its initialization.
|
||||
*
|
||||
* @param SingleConnectionInterface $connection Instance of a connection object.
|
||||
*/
|
||||
protected function prepareConnection(SingleConnectionInterface $connection)
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
$parameters = $connection->getParameters();
|
||||
|
||||
if (isset($parameters->password)) {
|
||||
$command = $this->profile->createCommand('auth', array($parameters->password));
|
||||
$connection->pushInitCommand($command);
|
||||
}
|
||||
|
||||
if (isset($parameters->database)) {
|
||||
$command = $this->profile->createCommand('select', array($parameters->database));
|
||||
$connection->pushInitCommand($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server profile used to create initialization commands for connections.
|
||||
*
|
||||
* @param ServerProfileInterface $profile Server profile instance.
|
||||
*/
|
||||
public function setProfile(ServerProfileInterface $profile)
|
||||
{
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the server profile used to create initialization commands for connections.
|
||||
*
|
||||
* @return ServerProfileInterface
|
||||
*/
|
||||
public function getProfile()
|
||||
{
|
||||
return $this->profile;
|
||||
}
|
||||
}
|
||||
53
Predis/Connection/ConnectionFactoryInterface.php
Normal file
53
Predis/Connection/ConnectionFactoryInterface.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
/**
|
||||
* Interface that must be implemented by classes that provide their own mechanism
|
||||
* to create and initialize new instances of Predis\Connection\SingleConnectionInterface.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface ConnectionFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Defines or overrides the connection class identified by a scheme prefix.
|
||||
*
|
||||
* @param string $scheme URI scheme identifying the connection class.
|
||||
* @param mixed $initializer FQN of a connection class or a callable object for lazy initialization.
|
||||
*/
|
||||
public function define($scheme, $initializer);
|
||||
|
||||
/**
|
||||
* Undefines the connection identified by a scheme prefix.
|
||||
*
|
||||
* @param string $scheme Parameters for the connection.
|
||||
*/
|
||||
public function undefine($scheme);
|
||||
|
||||
/**
|
||||
* Creates a new connection object.
|
||||
*
|
||||
* @param mixed $parameters Parameters for the connection.
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function create($parameters);
|
||||
|
||||
/**
|
||||
* Prepares an aggregation of connection objects.
|
||||
*
|
||||
* @param AggregatedConnectionInterface $cluster Instance of an aggregated connection class.
|
||||
* @param array $parameters List of parameters for each connection object.
|
||||
* @return AggregatedConnectionInterface
|
||||
*/
|
||||
public function createAggregated(AggregatedConnectionInterface $cluster, Array $parameters);
|
||||
}
|
||||
63
Predis/Connection/ConnectionInterface.php
Normal file
63
Predis/Connection/ConnectionInterface.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Command\CommandInterface;
|
||||
|
||||
/**
|
||||
* Defines a connection object used to communicate with one or multiple
|
||||
* Redis servers.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Opens the connection.
|
||||
*/
|
||||
public function connect();
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*/
|
||||
public function disconnect();
|
||||
|
||||
/**
|
||||
* Returns if the connection is open.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isConnected();
|
||||
|
||||
/**
|
||||
* Write a Redis command on the connection.
|
||||
*
|
||||
* @param CommandInterface $command Instance of a Redis command.
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command);
|
||||
|
||||
/**
|
||||
* Reads the reply for a Redis command from the connection.
|
||||
*
|
||||
* @param CommandInterface $command Instance of a Redis command.
|
||||
* @return mixed
|
||||
*/
|
||||
public function readResponse(CommandInterface $command);
|
||||
|
||||
/**
|
||||
* Writes a Redis command to the connection and reads back the reply.
|
||||
*
|
||||
* @param CommandInterface $command Instance of a Redis command.
|
||||
* @return mixed
|
||||
*/
|
||||
public function executeCommand(CommandInterface $command);
|
||||
}
|
||||
183
Predis/Connection/ConnectionParameters.php
Normal file
183
Predis/Connection/ConnectionParameters.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\ClientException;
|
||||
|
||||
/**
|
||||
* Handles parsing and validation of connection parameters.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class ConnectionParameters implements ConnectionParametersInterface
|
||||
{
|
||||
private $parameters;
|
||||
|
||||
private static $defaults = array(
|
||||
'scheme' => 'tcp',
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'timeout' => 5.0,
|
||||
);
|
||||
|
||||
/**
|
||||
* @param string|array $parameters Connection parameters in the form of an URI string or a named array.
|
||||
*/
|
||||
public function __construct($parameters = array())
|
||||
{
|
||||
if (!is_array($parameters)) {
|
||||
$parameters = self::parseURI($parameters);
|
||||
}
|
||||
|
||||
$this->parameters = $this->filter($parameters) + $this->getDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns some default parameters with their values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return self::$defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cast functions for user-supplied parameter values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getValueCasters()
|
||||
{
|
||||
return array(
|
||||
'port' => 'self::castInteger',
|
||||
'async_connect' => 'self::castBoolean',
|
||||
'persistent' => 'self::castBoolean',
|
||||
'timeout' => 'self::castFloat',
|
||||
'read_write_timeout' => 'self::castFloat',
|
||||
'iterable_multibulk' => 'self::castBoolean',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates value as boolean.
|
||||
*
|
||||
* @param mixed $value Input value.
|
||||
* @return bool
|
||||
*/
|
||||
private static function castBoolean($value)
|
||||
{
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates value as float.
|
||||
*
|
||||
* @param mixed $value Input value.
|
||||
* @return float
|
||||
*/
|
||||
private static function castFloat($value)
|
||||
{
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates value as integer.
|
||||
*
|
||||
* @param mixed $value Input value.
|
||||
* @return int
|
||||
*/
|
||||
private static function castInteger($value)
|
||||
{
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an URI string and returns an array of connection parameters.
|
||||
*
|
||||
* @param string $uri Connection string.
|
||||
* @return array
|
||||
*/
|
||||
public static function parseURI($uri)
|
||||
{
|
||||
if (stripos($uri, 'unix') === 0) {
|
||||
// Hack to support URIs for UNIX sockets with minimal effort.
|
||||
$uri = str_ireplace('unix:///', 'unix://localhost/', $uri);
|
||||
}
|
||||
|
||||
if (!($parsed = @parse_url($uri)) || !isset($parsed['host'])) {
|
||||
throw new ClientException("Invalid URI: $uri");
|
||||
}
|
||||
|
||||
if (isset($parsed['query'])) {
|
||||
parse_str($parsed['query'], $queryarray);
|
||||
unset($parsed['query']);
|
||||
|
||||
$parsed = array_merge($parsed, $queryarray);
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and converts each value of the connection parameters array.
|
||||
*
|
||||
* @param array $parameters Connection parameters.
|
||||
* @return array
|
||||
*/
|
||||
private function filter(Array $parameters)
|
||||
{
|
||||
if ($parameters) {
|
||||
$casters = array_intersect_key($this->getValueCasters(), $parameters);
|
||||
|
||||
foreach ($casters as $parameter => $caster) {
|
||||
$parameters[$parameter] = call_user_func($caster, $parameters[$parameter]);
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __get($parameter)
|
||||
{
|
||||
if (isset($this->{$parameter})) {
|
||||
return $this->parameters[$parameter];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __isset($parameter)
|
||||
{
|
||||
return isset($this->parameters[$parameter]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array('parameters');
|
||||
}
|
||||
}
|
||||
44
Predis/Connection/ConnectionParametersInterface.php
Normal file
44
Predis/Connection/ConnectionParametersInterface.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
/**
|
||||
* Interface that must be implemented by classes that provide their own mechanism
|
||||
* to parse and handle connection parameters.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface ConnectionParametersInterface
|
||||
{
|
||||
/**
|
||||
* Checks if the specified parameters is set.
|
||||
*
|
||||
* @param string $parameter Name of the parameter.
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($parameter);
|
||||
|
||||
/**
|
||||
* Returns the value of the specified parameter.
|
||||
*
|
||||
* @param string $parameter Name of the parameter.
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($parameter);
|
||||
|
||||
/**
|
||||
* Returns an array representation of the connection parameters.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray();
|
||||
}
|
||||
261
Predis/Connection/MasterSlaveReplication.php
Normal file
261
Predis/Connection/MasterSlaveReplication.php
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Replication\ReplicationStrategy;
|
||||
|
||||
/**
|
||||
* Aggregated connection class used by to handle replication with a
|
||||
* group of servers in a master/slave configuration.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class MasterSlaveReplication implements ReplicationConnectionInterface
|
||||
{
|
||||
protected $strategy;
|
||||
protected $master;
|
||||
protected $slaves;
|
||||
protected $current;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct(ReplicationStrategy $strategy = null)
|
||||
{
|
||||
$this->slaves = array();
|
||||
$this->strategy = $strategy ?: new ReplicationStrategy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if one master and at least one slave have been defined.
|
||||
*/
|
||||
protected function check()
|
||||
{
|
||||
if (!isset($this->master) || !$this->slaves) {
|
||||
throw new \RuntimeException('Replication needs a master and at least one slave.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the connection state.
|
||||
*/
|
||||
protected function reset()
|
||||
{
|
||||
$this->current = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(SingleConnectionInterface $connection)
|
||||
{
|
||||
$alias = $connection->getParameters()->alias;
|
||||
|
||||
if ($alias === 'master') {
|
||||
$this->master = $connection;
|
||||
} else {
|
||||
$this->slaves[$alias ?: count($this->slaves)] = $connection;
|
||||
}
|
||||
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function remove(SingleConnectionInterface $connection)
|
||||
{
|
||||
if ($connection->getParameters()->alias === 'master') {
|
||||
$this->master = null;
|
||||
$this->reset();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (($id = array_search($connection, $this->slaves, true)) !== false) {
|
||||
unset($this->slaves[$id]);
|
||||
$this->reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConnection(CommandInterface $command)
|
||||
{
|
||||
if ($this->current === null) {
|
||||
$this->check();
|
||||
$this->current = $this->strategy->isReadOperation($command) ? $this->pickSlave() : $this->master;
|
||||
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
if ($this->current === $this->master) {
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
if (!$this->strategy->isReadOperation($command)) {
|
||||
$this->current = $this->master;
|
||||
}
|
||||
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConnectionById($connectionId)
|
||||
{
|
||||
if ($connectionId === 'master') {
|
||||
return $this->master;
|
||||
}
|
||||
|
||||
if (isset($this->slaves[$connectionId])) {
|
||||
return $this->slaves[$connectionId];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function switchTo($connection)
|
||||
{
|
||||
$this->check();
|
||||
|
||||
if (!$connection instanceof SingleConnectionInterface) {
|
||||
$connection = $this->getConnectionById($connection);
|
||||
}
|
||||
if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
|
||||
throw new \InvalidArgumentException('The specified connection is not valid.');
|
||||
}
|
||||
|
||||
$this->current = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCurrent()
|
||||
{
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMaster()
|
||||
{
|
||||
return $this->master;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSlaves()
|
||||
{
|
||||
return array_values($this->slaves);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying replication strategy.
|
||||
*
|
||||
* @return ReplicationStrategy
|
||||
*/
|
||||
public function getReplicationStrategy()
|
||||
{
|
||||
return $this->strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random slave.
|
||||
*
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
protected function pickSlave()
|
||||
{
|
||||
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 === null) {
|
||||
$this->check();
|
||||
$this->current = $this->pickSlave();
|
||||
}
|
||||
|
||||
$this->current->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->master) {
|
||||
$this->master->disconnect();
|
||||
}
|
||||
|
||||
foreach ($this->slaves as $connection) {
|
||||
$connection->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$this->getConnection($command)->writeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readResponse(CommandInterface $command)
|
||||
{
|
||||
return $this->getConnection($command)->readResponse($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeCommand(CommandInterface $command)
|
||||
{
|
||||
return $this->getConnection($command)->executeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array('master', 'slaves', 'strategy');
|
||||
}
|
||||
}
|
||||
393
Predis/Connection/PhpiredisConnection.php
Normal file
393
Predis/Connection/PhpiredisConnection.php
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\ResponseError;
|
||||
use Predis\ResponseQueued;
|
||||
use Predis\Command\CommandInterface;
|
||||
|
||||
/**
|
||||
* This class provides the implementation of a Predis connection that uses the
|
||||
* PHP socket extension for network communication and wraps the phpiredis C
|
||||
* extension (PHP bindings for hiredis) to parse the Redis protocol. Everything
|
||||
* is highly experimental (even the very same phpiredis since it is quite new),
|
||||
* so use it at your own risk.
|
||||
*
|
||||
* This class is mainly intended to provide an optional low-overhead alternative
|
||||
* for processing replies from Redis compared to the standard pure-PHP classes.
|
||||
* Differences in speed when dealing with short inline replies are practically
|
||||
* nonexistent, the actual speed boost is for long multibulk replies when this
|
||||
* protocol processor can parse and return replies very fast.
|
||||
*
|
||||
* For instructions on how to build and install the phpiredis extension, please
|
||||
* consult the repository of the project.
|
||||
*
|
||||
* The connection parameters supported by this class are:
|
||||
*
|
||||
* - scheme: it can be either 'tcp' or 'unix'.
|
||||
* - host: hostname or IP address of the server.
|
||||
* - port: TCP port of the server.
|
||||
* - path: path of a UNIX domain socket when scheme is 'unix'.
|
||||
* - timeout: timeout to perform the connection.
|
||||
* - read_write_timeout: timeout of read / write operations.
|
||||
*
|
||||
* @link http://github.com/nrk/phpiredis
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class PhpiredisConnection extends AbstractConnection
|
||||
{
|
||||
const ERR_MSG_EXTENSION = 'The %s extension must be loaded in order to be able to use this connection class';
|
||||
|
||||
private $reader;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$this->checkExtensions();
|
||||
$this->initializeReader();
|
||||
|
||||
parent::__construct($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the server and destroys the underlying resource and the
|
||||
* protocol reader resource when PHP's garbage collector kicks in.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
phpiredis_reader_destroy($this->reader);
|
||||
|
||||
parent::__destruct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the socket and phpiredis extensions are loaded in PHP.
|
||||
*/
|
||||
private function checkExtensions()
|
||||
{
|
||||
if (!function_exists('socket_create')) {
|
||||
throw new NotSupportedException(sprintf(self::ERR_MSG_EXTENSION, 'socket'));
|
||||
}
|
||||
if (!function_exists('phpiredis_reader_create')) {
|
||||
throw new NotSupportedException(sprintf(self::ERR_MSG_EXTENSION, 'phpiredis'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkParameters(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
if (isset($parameters->iterable_multibulk)) {
|
||||
$this->onInvalidOption('iterable_multibulk', $parameters);
|
||||
}
|
||||
if (isset($parameters->persistent)) {
|
||||
$this->onInvalidOption('persistent', $parameters);
|
||||
}
|
||||
|
||||
return parent::checkParameters($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the protocol reader resource.
|
||||
*/
|
||||
private function initializeReader()
|
||||
{
|
||||
$reader = phpiredis_reader_create();
|
||||
|
||||
phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
|
||||
phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
|
||||
|
||||
$this->reader = $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler used by the protocol reader to handle status replies.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
private function getStatusHandler()
|
||||
{
|
||||
return function ($payload) {
|
||||
switch ($payload) {
|
||||
case 'OK':
|
||||
return true;
|
||||
|
||||
case 'QUEUED':
|
||||
return new ResponseQueued();
|
||||
|
||||
default:
|
||||
return $payload;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler used by the protocol reader to handle Redis errors.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
private function getErrorHandler()
|
||||
{
|
||||
return function ($errorMessage) {
|
||||
return new ResponseError($errorMessage);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method used to throw exceptions on socket errors.
|
||||
*/
|
||||
private function emitSocketError()
|
||||
{
|
||||
$errno = socket_last_error();
|
||||
$errstr = socket_strerror($errno);
|
||||
|
||||
$this->disconnect();
|
||||
|
||||
$this->onConnectionError(trim($errstr), $errno);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createResource()
|
||||
{
|
||||
$parameters = $this->parameters;
|
||||
|
||||
$isUnix = $this->parameters->scheme === 'unix';
|
||||
$domain = $isUnix ? AF_UNIX : AF_INET;
|
||||
$protocol = $isUnix ? 0 : SOL_TCP;
|
||||
|
||||
$socket = @call_user_func('socket_create', $domain, SOCK_STREAM, $protocol);
|
||||
if (!is_resource($socket)) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
|
||||
$this->setSocketOptions($socket, $parameters);
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets options on the socket resource from the connection parameters.
|
||||
*
|
||||
* @param resource $socket Socket resource.
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
*/
|
||||
private function setSocketOptions($socket, ConnectionParametersInterface $parameters)
|
||||
{
|
||||
if ($parameters->scheme !== 'tcp') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1)) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
|
||||
if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
|
||||
if (isset($parameters->read_write_timeout)) {
|
||||
$rwtimeout = $parameters->read_write_timeout;
|
||||
$timeoutSec = floor($rwtimeout);
|
||||
$timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000;
|
||||
|
||||
$timeout = array(
|
||||
'sec' => $timeoutSec,
|
||||
'usec' => $timeoutUsec,
|
||||
);
|
||||
|
||||
if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
|
||||
if (!socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the address from the connection parameters.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @return string
|
||||
*/
|
||||
protected static function getAddress(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
if ($parameters->scheme === 'unix') {
|
||||
return $parameters->path;
|
||||
}
|
||||
|
||||
$host = $parameters->host;
|
||||
|
||||
if (ip2long($host) === false) {
|
||||
if (false === $addresses = gethostbynamel($host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $addresses[array_rand($addresses)];
|
||||
}
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the actual connection to the server with a timeout.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @return string
|
||||
*/
|
||||
private function connectWithTimeout(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
if (false === $host = self::getAddress($parameters)) {
|
||||
$this->onConnectionError("Cannot resolve the address of '$parameters->host'.");
|
||||
}
|
||||
|
||||
$socket = $this->getResource();
|
||||
|
||||
socket_set_nonblock($socket);
|
||||
|
||||
if (@socket_connect($socket, $host, $parameters->port) === false) {
|
||||
$error = socket_last_error();
|
||||
if ($error != SOCKET_EINPROGRESS && $error != SOCKET_EALREADY) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
}
|
||||
|
||||
socket_set_block($socket);
|
||||
|
||||
$null = null;
|
||||
$selectable = array($socket);
|
||||
|
||||
$timeout = $parameters->timeout;
|
||||
$timeoutSecs = floor($timeout);
|
||||
$timeoutUSecs = ($timeout - $timeoutSecs) * 1000000;
|
||||
|
||||
$selected = socket_select($selectable, $selectable, $null, $timeoutSecs, $timeoutUSecs);
|
||||
|
||||
if ($selected === 2) {
|
||||
$this->onConnectionError('Connection refused', SOCKET_ECONNREFUSED);
|
||||
}
|
||||
if ($selected === 0) {
|
||||
$this->onConnectionError('Connection timed out', SOCKET_ETIMEDOUT);
|
||||
}
|
||||
if ($selected === false) {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
parent::connect();
|
||||
|
||||
$this->connectWithTimeout($this->parameters);
|
||||
|
||||
if ($this->initCmds) {
|
||||
$this->sendInitializationCommands();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->isConnected()) {
|
||||
socket_close($this->getResource());
|
||||
parent::disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the initialization commands to Redis when the connection is opened.
|
||||
*/
|
||||
private function sendInitializationCommands()
|
||||
{
|
||||
foreach ($this->initCmds as $command) {
|
||||
$this->writeCommand($command);
|
||||
}
|
||||
foreach ($this->initCmds as $command) {
|
||||
$this->readResponse($command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function write($buffer)
|
||||
{
|
||||
$socket = $this->getResource();
|
||||
|
||||
while (($length = strlen($buffer)) > 0) {
|
||||
$written = socket_write($socket, $buffer, $length);
|
||||
|
||||
if ($length === $written) {
|
||||
return;
|
||||
}
|
||||
if ($written === false) {
|
||||
$this->onConnectionError('Error while writing bytes to the server');
|
||||
}
|
||||
|
||||
$buffer = substr($buffer, $written);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$socket = $this->getResource();
|
||||
$reader = $this->reader;
|
||||
|
||||
while (($state = phpiredis_reader_get_state($reader)) === PHPIREDIS_READER_STATE_INCOMPLETE) {
|
||||
if (@socket_recv($socket, $buffer, 4096, 0) === false || $buffer === '') {
|
||||
$this->emitSocketError();
|
||||
}
|
||||
|
||||
phpiredis_reader_feed($reader, $buffer);
|
||||
}
|
||||
|
||||
if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
|
||||
return phpiredis_reader_get_reply($reader);
|
||||
} else {
|
||||
$this->onProtocolError(phpiredis_reader_get_error($reader));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$cmdargs = $command->getArguments();
|
||||
array_unshift($cmdargs, $command->getId());
|
||||
$this->write(phpiredis_format_command($cmdargs));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->checkExtensions();
|
||||
$this->initializeReader();
|
||||
}
|
||||
}
|
||||
196
Predis/Connection/PhpiredisStreamConnection.php
Normal file
196
Predis/Connection/PhpiredisStreamConnection.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\ResponseError;
|
||||
use Predis\ResponseQueued;
|
||||
use Predis\Command\CommandInterface;
|
||||
|
||||
/**
|
||||
* This class provides the implementation of a Predis connection that uses PHP's
|
||||
* streams for network communication and wraps the phpiredis C extension (PHP
|
||||
* bindings for hiredis) to parse and serialize the Redis protocol. Everything
|
||||
* is highly experimental (even the very same phpiredis since it is quite new),
|
||||
* so use it at your own risk.
|
||||
*
|
||||
* This class is mainly intended to provide an optional low-overhead alternative
|
||||
* for processing replies from Redis compared to the standard pure-PHP classes.
|
||||
* Differences in speed when dealing with short inline replies are practically
|
||||
* nonexistent, the actual speed boost is for long multibulk replies when this
|
||||
* protocol processor can parse and return replies very fast.
|
||||
*
|
||||
* For instructions on how to build and install the phpiredis extension, please
|
||||
* consult the repository of the project.
|
||||
*
|
||||
* The connection parameters supported by this class are:
|
||||
*
|
||||
* - scheme: it can be either 'tcp' or 'unix'.
|
||||
* - host: hostname or IP address of the server.
|
||||
* - port: TCP port of the server.
|
||||
* - path: path of a UNIX domain socket when scheme is 'unix'.
|
||||
* - timeout: timeout to perform the connection.
|
||||
* - read_write_timeout: timeout of read / write operations.
|
||||
* - async_connect: performs the connection asynchronously.
|
||||
* - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
|
||||
* - persistent: the connection is left intact after a GC collection.
|
||||
*
|
||||
* @link https://github.com/nrk/phpiredis
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class PhpiredisStreamConnection extends StreamConnection
|
||||
{
|
||||
private $reader;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$this->checkExtensions();
|
||||
$this->initializeReader();
|
||||
|
||||
parent::__construct($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
phpiredis_reader_destroy($this->reader);
|
||||
|
||||
parent::__destruct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the phpiredis extension is loaded in PHP.
|
||||
*/
|
||||
protected function checkExtensions()
|
||||
{
|
||||
if (!function_exists('phpiredis_reader_create')) {
|
||||
throw new NotSupportedException(
|
||||
'The phpiredis extension must be loaded in order to be able to use this connection class'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkParameters(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
if (isset($parameters->iterable_multibulk)) {
|
||||
$this->onInvalidOption('iterable_multibulk', $parameters);
|
||||
}
|
||||
|
||||
return parent::checkParameters($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the protocol reader resource.
|
||||
*/
|
||||
protected function initializeReader()
|
||||
{
|
||||
$reader = phpiredis_reader_create();
|
||||
|
||||
phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
|
||||
phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
|
||||
|
||||
$this->reader = $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler used by the protocol reader to handle status replies.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function getStatusHandler()
|
||||
{
|
||||
return function ($payload) {
|
||||
switch ($payload) {
|
||||
case 'OK':
|
||||
return true;
|
||||
|
||||
case 'QUEUED':
|
||||
return new ResponseQueued();
|
||||
|
||||
default:
|
||||
return $payload;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler used by the protocol reader to handle Redis errors.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function getErrorHandler()
|
||||
{
|
||||
return function ($errorMessage) {
|
||||
return new ResponseError($errorMessage);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$socket = $this->getResource();
|
||||
$reader = $this->reader;
|
||||
|
||||
while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) {
|
||||
$buffer = fread($socket, 4096);
|
||||
|
||||
if ($buffer === false || $buffer === '') {
|
||||
$this->onConnectionError('Error while reading bytes from the server');
|
||||
}
|
||||
|
||||
phpiredis_reader_feed($reader, $buffer);
|
||||
}
|
||||
|
||||
if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
|
||||
return phpiredis_reader_get_reply($reader);
|
||||
} else {
|
||||
$this->onProtocolError(phpiredis_reader_get_error($reader));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$cmdargs = $command->getArguments();
|
||||
array_unshift($cmdargs, $command->getId());
|
||||
$this->writeBytes(phpiredis_format_command($cmdargs));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array_diff(parent::__sleep(), array('mbiterable'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->checkExtensions();
|
||||
$this->initializeReader();
|
||||
}
|
||||
}
|
||||
232
Predis/Connection/PredisCluster.php
Normal file
232
Predis/Connection/PredisCluster.php
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Cluster\CommandHashStrategyInterface;
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\Cluster\PredisClusterHashStrategy;
|
||||
use Predis\Cluster\Distribution\DistributionStrategyInterface;
|
||||
use Predis\Cluster\Distribution\HashRing;
|
||||
use Predis\Command\CommandInterface;
|
||||
|
||||
/**
|
||||
* Abstraction for a cluster of aggregated connections to various Redis servers
|
||||
* implementing client-side sharding based on pluggable distribution strategies.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
* @todo Add the ability to remove connections from pool.
|
||||
*/
|
||||
class PredisCluster implements ClusterConnectionInterface, \IteratorAggregate, \Countable
|
||||
{
|
||||
private $pool;
|
||||
private $strategy;
|
||||
private $distributor;
|
||||
|
||||
/**
|
||||
* @param DistributionStrategyInterface $distributor Distribution strategy used by the cluster.
|
||||
*/
|
||||
public function __construct(DistributionStrategyInterface $distributor = null)
|
||||
{
|
||||
$distributor = $distributor ?: new HashRing();
|
||||
|
||||
$this->pool = array();
|
||||
$this->strategy = new PredisClusterHashStrategy($distributor->getHashGenerator());
|
||||
$this->distributor = $distributor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isConnected()
|
||||
{
|
||||
foreach ($this->pool as $connection) {
|
||||
if ($connection->isConnected()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
foreach ($this->pool as $connection) {
|
||||
$connection->connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
foreach ($this->pool as $connection) {
|
||||
$connection->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(SingleConnectionInterface $connection)
|
||||
{
|
||||
$parameters = $connection->getParameters();
|
||||
|
||||
if (isset($parameters->alias)) {
|
||||
$this->pool[$parameters->alias] = $connection;
|
||||
} else {
|
||||
$this->pool[] = $connection;
|
||||
}
|
||||
|
||||
$weight = isset($parameters->weight) ? $parameters->weight : null;
|
||||
$this->distributor->add($connection, $weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function remove(SingleConnectionInterface $connection)
|
||||
{
|
||||
if (($id = array_search($connection, $this->pool, true)) !== false) {
|
||||
unset($this->pool[$id]);
|
||||
$this->distributor->remove($connection);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection instance using its alias or index.
|
||||
*
|
||||
* @param string $connectionId Alias or index of a connection.
|
||||
* @return bool Returns true if the connection was in the pool.
|
||||
*/
|
||||
public function removeById($connectionId)
|
||||
{
|
||||
if ($connection = $this->getConnectionById($connectionId)) {
|
||||
return $this->remove($connection);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConnection(CommandInterface $command)
|
||||
{
|
||||
$hash = $this->strategy->getHash($command);
|
||||
|
||||
if (!isset($hash)) {
|
||||
throw new NotSupportedException("Cannot use {$command->getId()} with a cluster of connections");
|
||||
}
|
||||
|
||||
$node = $this->distributor->get($hash);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConnectionById($connectionId)
|
||||
{
|
||||
return isset($this->pool[$connectionId]) ? $this->pool[$connectionId] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a connection instance from the cluster using a key.
|
||||
*
|
||||
* @param string $key Key of a Redis value.
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getConnectionByKey($key)
|
||||
{
|
||||
$hash = $this->strategy->getKeyHash($key);
|
||||
$node = $this->distributor->get($hash);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying command hash strategy used to hash
|
||||
* commands by their keys.
|
||||
*
|
||||
* @return CommandHashStrategyInterface
|
||||
*/
|
||||
public function getCommandHashStrategy()
|
||||
{
|
||||
return $this->strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new \ArrayIterator($this->pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$this->getConnection($command)->writeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readResponse(CommandInterface $command)
|
||||
{
|
||||
return $this->getConnection($command)->readResponse($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeCommand(CommandInterface $command)
|
||||
{
|
||||
return $this->getConnection($command)->executeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the specified Redis command on all the nodes of a cluster.
|
||||
*
|
||||
* @param CommandInterface $command A Redis command.
|
||||
* @return array
|
||||
*/
|
||||
public function executeCommandOnNodes(CommandInterface $command)
|
||||
{
|
||||
$replies = array();
|
||||
|
||||
foreach ($this->pool as $connection) {
|
||||
$replies[] = $connection->executeCommand($command);
|
||||
}
|
||||
|
||||
return $replies;
|
||||
}
|
||||
}
|
||||
526
Predis/Connection/RedisCluster.php
Normal file
526
Predis/Connection/RedisCluster.php
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
use OutOfBoundsException;
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\ResponseErrorInterface;
|
||||
use Predis\Cluster\CommandHashStrategyInterface;
|
||||
use Predis\Cluster\RedisClusterHashStrategy;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Command\RawCommand;
|
||||
|
||||
/**
|
||||
* Abstraction for a Redis-backed cluster of nodes (Redis >= 3.0.0).
|
||||
*
|
||||
* This connection backend offers smart support for redis-cluster by handling
|
||||
* automatic slots map (re)generation upon -MOVE or -ASK responses returned by
|
||||
* Redis when redirecting a client to a different node.
|
||||
*
|
||||
* The cluster can be pre-initialized using only a subset of the actual nodes in
|
||||
* the cluster, Predis will do the rest by adjusting the slots map and creating
|
||||
* the missing underlying connection instances on the fly.
|
||||
*
|
||||
* It is possible to pre-associate connections to a slots range with the "slots"
|
||||
* parameter in the form "$first-$last". This can greatly reduce runtime node
|
||||
* guessing and redirections.
|
||||
*
|
||||
* It is also possible to ask for the full and updated slots map directly to one
|
||||
* of the nodes and optionally enable such a behaviour upon -MOVED redirections.
|
||||
* Asking for the cluster configuration to Redis is actually done by issuing a
|
||||
* CLUSTER NODES command to a random node in the pool.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class RedisCluster implements ClusterConnectionInterface, IteratorAggregate, Countable
|
||||
{
|
||||
private $askClusterNodes = false;
|
||||
private $defaultParameters = array();
|
||||
private $pool = array();
|
||||
private $slots = array();
|
||||
private $slotsMap;
|
||||
private $strategy;
|
||||
private $connections;
|
||||
|
||||
/**
|
||||
* @param ConnectionFactoryInterface $connections Connection factory object.
|
||||
*/
|
||||
public function __construct(ConnectionFactoryInterface $connections = null)
|
||||
{
|
||||
$this->strategy = new RedisClusterHashStrategy();
|
||||
$this->connections = $connections ?: new ConnectionFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isConnected()
|
||||
{
|
||||
foreach ($this->pool as $connection) {
|
||||
if ($connection->isConnected()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if ($connection = $this->getRandomConnection()) {
|
||||
$connection->connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
foreach ($this->pool as $connection) {
|
||||
$connection->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function add(SingleConnectionInterface $connection)
|
||||
{
|
||||
$this->pool[(string) $connection] = $connection;
|
||||
unset($this->slotsMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function remove(SingleConnectionInterface $connection)
|
||||
{
|
||||
if (false !== $id = array_search($connection, $this->pool, true)) {
|
||||
unset(
|
||||
$this->pool[$id],
|
||||
$this->slotsMap
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection instance by using its identifier.
|
||||
*
|
||||
* @param string $connectionID Connection identifier.
|
||||
* @return bool True if the connection was in the pool.
|
||||
*/
|
||||
public function removeById($connectionID)
|
||||
{
|
||||
if (isset($this->pool[$connectionID])) {
|
||||
unset(
|
||||
$this->pool[$connectionID],
|
||||
$this->slotsMap
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the current slots map by guessing the cluster configuration out
|
||||
* of the connection parameters of the connections in the pool.
|
||||
*
|
||||
* Generation is based on the same algorithm used by Redis to generate the
|
||||
* cluster, so it is most effective when all of the connections supplied on
|
||||
* initialization have the "slots" parameter properly set accordingly to the
|
||||
* current cluster configuration.
|
||||
*/
|
||||
public function buildSlotsMap()
|
||||
{
|
||||
$this->slotsMap = array();
|
||||
|
||||
foreach ($this->pool as $connectionID => $connection) {
|
||||
$parameters = $connection->getParameters();
|
||||
|
||||
if (!isset($parameters->slots)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slots = explode('-', $parameters->slots, 2);
|
||||
$this->setSlots($slots[0], $slots[1], $connectionID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the current slots map by fetching the cluster configuration to
|
||||
* one of the nodes by leveraging the CLUSTER NODES command.
|
||||
*/
|
||||
public function askClusterNodes()
|
||||
{
|
||||
if (!$connection = $this->getRandomConnection()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$cmdCluster = RawCommand::create('CLUSTER', 'NODES');
|
||||
$response = $connection->executeCommand($cmdCluster);
|
||||
|
||||
$nodes = explode("\n", $response, -1);
|
||||
$count = count($nodes);
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$node = explode(' ', $nodes[$i], 9);
|
||||
$slots = explode('-', $node[8], 2);
|
||||
|
||||
if ($node[1] === ':0') {
|
||||
$this->setSlots($slots[0], $slots[1], (string) $connection);
|
||||
} else {
|
||||
$this->setSlots($slots[0], $slots[1], $node[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current slots map for the cluster.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSlotsMap()
|
||||
{
|
||||
if (!isset($this->slotsMap)) {
|
||||
$this->slotsMap = array();
|
||||
}
|
||||
|
||||
return $this->slotsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-associates a connection to a slots range to avoid runtime guessing.
|
||||
*
|
||||
* @param int $first Initial slot of the range.
|
||||
* @param int $last Last slot of the range.
|
||||
* @param SingleConnectionInterface|string $connection ID or connection instance.
|
||||
*/
|
||||
public function setSlots($first, $last, $connection)
|
||||
{
|
||||
if ($first < 0x0000 || $first > 0x3FFF ||
|
||||
$last < 0x0000 || $last > 0x3FFF ||
|
||||
$last < $first
|
||||
) {
|
||||
throw new OutOfBoundsException(
|
||||
"Invalid slot range for $connection: [$first-$last]"
|
||||
);
|
||||
}
|
||||
|
||||
$slots = array_fill($first, $last - $first + 1, (string) $connection);
|
||||
$this->slotsMap = $this->getSlotsMap() + $slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guesses the correct node associated to a given slot using a precalculated
|
||||
* slots map, falling back to the same logic used by Redis to initialize a
|
||||
* cluster (best-effort).
|
||||
*
|
||||
* @param int $slot Slot index.
|
||||
* @return string Connection ID.
|
||||
*/
|
||||
protected function guessNode($slot)
|
||||
{
|
||||
if (!isset($this->slotsMap)) {
|
||||
$this->buildSlotsMap();
|
||||
}
|
||||
|
||||
if (isset($this->slotsMap[$slot])) {
|
||||
return $this->slotsMap[$slot];
|
||||
}
|
||||
|
||||
$count = count($this->pool);
|
||||
$index = min((int) ($slot / (int) (16384 / $count)), $count - 1);
|
||||
$nodes = array_keys($this->pool);
|
||||
|
||||
return $nodes[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new connection instance from the given connection ID.
|
||||
*
|
||||
* @param string $connectionID Identifier for the connection.
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
protected function createConnection($connectionID)
|
||||
{
|
||||
$host = explode(':', $connectionID, 2);
|
||||
|
||||
$parameters = array_merge($this->defaultParameters, array(
|
||||
'host' => $host[0],
|
||||
'port' => $host[1],
|
||||
));
|
||||
|
||||
$connection = $this->connections->create($parameters);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConnection(CommandInterface $command)
|
||||
{
|
||||
$hash = $this->strategy->getHash($command);
|
||||
|
||||
if (!isset($hash)) {
|
||||
throw new NotSupportedException(
|
||||
"Cannot use {$command->getId()} with redis-cluster"
|
||||
);
|
||||
}
|
||||
|
||||
$slot = $hash & 0x3FFF;
|
||||
|
||||
if (isset($this->slots[$slot])) {
|
||||
return $this->slots[$slot];
|
||||
} else {
|
||||
return $this->getConnectionBySlot($slot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection currently associated to a given slot.
|
||||
*
|
||||
* @param int $slot Slot index.
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getConnectionBySlot($slot)
|
||||
{
|
||||
if ($slot < 0x0000 || $slot > 0x3FFF) {
|
||||
throw new OutOfBoundsException("Invalid slot [$slot]");
|
||||
}
|
||||
|
||||
if (isset($this->slots[$slot])) {
|
||||
return $this->slots[$slot];
|
||||
}
|
||||
|
||||
$connectionID = $this->guessNode($slot);
|
||||
|
||||
if (!$connection = $this->getConnectionById($connectionID)) {
|
||||
$connection = $this->createConnection($connectionID);
|
||||
$this->pool[$connectionID] = $connection;
|
||||
}
|
||||
|
||||
return $this->slots[$slot] = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConnectionById($connectionID)
|
||||
{
|
||||
if (isset($this->pool[$connectionID])) {
|
||||
return $this->pool[$connectionID];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random connection from the pool.
|
||||
*
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
protected function getRandomConnection()
|
||||
{
|
||||
if ($this->pool) {
|
||||
return $this->pool[array_rand($this->pool)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently associates the connection instance to a new slot.
|
||||
* The connection is added to the connections pool if not yet included.
|
||||
*
|
||||
* @param SingleConnectionInterface $connection Connection instance.
|
||||
* @param int $slot Target slot index.
|
||||
*/
|
||||
protected function move(SingleConnectionInterface $connection, $slot)
|
||||
{
|
||||
$this->pool[(string) $connection] = $connection;
|
||||
$this->slots[(int) $slot] = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles -ERR responses returned by Redis.
|
||||
*
|
||||
* @param CommandInterface $command Command that generated the -ERR response.
|
||||
* @param ResponseErrorInterface $error Redis error response object.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function onErrorResponse(CommandInterface $command, ResponseErrorInterface $error)
|
||||
{
|
||||
$details = explode(' ', $error->getMessage(), 2);
|
||||
|
||||
switch ($details[0]) {
|
||||
case 'MOVED':
|
||||
return $this->onMovedResponse($command, $details[1]);
|
||||
|
||||
case 'ASK':
|
||||
return $this->onAskResponse($command, $details[1]);
|
||||
|
||||
default:
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles -MOVED responses by executing again the command against the node
|
||||
* indicated by the Redis response.
|
||||
*
|
||||
* @param CommandInterface $command Command that generated the -MOVED response.
|
||||
* @param string $details Parameters of the -MOVED response.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function onMovedResponse(CommandInterface $command, $details)
|
||||
{
|
||||
list($slot, $connectionID) = explode(' ', $details, 2);
|
||||
|
||||
if (!$connection = $this->getConnectionById($connectionID)) {
|
||||
$connection = $this->createConnection($connectionID);
|
||||
}
|
||||
|
||||
if ($this->askClusterNodes) {
|
||||
$this->askClusterNodes();
|
||||
}
|
||||
|
||||
$this->move($connection, $slot);
|
||||
$response = $this->executeCommand($command);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles -ASK responses by executing again the command against the node
|
||||
* indicated by the Redis response.
|
||||
*
|
||||
* @param CommandInterface $command Command that generated the -ASK response.
|
||||
* @param string $details Parameters of the -ASK response.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function onAskResponse(CommandInterface $command, $details)
|
||||
{
|
||||
list($slot, $connectionID) = explode(' ', $details, 2);
|
||||
|
||||
if (!$connection = $this->getConnectionById($connectionID)) {
|
||||
$connection = $this->createConnection($connectionID);
|
||||
}
|
||||
|
||||
$connection->executeCommand(RawCommand::create('ASKING'));
|
||||
$response = $connection->executeCommand($command);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$this->getConnection($command)->writeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readResponse(CommandInterface $command)
|
||||
{
|
||||
return $this->getConnection($command)->readResponse($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeCommand(CommandInterface $command)
|
||||
{
|
||||
$connection = $this->getConnection($command);
|
||||
$response = $connection->executeCommand($command);
|
||||
|
||||
if ($response instanceof ResponseErrorInterface) {
|
||||
return $this->onErrorResponse($command, $response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator(array_values($this->pool));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying hash strategy used to hash commands by their keys.
|
||||
*
|
||||
* @return CommandHashStrategyInterface
|
||||
*/
|
||||
public function getCommandHashStrategy()
|
||||
{
|
||||
return $this->strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables automatic fetching of the current slots map from one of the nodes
|
||||
* using the CLUSTER NODES command. This option is disabled by default but
|
||||
* asking the current slots map to Redis upon -MOVE responses may reduce
|
||||
* overhead by eliminating the trial-and-error nature of the node guessing
|
||||
* procedure, mostly when targeting many keys that would end up in a lot of
|
||||
* redirections.
|
||||
*
|
||||
* The slots map can still be manually fetched using the askClusterNodes()
|
||||
* method whether or not this option is enabled.
|
||||
*
|
||||
* @param bool $value Enable or disable the use of CLUSTER NODES.
|
||||
*/
|
||||
public function enableClusterNodes($value)
|
||||
{
|
||||
$this->askClusterNodes = (bool) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a default array of connection parameters to be applied when creating
|
||||
* new connection instances on the fly when they are not part of the initial
|
||||
* pool supplied upon cluster initialization.
|
||||
*
|
||||
* These parameters are not applied to connections added to the pool using
|
||||
* the add() method.
|
||||
*
|
||||
* @param array $parameters Array of connection parameters.
|
||||
*/
|
||||
public function setDefaultParameters(array $parameters)
|
||||
{
|
||||
$this->defaultParameters = array_merge(
|
||||
$this->defaultParameters,
|
||||
$parameters ?: array()
|
||||
);
|
||||
}
|
||||
}
|
||||
48
Predis/Connection/ReplicationConnectionInterface.php
Normal file
48
Predis/Connection/ReplicationConnectionInterface.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
/**
|
||||
* Defines a group of Redis servers in a master/slave replication configuration.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface ReplicationConnectionInterface extends AggregatedConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Switches the internal connection object being used.
|
||||
*
|
||||
* @param string $connection Alias of a connection
|
||||
*/
|
||||
public function switchTo($connection);
|
||||
|
||||
/**
|
||||
* Retrieves the connection object currently being used.
|
||||
*
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getCurrent();
|
||||
|
||||
/**
|
||||
* Retrieves the connection object to the master Redis server.
|
||||
*
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getMaster();
|
||||
|
||||
/**
|
||||
* Retrieves a list of connection objects to slaves Redis servers.
|
||||
*
|
||||
* @return SingleConnectionInterface
|
||||
*/
|
||||
public function getSlaves();
|
||||
}
|
||||
58
Predis/Connection/SingleConnectionInterface.php
Normal file
58
Predis/Connection/SingleConnectionInterface.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\Command\CommandInterface;
|
||||
|
||||
/**
|
||||
* Defines a connection object used to communicate with a single Redis server.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface SingleConnectionInterface extends ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Returns a string representation of the connection.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
|
||||
/**
|
||||
* Returns the underlying resource used to communicate with a Redis server.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getResource();
|
||||
|
||||
/**
|
||||
* Gets the parameters used to initialize the connection object.
|
||||
*
|
||||
* @return ConnectionParametersInterface
|
||||
*/
|
||||
public function getParameters();
|
||||
|
||||
/**
|
||||
* Pushes the instance of a Redis command to the queue of commands executed
|
||||
* when the actual connection to a server is estabilished.
|
||||
*
|
||||
* @param CommandInterface $command Instance of a Redis command.
|
||||
*/
|
||||
public function pushInitCommand(CommandInterface $command);
|
||||
|
||||
/**
|
||||
* Reads a reply from the server.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function read();
|
||||
}
|
||||
307
Predis/Connection/StreamConnection.php
Normal file
307
Predis/Connection/StreamConnection.php
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\ResponseError;
|
||||
use Predis\ResponseQueued;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Iterator\MultiBulkResponseSimple;
|
||||
|
||||
/**
|
||||
* Standard connection to Redis servers implemented on top of PHP's streams.
|
||||
* The connection parameters supported by this class are:
|
||||
*
|
||||
* - scheme: it can be either 'tcp' or 'unix'.
|
||||
* - host: hostname or IP address of the server.
|
||||
* - port: TCP port of the server.
|
||||
* - path: path of a UNIX domain socket when scheme is 'unix'.
|
||||
* - timeout: timeout to perform the connection.
|
||||
* - read_write_timeout: timeout of read / write operations.
|
||||
* - async_connect: performs the connection asynchronously.
|
||||
* - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
|
||||
* - persistent: the connection is left intact after a GC collection.
|
||||
* - iterable_multibulk: multibulk replies treated as iterable objects.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class StreamConnection extends AbstractConnection
|
||||
{
|
||||
private $mbiterable;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$this->mbiterable = (bool) $parameters->iterable_multibulk;
|
||||
|
||||
parent::__construct($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the server and destroys the underlying resource when
|
||||
* PHP's garbage collector kicks in only if the connection has not been
|
||||
* marked as persistent.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (isset($this->parameters) && !$this->parameters->persistent) {
|
||||
$this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createResource()
|
||||
{
|
||||
$parameters = $this->parameters;
|
||||
$initializer = "{$parameters->scheme}StreamInitializer";
|
||||
|
||||
return $this->$initializer($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a TCP stream resource.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @return resource
|
||||
*/
|
||||
private function tcpStreamInitializer(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$uri = "tcp://{$parameters->host}:{$parameters->port}";
|
||||
$flags = STREAM_CLIENT_CONNECT;
|
||||
|
||||
if (isset($parameters->async_connect) && $parameters->async_connect) {
|
||||
$flags |= STREAM_CLIENT_ASYNC_CONNECT;
|
||||
}
|
||||
|
||||
if (isset($parameters->persistent) && $parameters->persistent) {
|
||||
$flags |= STREAM_CLIENT_PERSISTENT;
|
||||
$uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path";
|
||||
}
|
||||
|
||||
$resource = @stream_socket_client($uri, $errno, $errstr, $parameters->timeout, $flags);
|
||||
|
||||
if (!$resource) {
|
||||
$this->onConnectionError(trim($errstr), $errno);
|
||||
}
|
||||
|
||||
if (isset($parameters->read_write_timeout)) {
|
||||
$rwtimeout = $parameters->read_write_timeout;
|
||||
$rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
|
||||
$timeoutSeconds = floor($rwtimeout);
|
||||
$timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
|
||||
stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
|
||||
}
|
||||
|
||||
if (isset($parameters->tcp_nodelay) && version_compare(PHP_VERSION, '5.4.0') >= 0) {
|
||||
$socket = socket_import_stream($resource);
|
||||
socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a UNIX stream resource.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @return resource
|
||||
*/
|
||||
private function unixStreamInitializer(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$uri = "unix://{$parameters->path}";
|
||||
$flags = STREAM_CLIENT_CONNECT;
|
||||
|
||||
if ($parameters->persistent) {
|
||||
$flags |= STREAM_CLIENT_PERSISTENT;
|
||||
}
|
||||
|
||||
$resource = @stream_socket_client($uri, $errno, $errstr, $parameters->timeout, $flags);
|
||||
|
||||
if (!$resource) {
|
||||
$this->onConnectionError(trim($errstr), $errno);
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
parent::connect();
|
||||
|
||||
if ($this->initCmds) {
|
||||
$this->sendInitializationCommands();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->isConnected()) {
|
||||
fclose($this->getResource());
|
||||
parent::disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the initialization commands to Redis when the connection is opened.
|
||||
*/
|
||||
private function sendInitializationCommands()
|
||||
{
|
||||
foreach ($this->initCmds as $command) {
|
||||
$this->writeCommand($command);
|
||||
}
|
||||
foreach ($this->initCmds as $command) {
|
||||
$this->readResponse($command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a write operation on the stream of the buffer containing a
|
||||
* command serialized with the Redis wire protocol.
|
||||
*
|
||||
* @param string $buffer Redis wire protocol representation of a command.
|
||||
*/
|
||||
protected function writeBytes($buffer)
|
||||
{
|
||||
$socket = $this->getResource();
|
||||
|
||||
while (($length = strlen($buffer)) > 0) {
|
||||
$written = fwrite($socket, $buffer);
|
||||
|
||||
if ($length === $written) {
|
||||
return;
|
||||
}
|
||||
if ($written === false || $written === 0) {
|
||||
$this->onConnectionError('Error while writing bytes to the server');
|
||||
}
|
||||
|
||||
$buffer = substr($buffer, $written);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$socket = $this->getResource();
|
||||
$chunk = fgets($socket);
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->onConnectionError('Error while reading line from the server');
|
||||
}
|
||||
|
||||
$prefix = $chunk[0];
|
||||
$payload = substr($chunk, 1, -2);
|
||||
|
||||
switch ($prefix) {
|
||||
case '+':
|
||||
switch ($payload) {
|
||||
case 'OK':
|
||||
return true;
|
||||
|
||||
case 'QUEUED':
|
||||
return new ResponseQueued();
|
||||
|
||||
default:
|
||||
return $payload;
|
||||
}
|
||||
|
||||
case '$':
|
||||
$size = (int) $payload;
|
||||
if ($size === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bulkData = '';
|
||||
$bytesLeft = ($size += 2);
|
||||
|
||||
do {
|
||||
$chunk = fread($socket, min($bytesLeft, 4096));
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->onConnectionError('Error while reading bytes from the server');
|
||||
}
|
||||
|
||||
$bulkData .= $chunk;
|
||||
$bytesLeft = $size - strlen($bulkData);
|
||||
} while ($bytesLeft > 0);
|
||||
|
||||
return substr($bulkData, 0, -2);
|
||||
|
||||
case '*':
|
||||
$count = (int) $payload;
|
||||
|
||||
if ($count === -1) {
|
||||
return null;
|
||||
}
|
||||
if ($this->mbiterable) {
|
||||
return new MultiBulkResponseSimple($this, $count);
|
||||
}
|
||||
|
||||
$multibulk = array();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$multibulk[$i] = $this->read();
|
||||
}
|
||||
|
||||
return $multibulk;
|
||||
|
||||
case ':':
|
||||
return (int) $payload;
|
||||
|
||||
case '-':
|
||||
return new ResponseError($payload);
|
||||
|
||||
default:
|
||||
$this->onProtocolError("Unknown prefix: '$prefix'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$commandId = $command->getId();
|
||||
$arguments = $command->getArguments();
|
||||
|
||||
$cmdlen = strlen($commandId);
|
||||
$reqlen = count($arguments) + 1;
|
||||
|
||||
$buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandId}\r\n";
|
||||
|
||||
for ($i = 0, $reqlen--; $i < $reqlen; $i++) {
|
||||
$argument = $arguments[$i];
|
||||
$arglen = strlen($argument);
|
||||
$buffer .= "\${$arglen}\r\n{$argument}\r\n";
|
||||
}
|
||||
|
||||
$this->writeBytes($buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array_merge(parent::__sleep(), array('mbiterable'));
|
||||
}
|
||||
}
|
||||
335
Predis/Connection/WebdisConnection.php
Normal file
335
Predis/Connection/WebdisConnection.php
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Connection;
|
||||
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\ResponseError;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Connection\ConnectionException;
|
||||
use Predis\Protocol\ProtocolException;
|
||||
|
||||
/**
|
||||
* This class implements a Predis connection that actually talks with Webdis
|
||||
* instead of connecting directly to Redis. It relies on the cURL extension to
|
||||
* communicate with the web server and the phpiredis extension to parse the
|
||||
* protocol of the replies returned in the http response bodies.
|
||||
*
|
||||
* Some features are not yet available or they simply cannot be implemented:
|
||||
* - Pipelining commands.
|
||||
* - Publish / Subscribe.
|
||||
* - MULTI / EXEC transactions (not yet supported by Webdis).
|
||||
*
|
||||
* The connection parameters supported by this class are:
|
||||
*
|
||||
* - scheme: must be 'http'.
|
||||
* - host: hostname or IP address of the server.
|
||||
* - port: TCP port of the server.
|
||||
* - timeout: timeout to perform the connection.
|
||||
* - user: username for authentication.
|
||||
* - pass: password for authentication.
|
||||
*
|
||||
* @link http://webd.is
|
||||
* @link http://github.com/nicolasff/webdis
|
||||
* @link http://github.com/seppo0010/phpiredis
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class WebdisConnection implements SingleConnectionInterface
|
||||
{
|
||||
const ERR_MSG_EXTENSION = 'The %s extension must be loaded in order to be able to use this connection class';
|
||||
|
||||
private $parameters;
|
||||
private $resource;
|
||||
private $reader;
|
||||
|
||||
/**
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
*/
|
||||
public function __construct(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$this->checkExtensions();
|
||||
|
||||
if ($parameters->scheme !== 'http') {
|
||||
throw new \InvalidArgumentException("Invalid scheme: {$parameters->scheme}");
|
||||
}
|
||||
|
||||
$this->parameters = $parameters;
|
||||
$this->resource = $this->initializeCurl($parameters);
|
||||
$this->reader = $this->initializeReader($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the underlying cURL and protocol reader resources when PHP's
|
||||
* garbage collector kicks in.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
curl_close($this->resource);
|
||||
phpiredis_reader_destroy($this->reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method used to throw on unsupported methods.
|
||||
*/
|
||||
private function throwNotSupportedException($function)
|
||||
{
|
||||
$class = __CLASS__;
|
||||
throw new NotSupportedException("The method $class::$function() is not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cURL and phpiredis extensions are loaded in PHP.
|
||||
*/
|
||||
private function checkExtensions()
|
||||
{
|
||||
if (!function_exists('curl_init')) {
|
||||
throw new NotSupportedException(sprintf(self::ERR_MSG_EXTENSION, 'curl'));
|
||||
}
|
||||
|
||||
if (!function_exists('phpiredis_reader_create')) {
|
||||
throw new NotSupportedException(sprintf(self::ERR_MSG_EXTENSION, 'phpiredis'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes cURL.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @return resource
|
||||
*/
|
||||
private function initializeCurl(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$options = array(
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_CONNECTTIMEOUT_MS => $parameters->timeout * 1000,
|
||||
CURLOPT_URL => "{$parameters->scheme}://{$parameters->host}:{$parameters->port}",
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_WRITEFUNCTION => array($this, 'feedReader'),
|
||||
);
|
||||
|
||||
if (isset($parameters->user, $parameters->pass)) {
|
||||
$options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}";
|
||||
}
|
||||
|
||||
curl_setopt_array($resource = curl_init(), $options);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes phpiredis' protocol reader.
|
||||
*
|
||||
* @param ConnectionParametersInterface $parameters Parameters used to initialize the connection.
|
||||
* @return resource
|
||||
*/
|
||||
private function initializeReader(ConnectionParametersInterface $parameters)
|
||||
{
|
||||
$reader = phpiredis_reader_create();
|
||||
|
||||
phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
|
||||
phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
|
||||
|
||||
return $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler used by the protocol reader to handle status replies.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function getStatusHandler()
|
||||
{
|
||||
return function ($payload) {
|
||||
return $payload === 'OK' ? true : $payload;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler used by the protocol reader to handle Redis errors.
|
||||
*
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function getErrorHandler()
|
||||
{
|
||||
return function ($errorMessage) {
|
||||
return new ResponseError($errorMessage);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Feeds phpredis' reader resource with the data read from the network.
|
||||
*
|
||||
* @param resource $resource Reader resource.
|
||||
* @param string $buffer Buffer with the reply read from the network.
|
||||
* @return int
|
||||
*/
|
||||
protected function feedReader($resource, $buffer)
|
||||
{
|
||||
phpiredis_reader_feed($this->reader, $buffer);
|
||||
|
||||
return strlen($buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
// NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
// NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isConnected()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified command is supported by this connection class.
|
||||
*
|
||||
* @param CommandInterface $command The instance of a Redis command.
|
||||
* @return string
|
||||
*/
|
||||
protected function getCommandId(CommandInterface $command)
|
||||
{
|
||||
switch (($commandId = $command->getId())) {
|
||||
case 'AUTH':
|
||||
case 'SELECT':
|
||||
case 'MULTI':
|
||||
case 'EXEC':
|
||||
case 'WATCH':
|
||||
case 'UNWATCH':
|
||||
case 'DISCARD':
|
||||
case 'MONITOR':
|
||||
throw new NotSupportedException("Disabled command: {$command->getId()}");
|
||||
|
||||
default:
|
||||
return $commandId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function writeCommand(CommandInterface $command)
|
||||
{
|
||||
$this->throwNotSupportedException(__FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readResponse(CommandInterface $command)
|
||||
{
|
||||
$this->throwNotSupportedException(__FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeCommand(CommandInterface $command)
|
||||
{
|
||||
$resource = $this->resource;
|
||||
$commandId = $this->getCommandId($command);
|
||||
|
||||
if ($arguments = $command->getArguments()) {
|
||||
$arguments = implode('/', array_map('urlencode', $arguments));
|
||||
$serializedCommand = "$commandId/$arguments.raw";
|
||||
} else {
|
||||
$serializedCommand = "$commandId.raw";
|
||||
}
|
||||
|
||||
curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand);
|
||||
|
||||
if (curl_exec($resource) === false) {
|
||||
$error = curl_error($resource);
|
||||
$errno = curl_errno($resource);
|
||||
throw new ConnectionException($this, trim($error), $errno);
|
||||
}
|
||||
|
||||
if (phpiredis_reader_get_state($this->reader) !== PHPIREDIS_READER_STATE_COMPLETE) {
|
||||
throw new ProtocolException($this, phpiredis_reader_get_error($this->reader));
|
||||
}
|
||||
|
||||
return phpiredis_reader_get_reply($this->reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function pushInitCommand(CommandInterface $command)
|
||||
{
|
||||
$this->throwNotSupportedException(__FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
$this->throwNotSupportedException(__FUNCTION__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return "{$this->parameters->host}:{$this->parameters->port}";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return array('parameters');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->checkExtensions();
|
||||
$parameters = $this->getParameters();
|
||||
|
||||
$this->resource = $this->initializeCurl($parameters);
|
||||
$this->reader = $this->initializeReader($parameters);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue