mirror of
https://github.com/predis/predis.git
synced 2026-08-19 19:51:50 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 135fd5004b | |||
| c25a815317 | |||
| 39a9fc2ddd | |||
| f5bca54bba | |||
| 364aa180f9 | |||
| 4da506cd9e | |||
| ef5034dc72 | |||
| 994f045a4a | |||
| 8cd8d4e580 | |||
| 22f1aafae8 | |||
| c47b009b69 | |||
| 2cff6f0886 | |||
| 63cb150fc2 | |||
| 466c666547 | |||
| 8d5c794efe | |||
| e3fba5d4b7 | |||
| a8400f2ed8 | |||
| 44629a33db | |||
| d38f164d3f | |||
| 9bc5685959 | |||
| 565690a5b1 | |||
| bdbbe18e6c | |||
| 78027f0498 | |||
| 104cd1eae7 | |||
| 5148ce16c6 | |||
| 4b9ab0bcce | |||
| 39ed8e3e8f | |||
| 6f4347010d | |||
| c43278eceb |
@@ -1,2 +1,5 @@
|
||||
*.tgz
|
||||
*.phar
|
||||
phpunit.xml
|
||||
package.xml
|
||||
experiments/
|
||||
|
||||
+20
-1
@@ -1,3 +1,22 @@
|
||||
v0.7.1 (2011-12-27)
|
||||
===============================================================================
|
||||
|
||||
- The PEAR channel on PearHub has been deprecated in favour of `pear.nrk.io`.
|
||||
|
||||
- Miscellaneous minor fixes.
|
||||
|
||||
- Added transparent support for master / slave replication configurations where
|
||||
write operations are performed on the master server and read operations are
|
||||
routed to one of the slaves. Please refer to ISSUE #21 for a bit of history
|
||||
and more details about replication support in Predis.
|
||||
|
||||
- The `profile` client option now accepts a callable object used to initialize
|
||||
a new instance of `Predis\Profiles\IServerProfile`.
|
||||
|
||||
- Exposed a method for MULTI / EXEC contexts that adds the ability to execute
|
||||
instances of Redis commands against transaction objects.
|
||||
|
||||
|
||||
v0.7.0 (2011-12-11)
|
||||
===============================================================================
|
||||
|
||||
@@ -66,7 +85,7 @@ v0.7.0 (2011-12-11)
|
||||
thus removed. Serialization of commands is now a competence of connections.
|
||||
|
||||
- The `Predis\IConnection` interface has been splitted into two new interfaces:
|
||||
`Predis\Network\IConnectionSingle and `Predis\Network\IConnectionCluster`.
|
||||
`Predis\Network\IConnectionSingle` and `Predis\Network\IConnectionCluster`.
|
||||
|
||||
- The constructor of `Predis\Client` now accepts more type of arguments such as
|
||||
instances of `Predis\IConnectionParameters` and `Predis\Network\IConnection`.
|
||||
|
||||
@@ -13,6 +13,7 @@ project,
|
||||
- Complete support for Redis from __1.2__ to __2.4__ and the current development versions using different
|
||||
server profiles.
|
||||
- Client-side sharding with support for consistent hashing or custom distribution strategies.
|
||||
- Support for master / slave replication configurations (write on master, read from slaves).
|
||||
- Command pipelining on single and aggregated connections.
|
||||
- Transparent key prefixing strategy capable of handling any command known that has keys in its arguments.
|
||||
- Abstraction for Redis transactions (Redis >= 2.0) with support for CAS operations (Redis >= 2.2).
|
||||
@@ -27,9 +28,9 @@ project,
|
||||
Predis is available on [Packagist](http://packagist.org/packages/predis/predis) for an easy installation
|
||||
using [Composer](http://packagist.org/about-composer). Composer helps you manage dependencies for your
|
||||
projects and libraries without much hassle which makes it the preferred way to get up and running with
|
||||
new applications. Alternatively, the library is available on [PearHub](http://pearhub.org/projects/predis)'s
|
||||
channel for a more traditional installation via PEAR. Zip and tar.gz archives are also downloadable from
|
||||
GitHub by browsing the list of [tagged releases](http://github.com/nrk/predis/tags).
|
||||
new applications. Alternatively, the library is available on our [own PEAR channel](http://pear.nrk.io)
|
||||
for a more traditional installation via PEAR. Zip and tar.gz archives are also downloadable from GitHub
|
||||
by browsing the list of [tagged releases](http://github.com/nrk/predis/tags).
|
||||
|
||||
|
||||
### Loading the library ###
|
||||
@@ -42,7 +43,8 @@ if you are going to use it in a project or script without any PSR-0 compliant au
|
||||
|
||||
``` php
|
||||
<?php
|
||||
require PREDIS_BASE_PATH . '/Autoloader.php';
|
||||
// prepend a base path if Predis is not present in your "include_path".
|
||||
require 'Predis/Autoloader.php';
|
||||
|
||||
Predis\Autoloader::register();
|
||||
```
|
||||
@@ -53,7 +55,7 @@ stub defining an autoloader function for Predis, so you just need to require the
|
||||
the library.
|
||||
|
||||
Alternatively you can generate a single PHP file that holds every class, just like older versions of
|
||||
Predis, using the `bin/create-single-file.php` executable sript. In this way you can load Predis in your
|
||||
Predis, using the `bin/create-single-file.php` executable script. In this way you can load Predis in your
|
||||
scripts simply by using functions such as `require` and `include`, but this practice is not encouraged.
|
||||
|
||||
|
||||
@@ -197,6 +199,7 @@ other Git hosting provider of your preference.
|
||||
- [Source code](http://github.com/nrk/predis/)
|
||||
- [Wiki](http://wiki.github.com/nrk/predis/)
|
||||
- [Issue tracker](http://github.com/nrk/predis/issues)
|
||||
- [PEAR channel](http://pear.nrk.io)
|
||||
|
||||
### Related ###
|
||||
- [Redis](http://redis.io/)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
require 'SharedConfigurations.php';
|
||||
|
||||
// Predis supports master / slave replication scenarios where write operations are
|
||||
// performed on the master server and read operations are executed against one of
|
||||
// the slaves. The behaviour of commands or EVAL scripts can be customized at will.
|
||||
// As soon as a write operation is performed, all the subsequent requests (reads
|
||||
// or writes) will be served by the master server.
|
||||
//
|
||||
// This example must be executed with the second Redis server acting as the slave
|
||||
// of the first one using the SLAVEOF command.
|
||||
//
|
||||
|
||||
$parameters = array(
|
||||
'tcp://127.0.0.1:6379?database=15&alias=master',
|
||||
'tcp://127.0.0.1:6380?database=15&alias=slave',
|
||||
);
|
||||
|
||||
$options = array('replication' => true);
|
||||
|
||||
$client = new Predis\Client($parameters, $options);
|
||||
|
||||
// Read operation.
|
||||
$exists = $client->exists('foo') ? 'yes' : 'no';
|
||||
$current = $client->getConnection()->getCurrent()->getParameters();
|
||||
echo "Does 'foo' exist on {$current->alias}? $exists.\n";
|
||||
|
||||
// Write operation.
|
||||
$client->set('foo', 'bar');
|
||||
$current = $client->getConnection()->getCurrent()->getParameters();
|
||||
echo "Now 'foo' has been set to 'bar' on {$current->alias}!\n";
|
||||
|
||||
// Read operation.
|
||||
$bar = $client->get('foo');
|
||||
$current = $client->getConnection()->getCurrent()->getParameters();
|
||||
echo "We just fetched 'foo' from {$current->alias} and its value is '$bar'.\n";
|
||||
|
||||
/* OUTPUT:
|
||||
Does 'foo' exist on slave? yes.
|
||||
Now 'foo' has been set to 'bar' on master!
|
||||
We just fetched 'foo' from master and its value is 'bar'.
|
||||
*/
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
require 'SharedConfigurations.php';
|
||||
|
||||
// Predis allows to set Lua scripts as read-only operations in the context of
|
||||
// replication. This works for both EVAL and EVALSHA and also for the client-side
|
||||
// abstraction built upon them (Predis\Commands\ScriptedCommand). This example
|
||||
// shows a slightly more complex configuration that injects a new scripted command
|
||||
// in the server profile used by the new client instance and marks it marks it as
|
||||
// a read-only operation for replication so that it will be executed on slaves.
|
||||
|
||||
use Predis\Profiles\ServerProfile;
|
||||
use Predis\Commands\ScriptedCommand;
|
||||
use Predis\Network\MasterSlaveReplication;
|
||||
|
||||
// ------------------------------------------------------------------------- //
|
||||
|
||||
// Define a new scripted command that returns all the fields
|
||||
// of a variable number of hashes with a single roundtrip.
|
||||
|
||||
class HashMultipleGetAll extends ScriptedCommand {
|
||||
const BODY = <<<EOS
|
||||
local hashes = {}
|
||||
for _, key in pairs(KEYS) do
|
||||
table.insert(hashes, key)
|
||||
table.insert(hashes, redis.call('hgetall', key))
|
||||
end
|
||||
return hashes
|
||||
EOS;
|
||||
|
||||
public function getScript() {
|
||||
return self::BODY;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------- //
|
||||
|
||||
$parameters = array(
|
||||
'tcp://127.0.0.1:6379/?alias=master',
|
||||
'tcp://127.0.0.1:6380/?alias=slave',
|
||||
);
|
||||
|
||||
$options = array(
|
||||
'profile' => function($options) {
|
||||
$profile = ServerProfile::get('dev');
|
||||
$profile->defineCommand('hmgetall', 'HashMultipleGetAll');
|
||||
|
||||
return $profile;
|
||||
},
|
||||
'replication' => function($options) {
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->setScriptReadOnly(HashMultipleGetAll::BODY);
|
||||
|
||||
return $replication;
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------------- //
|
||||
|
||||
$client = new Predis\Client($parameters, $options);
|
||||
|
||||
// Execute the following commands on the master server using redis-cli:
|
||||
// $ ./redis-cli HMSET metavars foo bar hoge piyo
|
||||
// $ ./redis-cli HMSET servers master host1 slave host2
|
||||
|
||||
$hashes = $client->hmgetall('metavars', 'servers');
|
||||
|
||||
$replication = $client->getConnection();
|
||||
$stillOnSlave = $replication->getCurrent() === $replication->getConnectionById('slave');
|
||||
|
||||
echo "Is still on slave? ", $stillOnSlave ? 'YES' : 'NO', "!\n";
|
||||
var_export($hashes);
|
||||
+10
-4
@@ -29,7 +29,7 @@ use Predis\Transaction\MultiExecContext;
|
||||
*/
|
||||
class Client
|
||||
{
|
||||
const VERSION = '0.7.0';
|
||||
const VERSION = '0.7.1';
|
||||
|
||||
private $options;
|
||||
private $profile;
|
||||
@@ -90,8 +90,14 @@ class Client
|
||||
if ($parameters instanceof IConnection) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
if (is_array($parameters) && isset($parameters[0])) {
|
||||
return $this->connections->createCluster($this->options->cluster, $parameters, $this->profile);
|
||||
$replication = isset($this->options->replication) && $this->options->replication;
|
||||
|
||||
$connection = $this->options->{$replication ? 'replication' : 'cluster'};
|
||||
$initializer = $replication ? 'createReplication' : 'createCluster';
|
||||
|
||||
return $this->connections->$initializer($connection, $parameters, $this->profile);
|
||||
}
|
||||
|
||||
return $this->connections->create($parameters, $this->profile);
|
||||
@@ -190,8 +196,8 @@ class Client
|
||||
public function getConnection($id = null)
|
||||
{
|
||||
if (isset($id)) {
|
||||
if (!Helpers::isCluster($this->connection)) {
|
||||
$message = 'Retrieving connections by alias is supported only with clustered connections';
|
||||
if (!Helpers::isAggregated($this->connection)) {
|
||||
$message = 'Retrieving connections by alias is supported only with aggregated connections (cluster or replication)';
|
||||
throw new NotSupportedException($message);
|
||||
}
|
||||
return $this->connection->getConnectionById($id);
|
||||
|
||||
@@ -28,19 +28,21 @@ abstract class ScriptedCommand extends ServerEval
|
||||
public abstract function getScript();
|
||||
|
||||
/**
|
||||
* Gets the number of arguments that should be considered as keys.
|
||||
* Specifies the number of arguments that should be considered as keys.
|
||||
*
|
||||
* @todo Should we make a scripted command act by default as a variadic
|
||||
* command where the first argument is the key (KEYS[1]) and the
|
||||
* rest is the list of values (ARGV)?
|
||||
* The default behaviour for the base class is to return FALSE to indicate that
|
||||
* all the elements of the arguments array should be considered as keys, but
|
||||
* subclasses can enforce a static number of keys.
|
||||
*
|
||||
* @return int
|
||||
* @todo How about returning 1 by default to make scripted commands act like
|
||||
* variadic ones where the first argument is the key (KEYS[1]) and the
|
||||
* rest are values (ARGV)?
|
||||
*
|
||||
* @return int|Boolean
|
||||
*/
|
||||
public function getKeysCount()
|
||||
protected function getKeysCount()
|
||||
{
|
||||
// The default behaviour for the base class is to use all the arguments
|
||||
// passed to a scripted command to populate the KEYS table in Lua.
|
||||
return count($this->getArguments());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +60,8 @@ abstract class ScriptedCommand extends ServerEval
|
||||
*/
|
||||
protected function filterArguments(Array $arguments)
|
||||
{
|
||||
return array_merge(array($this->getScript(), $this->getKeysCount()), $arguments);
|
||||
$header = array($this->getScript(), ($keys = $this->getKeysCount()) !== false ? $keys : count($arguments));
|
||||
|
||||
return array_merge($header, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Predis;
|
||||
use Predis\Profiles\IServerProfile;
|
||||
use Predis\Network\IConnectionSingle;
|
||||
use Predis\Network\IConnectionCluster;
|
||||
use Predis\Network\IConnectionReplication;
|
||||
use Predis\Profiles\ServerProfile;
|
||||
|
||||
/**
|
||||
@@ -135,6 +136,18 @@ class ConnectionFactory implements IConnectionFactory
|
||||
return $cluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createReplication(IConnectionReplication $replication, $parameters, IServerProfile $profile = null)
|
||||
{
|
||||
foreach ($parameters as $node) {
|
||||
$replication->add($node instanceof IConnectionSingle ? $node : $this->create($node, $profile));
|
||||
}
|
||||
|
||||
return $replication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a connection object after its initialization.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Predis;
|
||||
|
||||
use Predis\Network\IConnection;
|
||||
use Predis\Network\IConnectionCluster;
|
||||
use Predis\Network\IConnectionReplication;
|
||||
|
||||
/**
|
||||
* Defines a few helper methods.
|
||||
@@ -21,6 +22,17 @@ use Predis\Network\IConnectionCluster;
|
||||
*/
|
||||
class Helpers
|
||||
{
|
||||
/**
|
||||
* Checks if the specified connection represents an aggregation of connections.
|
||||
*
|
||||
* @param IConnection $connection Connection object.
|
||||
* @return Boolean
|
||||
*/
|
||||
public static function isAggregated(IConnection $connection)
|
||||
{
|
||||
return $connection instanceof IConnectionCluster || $connection instanceof IConnectionReplication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified connection represents a cluster.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Predis;
|
||||
|
||||
use Predis\Profiles\IServerProfile;
|
||||
use Predis\Network\IConnectionCluster;
|
||||
use Predis\Network\IConnectionReplication;
|
||||
|
||||
/**
|
||||
* Interface that must be implemented by classes that provide their own mechanism
|
||||
@@ -53,4 +54,13 @@ interface IConnectionFactory
|
||||
* @return Predis\Network\IConnectionCluster
|
||||
*/
|
||||
public function createCluster(IConnectionCluster $cluster, $parameters, IServerProfile $profile = null);
|
||||
|
||||
/**
|
||||
* Prepares a master / slave replication configuration.
|
||||
*
|
||||
* @param IConnectionReplication Instance of a connection cluster class.
|
||||
* @param array $parameters List of parameters for each connection object.
|
||||
* @return Predis\Network\IConnectionReplication
|
||||
*/
|
||||
public function createReplication(IConnectionReplication $replication, $parameters, IServerProfile $profile = null);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Predis\Helpers;
|
||||
use Predis\IReplyObject;
|
||||
use Predis\IConnectionParameters;
|
||||
use Predis\ClientException;
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\Commands\ICommand;
|
||||
use Predis\Protocol\ProtocolException;
|
||||
|
||||
@@ -169,7 +170,7 @@ abstract class ConnectionBase implements IConnectionSingle
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle invalid connection parameters.
|
||||
* Helper method to handle not supported connection parameters.
|
||||
*
|
||||
* @param string $option Name of the option.
|
||||
* @param IConnectionParameters $parameters Parameters used to initialize the connection.
|
||||
@@ -181,7 +182,7 @@ abstract class ConnectionBase implements IConnectionSingle
|
||||
$message .= " [$parameters]";
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException($message);
|
||||
throw new NotSupportedException($message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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\Network;
|
||||
|
||||
use Predis\Commands\ICommand;
|
||||
|
||||
/**
|
||||
* Defines a group of Redis servers in a master/slave replication configuration.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
interface IConnectionReplication extends IConnection
|
||||
{
|
||||
/**
|
||||
* Adds a connection instance to the cluster.
|
||||
*
|
||||
* @param IConnectionSingle $connection Instance of a connection.
|
||||
*/
|
||||
public function add(IConnectionSingle $connection);
|
||||
|
||||
/**
|
||||
* Removes the specified connection instance from the cluster.
|
||||
*
|
||||
* @param IConnectionSingle $connection Instance of a connection.
|
||||
* @return Boolean Returns true if the connection was in the pool.
|
||||
*/
|
||||
public function remove(IConnectionSingle $connection);
|
||||
|
||||
/**
|
||||
* Gets the actual connection instance in charge of the specified command.
|
||||
*
|
||||
* @param ICommand $command Instance of a Redis command.
|
||||
* @return IConnectionSingle
|
||||
*/
|
||||
public function getConnection(ICommand $command);
|
||||
|
||||
/**
|
||||
* Retrieves a connection instance from the cluster using an alias.
|
||||
*
|
||||
* @param string $connectionId Alias of a connection
|
||||
* @return IConnectionSingle
|
||||
*/
|
||||
public function getConnectionById($connectionId);
|
||||
|
||||
/**
|
||||
* 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 IConnectionSingle
|
||||
*/
|
||||
public function getCurrent();
|
||||
|
||||
/**
|
||||
* Retrieves the connection object to the master Redis server.
|
||||
*
|
||||
* @return IConnectionSingle
|
||||
*/
|
||||
public function getMaster();
|
||||
|
||||
/**
|
||||
* Retrieves a list of connection objects to slaves Redis servers.
|
||||
*
|
||||
* @return IConnectionSingle
|
||||
*/
|
||||
public function getSlaves();
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
<?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\Network;
|
||||
|
||||
use Predis\Commands\ICommand;
|
||||
use Predis\NotSupportedException;
|
||||
|
||||
/**
|
||||
* Defines the standard virtual connection class that is used
|
||||
* by Predis to handle replication with a group of servers in
|
||||
* a master/slave configuration.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class MasterSlaveReplication implements IConnectionReplication
|
||||
{
|
||||
private $disallowed = array();
|
||||
private $readonly = array();
|
||||
private $readonlySHA1 = array();
|
||||
private $current = null;
|
||||
private $master = null;
|
||||
private $slaves = array();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->disallowed = $this->getDisallowedOperations();
|
||||
$this->readonly = $this->getReadOnlyOperations();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(IConnectionSingle $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(IConnectionSingle $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(ICommand $command)
|
||||
{
|
||||
if ($this->current === null) {
|
||||
$this->check();
|
||||
$this->current = $this->isReadOperation($command) ? $this->pickSlave() : $this->master;
|
||||
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
if ($this->current === $this->master) {
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
if (!$this->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 IConnectionSingle) {
|
||||
$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 a random slave.
|
||||
*
|
||||
* @return IConnectionSingle
|
||||
*/
|
||||
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(ICommand $command)
|
||||
{
|
||||
$this->getConnection($command)->writeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readResponse(ICommand $command)
|
||||
{
|
||||
return $this->getConnection($command)->readResponse($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeCommand(ICommand $command)
|
||||
{
|
||||
return $this->getConnection($command)->executeCommand($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the specified command performs a read-only operation
|
||||
* against a key stored on Redis.
|
||||
*
|
||||
* @param ICommand $command Instance of Redis command.
|
||||
* @return Boolean
|
||||
*/
|
||||
protected function isReadOperation(ICommand $command)
|
||||
{
|
||||
if (isset($this->disallowed[$id = $command->getId()])) {
|
||||
throw new NotSupportedException("The command $id is not allowed in replication mode");
|
||||
}
|
||||
|
||||
if (isset($this->readonly[$id])) {
|
||||
if (true === $readonly = $this->readonly[$id]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $readonly($command);
|
||||
}
|
||||
|
||||
if (($eval = $id === 'EVAL') || $id === 'EVALSHA') {
|
||||
$sha1 = $eval ? sha1($command->getArgument(0)) : $command->getArgument(0);
|
||||
|
||||
if (isset($this->readonlySHA1[$sha1])) {
|
||||
if (true === $readonly = $this->readonlySHA1[$sha1]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $readonly($command);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a command as a read-only operation. When the behaviour of a
|
||||
* command can be decided only at runtime depending on its arguments,
|
||||
* a callable object can be provided to dinamically check if the passed
|
||||
* instance of a command performs write operations or not.
|
||||
*
|
||||
* @param string $commandID ID of the command.
|
||||
* @param mixed $readonly A boolean or a callable object.
|
||||
*/
|
||||
public function setCommandReadOnly($commandID, $readonly = true)
|
||||
{
|
||||
$commandID = strtoupper($commandID);
|
||||
|
||||
if ($readonly) {
|
||||
$this->readonly[$commandID] = $readonly;
|
||||
}
|
||||
else {
|
||||
unset($this->readonly[$commandID]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a Lua script for EVAL and EVALSHA as a read-only operation. When
|
||||
* the behaviour of a script can be decided only at runtime depending on
|
||||
* its arguments, a callable object can be provided to dinamically check
|
||||
* if the passed instance of EVAL or EVALSHA performs write operations or
|
||||
* not.
|
||||
*
|
||||
* @param string $script Body of the Lua script.
|
||||
* @param mixed $readonly A boolean or a callable object.
|
||||
*/
|
||||
public function setScriptReadOnly($script, $readonly = true)
|
||||
{
|
||||
$sha1 = sha1($script);
|
||||
|
||||
if ($readonly) {
|
||||
$this->readonlySHA1[$sha1] = $readonly;
|
||||
}
|
||||
else {
|
||||
unset($this->readonlySHA1[$sha1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default list of disallowed commands.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDisallowedOperations()
|
||||
{
|
||||
return array(
|
||||
'SHUTDOWN' => true,
|
||||
'INFO' => true,
|
||||
'DBSIZE' => true,
|
||||
'LASTSAVE' => true,
|
||||
'CONFIG' => true,
|
||||
'MONITOR' => true,
|
||||
'SLAVEOF' => true,
|
||||
'SAVE' => true,
|
||||
'BGSAVE' => true,
|
||||
'BGREWRITEAOF' => true,
|
||||
'SLOWLOG' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default list of commands performing read-only operations.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getReadOnlyOperations()
|
||||
{
|
||||
return array(
|
||||
'EXISTS' => true,
|
||||
'TYPE' => true,
|
||||
'KEYS' => true,
|
||||
'RANDOMKEY' => true,
|
||||
'TTL' => true,
|
||||
'GET' => true,
|
||||
'MGET' => true,
|
||||
'SUBSTR' => true,
|
||||
'STRLEN' => true,
|
||||
'GETRANGE' => true,
|
||||
'GETBIT' => true,
|
||||
'LLEN' => true,
|
||||
'LRANGE' => true,
|
||||
'LINDEX' => true,
|
||||
'SCARD' => true,
|
||||
'SISMEMBER' => true,
|
||||
'SINTER' => true,
|
||||
'SUNION' => true,
|
||||
'SDIFF' => true,
|
||||
'SMEMBERS' => true,
|
||||
'SRANDMEMBER' => true,
|
||||
'ZRANGE' => true,
|
||||
'ZREVRANGE' => true,
|
||||
'ZRANGEBYSCORE' => true,
|
||||
'ZREVRANGEBYSCORE' => true,
|
||||
'ZCARD' => true,
|
||||
'ZSCORE' => true,
|
||||
'ZCOUNT' => true,
|
||||
'ZRANK' => true,
|
||||
'ZREVRANK' => true,
|
||||
'HGET' => true,
|
||||
'HMGET' => true,
|
||||
'HEXISTS' => true,
|
||||
'HLEN' => true,
|
||||
'HKEYS' => true,
|
||||
'HVELS' => true,
|
||||
'HGETALL' => true,
|
||||
'PING' => true,
|
||||
'AUTH' => true,
|
||||
'SELECT' => true,
|
||||
'ECHO' => true,
|
||||
'QUIT' => true,
|
||||
'OBJECT' => true,
|
||||
'SORT' => function(ICommand $command) {
|
||||
$arguments = $command->getArguments();
|
||||
return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE';
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ class ClientCluster extends Option
|
||||
default:
|
||||
// TODO: we should not even allow non-string values here.
|
||||
if (is_string($fqnOrType) && !class_exists($fqnOrType)) {
|
||||
throw new \InvalidArgumentException('Class $fqnOrType does not exist');
|
||||
throw new \InvalidArgumentException("Class $fqnOrType does not exist");
|
||||
}
|
||||
return function() use($fqnOrType) {
|
||||
return new $fqnOrType();
|
||||
|
||||
@@ -42,6 +42,7 @@ class ClientOptions implements IClientOptions
|
||||
'profile' => new ClientProfile(),
|
||||
'connections' => new ClientConnectionFactory(),
|
||||
'cluster' => new ClientCluster(),
|
||||
'replication' => new ClientReplication(),
|
||||
'prefix' => new ClientPrefix(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ class ClientProfile extends Option
|
||||
}
|
||||
}
|
||||
|
||||
if (is_callable($value)) {
|
||||
$value = call_user_func($value, $options);
|
||||
}
|
||||
|
||||
if (!$value instanceof IServerProfile) {
|
||||
throw new \InvalidArgumentException('Invalid value for the profile option');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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\Options;
|
||||
|
||||
use Predis\Network\IConnectionReplication;
|
||||
use Predis\Network\MasterSlaveReplication;
|
||||
|
||||
/**
|
||||
* Option class that returns a replication connection be used by a client.
|
||||
*
|
||||
* @author Daniele Alessandri <suppakilla@gmail.com>
|
||||
*/
|
||||
class ClientReplication extends Option
|
||||
{
|
||||
/**
|
||||
* Checks if the specified value is a valid instance of IConnectionReplication.
|
||||
*
|
||||
* @param IConnectionReplication $cluster Instance of a connection cluster.
|
||||
* @return IConnectionReplication
|
||||
*/
|
||||
protected function checkInstance($connection)
|
||||
{
|
||||
if (!$connection instanceof IConnectionReplication) {
|
||||
throw new \InvalidArgumentException('Instance of Predis\Network\IConnectionReplication expected');
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function filter(IClientOptions $options, $value)
|
||||
{
|
||||
if (is_callable($value)) {
|
||||
$connection = call_user_func($value, $options);
|
||||
if (!$connection instanceof IConnectionReplication) {
|
||||
throw new \InvalidArgumentException('Instance of Predis\Network\IConnectionReplication expected');
|
||||
}
|
||||
return $connection;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if (!class_exists($value)) {
|
||||
throw new \InvalidArgumentException("Class $value does not exist");
|
||||
}
|
||||
if (!($connection = new $value()) instanceof IConnectionReplication) {
|
||||
throw new \InvalidArgumentException('Instance of Predis\Network\IConnectionReplication expected');
|
||||
}
|
||||
return $connection;
|
||||
}
|
||||
|
||||
if ($value == true) {
|
||||
return $this->getDefault($options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefault(IClientOptions $options)
|
||||
{
|
||||
return new MasterSlaveReplication();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Predis\Client;
|
||||
use Predis\Helpers;
|
||||
use Predis\ClientException;
|
||||
use Predis\Commands\ICommand;
|
||||
use Predis\Network\IConnectionReplication;
|
||||
|
||||
/**
|
||||
* Abstraction of a pipeline context where write and read operations
|
||||
@@ -120,6 +121,13 @@ class PipelineContext
|
||||
if (count($this->pipeline) > 0) {
|
||||
if ($send) {
|
||||
$connection = $this->client->getConnection();
|
||||
|
||||
// TODO: it would be better to use a dedicated pipeline executor
|
||||
// for classes implementing master/slave replication.
|
||||
if ($connection instanceof IConnectionReplication) {
|
||||
$connection->switchTo('master');
|
||||
}
|
||||
|
||||
$replies = $this->executor->execute($connection, $this->pipeline);
|
||||
$this->replies = array_merge($this->replies, $replies);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use Predis\Helpers;
|
||||
use Predis\ResponseQueued;
|
||||
use Predis\ClientException;
|
||||
use Predis\ServerException;
|
||||
use Predis\Commands\ICommand;
|
||||
use Predis\NotSupportedException;
|
||||
use Predis\CommunicationException;
|
||||
use Predis\Protocol\ProtocolException;
|
||||
@@ -180,20 +181,31 @@ class MultiExecContext
|
||||
*
|
||||
* @param string $method Command ID.
|
||||
* @param array $arguments Arguments for the command.
|
||||
* @return MultiExecContext
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $arguments)
|
||||
{
|
||||
$command = $this->client->createCommand($method, $arguments);
|
||||
$response = $this->executeCommand($command);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the specified Redis command.
|
||||
*
|
||||
* @param ICommand $command A Redis command.
|
||||
* @return mixed
|
||||
*/
|
||||
public function executeCommand(ICommand $command)
|
||||
{
|
||||
$this->initialize();
|
||||
$client = $this->client;
|
||||
|
||||
$response = $this->client->executeCommand($command);
|
||||
|
||||
if ($this->checkState(self::STATE_CAS)) {
|
||||
return call_user_func_array(array($client, $method), $arguments);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$command = $client->createCommand($method, $arguments);
|
||||
$response = $client->executeCommand($command);
|
||||
|
||||
if (!$response instanceof ResponseQueued) {
|
||||
$this->onProtocolError('The server did not respond with a QUEUED status reply');
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
; This file is meant to be used with Onion http://c9s.github.com/Onion/
|
||||
; In order to be able to build a PEAR package of Predis, open a new terminal
|
||||
; session and follow these two easy steps:
|
||||
;
|
||||
; $ wget https://github.com/c9s/Onion/raw/master/onion.phar
|
||||
; $ /usr/bin/env php onion.phar build
|
||||
;
|
||||
|
||||
[package]
|
||||
name = "Predis"
|
||||
desc = "Flexible and feature-complete PHP client library for Redis"
|
||||
homepage = "http://github.com/nrk/predis"
|
||||
license = "MIT"
|
||||
version = "0.7.1"
|
||||
stability = "stable"
|
||||
channel = "pear.nrk.io"
|
||||
|
||||
author = "Daniele Alessandri \"nrk\" <suppakilla@gmail.com>"
|
||||
|
||||
[require]
|
||||
php = ">= 5.3.2"
|
||||
pearinstaller = "1.4.1"
|
||||
|
||||
[roles]
|
||||
*.md = doc
|
||||
lib = php
|
||||
|
||||
[optional phpiredis]
|
||||
hint = "Add support for faster protocol handling with phpiredis"
|
||||
extensions[] = socket
|
||||
extensions[] = phpiredis
|
||||
|
||||
[optional webdis]
|
||||
hint = "Add support for Webdis"
|
||||
extensions[] = curl
|
||||
extensions[] = phpiredis
|
||||
@@ -15,6 +15,7 @@ use \PHPUnit_Framework_TestCase as StandardTestCase;
|
||||
|
||||
use Predis\Profiles\ServerProfile;
|
||||
use Predis\Network\PredisCluster;
|
||||
use Predis\Network\MasterSlaveReplication;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -179,6 +180,22 @@ class ClientTest extends StandardTestCase
|
||||
$this->assertSame($cluster, $client->getConnection());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testConstructorWithReplicationArgument()
|
||||
{
|
||||
$replication = new MasterSlaveReplication();
|
||||
|
||||
$factory = new ConnectionFactory();
|
||||
$factory->createReplication($replication, array('tcp://host1?alias=master', 'tcp://host2?alias=slave'));
|
||||
|
||||
$client = new Client($replication);
|
||||
|
||||
$this->assertInstanceOf('Predis\Network\IConnectionReplication', $client->getConnection());
|
||||
$this->assertSame($replication, $client->getConnection());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
@@ -217,6 +234,20 @@ class ClientTest extends StandardTestCase
|
||||
$this->assertSame($factory, $client->getConnectionFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testConstructorWithArrayAndOptionReplicationArgument()
|
||||
{
|
||||
$arg1 = array('tcp://host1?alias=master', 'tcp://host2?alias=slave');
|
||||
$arg2 = array('replication' => true);
|
||||
$client = new Client($arg1, $arg2);
|
||||
|
||||
$this->assertInstanceOf('Predis\Network\IConnectionReplication', $connection = $client->getConnection());
|
||||
$this->assertSame('host1', $connection->getConnectionById('master')->getParameters()->host);
|
||||
$this->assertSame('host2', $connection->getConnectionById('slave')->getParameters()->host);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
@@ -385,7 +416,7 @@ class ClientTest extends StandardTestCase
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException Predis\NotSupportedException
|
||||
* @expectedExceptionMessage Retrieving connections by alias is supported only with clustered connections
|
||||
* @expectedExceptionMessage Retrieving connections by alias is supported only with aggregated connections (cluster or replication)
|
||||
*/
|
||||
public function testGetConnectionWithAliasWorksOnlyWithCluster()
|
||||
{
|
||||
|
||||
@@ -335,6 +335,29 @@ class ConnectionFactoryTest extends StandardTestCase
|
||||
$factory->createCluster($cluster, $nodes, $profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testReplicationWithMixedConnectionParameters()
|
||||
{
|
||||
list(, $connectionClass) = $this->getMockConnectionClass();
|
||||
|
||||
$replication = $this->getMock('Predis\Network\IConnectionReplication');
|
||||
$replication->expects($this->exactly(4))
|
||||
->method('add')
|
||||
->with($this->isInstanceOf('Predis\Network\IConnectionSingle'));
|
||||
|
||||
$factory = $this->getMock('Predis\ConnectionFactory', array('create'));
|
||||
$factory->expects($this->exactly(3))
|
||||
->method('create')
|
||||
->will($this->returnCallback(function($_, $_) use($connectionClass) {
|
||||
return new $connectionClass;
|
||||
}));
|
||||
|
||||
$factory->createReplication($replication, array(null, 'tcp://127.0.0.1', array('scheme' => 'tcp'), new $connectionClass()));
|
||||
}
|
||||
|
||||
|
||||
// ******************************************************************** //
|
||||
// ---- HELPER METHODS ------------------------------------------------ //
|
||||
// ******************************************************************** //
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
<?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\Network;
|
||||
|
||||
use \PHPUnit_Framework_TestCase as StandardTestCase;
|
||||
|
||||
use Predis\ConnectionParameters;
|
||||
use Predis\Profiles\ServerProfile;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class MasterSlaveReplicationTest extends StandardTestCase
|
||||
{
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testAddingConnectionsToReplication()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave2 = $this->getMockConnection('tcp://host3?alias=slave2');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
$replication->add($slave2);
|
||||
|
||||
$this->assertSame($master, $replication->getConnectionById('master'));
|
||||
$this->assertSame($slave1, $replication->getConnectionById('slave1'));
|
||||
$this->assertSame($slave2, $replication->getConnectionById('slave2'));
|
||||
|
||||
$this->assertSame($master, $replication->getMaster());
|
||||
$this->assertSame(array($slave1, $slave2), $replication->getSlaves());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testRemovingConnectionsFromReplication()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave2 = $this->getMockConnection('tcp://host3?alias=slave2');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$this->assertTrue($replication->remove($slave1));
|
||||
$this->assertFalse($replication->remove($slave2));
|
||||
|
||||
$this->assertSame($master, $replication->getMaster());
|
||||
$this->assertSame(array(), $replication->getSlaves());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException RuntimeException
|
||||
* @expectedExceptionMessage Replication needs a master and at least one slave
|
||||
*/
|
||||
public function testThrowsExceptionOnEmptyReplication()
|
||||
{
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException RuntimeException
|
||||
* @expectedExceptionMessage Replication needs a master and at least one slave
|
||||
*/
|
||||
public function testThrowsExceptionOnMissingMaster()
|
||||
{
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($this->getMockConnection('tcp://host2?alias=slave1'));
|
||||
|
||||
$replication->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException RuntimeException
|
||||
* @expectedExceptionMessage Replication needs a master and at least one slave
|
||||
*/
|
||||
public function testThrowsExceptionOnMissingSlave()
|
||||
{
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($this->getMockConnection('tcp://host1?alias=master'));
|
||||
|
||||
$replication->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testConnectForcesConnectionToOneOfSlaves()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->never())->method('connect');
|
||||
|
||||
$slave = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave->expects($this->once())->method('connect');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
|
||||
$replication->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIsConnectedReturnsTrueIfAtLeastOneConnectionIsOpen()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->never())->method('isConnected')->will($this->returnValue(false));
|
||||
|
||||
$slave = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave->expects($this->once())->method('isConnected')->will($this->returnValue(true));
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
$replication->connect();
|
||||
|
||||
$this->assertTrue($replication->isConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testIsConnectedReturnsFalseIfAllConnectionsAreClosed()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->any())->method('isConnected')->will($this->returnValue(false));
|
||||
|
||||
$slave = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave->expects($this->any())->method('isConnected')->will($this->returnValue(false));
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
|
||||
$this->assertFalse($replication->isConnected());
|
||||
|
||||
$replication->connect();
|
||||
$replication->disconnect();
|
||||
|
||||
$this->assertFalse($replication->isConnected());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testDisconnectForcesCurrentConnectionToDisconnect()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('disconnect');
|
||||
|
||||
$slave = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave->expects($this->once())->method('disconnect');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave);
|
||||
|
||||
$replication->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCanSwitchConnectionByAlias()
|
||||
{
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$this->assertNull($replication->getCurrent());
|
||||
|
||||
$replication->switchTo('master');
|
||||
$this->assertSame($master, $replication->getCurrent());
|
||||
$replication->switchTo('slave1');
|
||||
$this->assertSame($slave1, $replication->getCurrent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException InvalidArgumentException
|
||||
* @expectedExceptionMessage The specified connection is not valid
|
||||
*/
|
||||
public function testThrowsErrorWhenSwitchingToUnknownConnection()
|
||||
{
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($this->getMockConnection('tcp://host1?alias=master'));
|
||||
$replication->add($this->getMockConnection('tcp://host2?alias=slave1'));
|
||||
|
||||
$replication->switchTo('unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testUsesSlavesOnReadOnlyCommands()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$cmd = $profile->createCommand('exists', array('foo'));
|
||||
$this->assertSame($slave1, $replication->getConnection($cmd));
|
||||
|
||||
$cmd = $profile->createCommand('get', array('foo'));
|
||||
$this->assertSame($slave1, $replication->getConnection($cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testUsesMasterOnWriteCommands()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$cmd = $profile->createCommand('set', array('foo', 'bar'));
|
||||
$this->assertSame($master, $replication->getConnection($cmd));
|
||||
|
||||
$cmd = $profile->createCommand('get', array('foo'));
|
||||
$this->assertSame($master, $replication->getConnection($cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testSwitchesFromSlaveToMasterOnWriteCommands()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$cmd = $profile->createCommand('exists', array('foo'));
|
||||
$this->assertSame($slave1, $replication->getConnection($cmd));
|
||||
|
||||
$cmd = $profile->createCommand('set', array('foo', 'bar'));
|
||||
$this->assertSame($master, $replication->getConnection($cmd));
|
||||
|
||||
$cmd = $profile->createCommand('exists', array('foo'));
|
||||
$this->assertSame($master, $replication->getConnection($cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWritesCommandToCorrectConnection()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdExists = $profile->createCommand('exists', array('foo'));
|
||||
$cmdSet = $profile->getDefault()->createCommand('set', array('foo', 'bar'));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('writeCommand')->with($cmdSet);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->once())->method('writeCommand')->with($cmdExists);
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->writeCommand($cmdExists);
|
||||
$replication->writeCommand($cmdSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testReadsCommandFromCorrectConnection()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdExists = $profile->createCommand('exists', array('foo'));
|
||||
$cmdSet = $profile->getDefault()->createCommand('set', array('foo', 'bar'));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('readResponse')->with($cmdSet);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->once())->method('readResponse')->with($cmdExists);
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->readResponse($cmdExists);
|
||||
$replication->readResponse($cmdSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testExecutesCommandOnCorrectConnection()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdExists = $profile->createCommand('exists', array('foo'));
|
||||
$cmdSet = $profile->getDefault()->createCommand('set', array('foo', 'bar'));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('executeCommand')->with($cmdSet);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->once())->method('executeCommand')->with($cmdExists);
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->executeCommand($cmdExists);
|
||||
$replication->executeCommand($cmdSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testWatchTriggersSwitchToMasterConnection()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdWatch = $profile->createCommand('watch', array('foo'));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('executeCommand')->with($cmdWatch);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->never())->method('executeCommand');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->executeCommand($cmdWatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testMultiTriggersSwitchToMasterConnection()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdMulti = $profile->createCommand('multi');
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('executeCommand')->with($cmdMulti);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->never())->method('executeCommand');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->executeCommand($cmdMulti);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testEvalTriggersSwitchToMasterConnection()
|
||||
{
|
||||
$profile = ServerProfile::get('dev');
|
||||
$cmdEval = $profile->createCommand('eval', array("return redis.call('info')"));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('executeCommand')->with($cmdEval);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->never())->method('executeCommand');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->executeCommand($cmdEval);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException Predis\NotSupportedException
|
||||
* @expectedExceptionMessage The command INFO is not allowed in replication mode
|
||||
*/
|
||||
public function testThrowsExceptionOnNonSupportedCommand()
|
||||
{
|
||||
$cmd = ServerProfile::getDefault()->createCommand('info');
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($this->getMockConnection('tcp://host1?alias=master'));
|
||||
$replication->add($this->getMockConnection('tcp://host2?alias=slave1'));
|
||||
|
||||
$replication->getConnection($cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCanOverrideReadOnlyFlagForCommands()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdSet = $profile->createCommand('set', array('foo', 'bar'));
|
||||
$cmdGet = $profile->createCommand('get', array('foo'));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('executeCommand')->with($cmdGet);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->once())->method('executeCommand')->with($cmdSet);
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->setCommandReadOnly($cmdSet->getId(), true);
|
||||
$replication->setCommandReadOnly($cmdGet->getId(), false);
|
||||
|
||||
$replication->executeCommand($cmdSet);
|
||||
$replication->executeCommand($cmdGet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testAcceptsCallableToOverrideReadOnlyFlagForCommands()
|
||||
{
|
||||
$profile = ServerProfile::getDefault();
|
||||
$cmdExistsFoo = $profile->createCommand('exists', array('foo'));
|
||||
$cmdExistsBar = $profile->createCommand('exists', array('bar'));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->once())->method('executeCommand')->with($cmdExistsBar);
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->once())->method('executeCommand')->with($cmdExistsFoo);
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->setCommandReadOnly('exists', function($cmd) {
|
||||
list($arg1) = $cmd->getArguments();
|
||||
return $arg1 === 'foo';
|
||||
});
|
||||
|
||||
$replication->executeCommand($cmdExistsFoo);
|
||||
$replication->executeCommand($cmdExistsBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testCanSetReadOnlyFlagForEvalScripts()
|
||||
{
|
||||
$profile = ServerProfile::get('dev');
|
||||
|
||||
$cmdEval = $profile->createCommand('eval', array($script = "return redis.call('info');"));
|
||||
$cmdEvalSha = $profile->createCommand('evalsha', array($scriptSHA1 = sha1($script)));
|
||||
|
||||
$master = $this->getMockConnection('tcp://host1?alias=master');
|
||||
$master->expects($this->never())->method('executeCommand');
|
||||
|
||||
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
|
||||
$slave1->expects($this->exactly(2))
|
||||
->method('executeCommand')
|
||||
->with($this->logicalOr($cmdEval, $cmdEvalSha));
|
||||
|
||||
$replication = new MasterSlaveReplication();
|
||||
$replication->add($master);
|
||||
$replication->add($slave1);
|
||||
|
||||
$replication->setScriptReadOnly($script);
|
||||
|
||||
$replication->executeCommand($cmdEval);
|
||||
$replication->executeCommand($cmdEvalSha);
|
||||
}
|
||||
|
||||
// ******************************************************************** //
|
||||
// ---- HELPER METHODS ------------------------------------------------ //
|
||||
// ******************************************************************** //
|
||||
|
||||
/**
|
||||
* Returns a base mocked connection from Predis\Network\IConnectionSingle.
|
||||
*
|
||||
* @param mixed $parameters Optional parameters.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getMockConnection($parameters = null)
|
||||
{
|
||||
$connection = $this->getMock('Predis\Network\IConnectionSingle');
|
||||
|
||||
if ($parameters) {
|
||||
$parameters = new ConnectionParameters($parameters);
|
||||
$hash = "{$parameters->host}:{$parameters->port}";
|
||||
|
||||
$connection->expects($this->any())
|
||||
->method('getParameters')
|
||||
->will($this->returnValue($parameters));
|
||||
$connection->expects($this->any())
|
||||
->method('__toString')
|
||||
->will($this->returnValue($hash));
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,28 @@ class ClientProfileTest extends StandardTestCase
|
||||
$this->assertNull($profile->getProcessor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testValidationAcceptsCallableObjectAsInitializers()
|
||||
{
|
||||
$value = $this->getMock('Predis\Profiles\IServerProfile');
|
||||
|
||||
$initializer = $this->getMock('stdClass', array('__invoke'));
|
||||
$initializer->expects($this->once())
|
||||
->method('__invoke')
|
||||
->with($this->isInstanceOf('Predis\Options\IClientOptions'))
|
||||
->will($this->returnValue($value));
|
||||
|
||||
$options = $this->getMock('Predis\Options\IClientOptions');
|
||||
$option = new ClientProfile();
|
||||
|
||||
$profile = $option->filter($options, $initializer);
|
||||
|
||||
$this->assertInstanceOf('Predis\Profiles\IServerProfile', $profile);
|
||||
$this->assertSame($value, $profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
@@ -169,4 +191,19 @@ class ClientProfileTest extends StandardTestCase
|
||||
$this->assertInstanceOf('Predis\Profiles\IServerProfile', $profile);
|
||||
$this->assertNull($profile->getProcessor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @expectedException InvalidArgumentException
|
||||
* @expectedExceptionMessage Invalid value for the profile option
|
||||
*/
|
||||
public function testValidationThrowsExceptionOnInvalidObjectReturnedByCallback()
|
||||
{
|
||||
$value = function($options) { return new \stdClass(); };
|
||||
|
||||
$options = $this->getMock('Predis\Options\IClientOptions');
|
||||
$option = new ClientProfile();
|
||||
|
||||
$option->filter($options, $value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user