Compare commits

..

29 Commits

Author SHA1 Message Date
Daniele Alessandri 135fd5004b Bump VERSION and update CHANGELOG. 2011-12-27 16:19:23 +01:00
Daniele Alessandri c25a815317 Update README. 2011-12-27 15:57:12 +01:00
Daniele Alessandri 39a9fc2ddd Add a new example for complex replication configuration. 2011-12-27 12:34:44 +01:00
Daniele Alessandri f5bca54bba Fix CHANGELOG. 2011-12-27 10:40:29 +01:00
Daniele Alessandri 364aa180f9 Use Predis\NotSupportedException for unsupported connection parameters. 2011-12-27 10:38:06 +01:00
Daniele Alessandri 4da506cd9e Track changes in Onion. 2011-12-24 19:03:13 +01:00
Daniele Alessandri ef5034dc72 Update CHANGELOG.
[ci skip]
2011-12-24 12:17:03 +01:00
Daniele Alessandri 994f045a4a Add a method to execute instances of Redis commands in a transaction. 2011-12-24 12:08:34 +01:00
Daniele Alessandri 8cd8d4e580 Fix broken scripted commands when all arguments are considered keys. 2011-12-21 23:14:46 +01:00
Daniele Alessandri 22f1aafae8 Fix example of replication. 2011-12-21 21:41:51 +01:00
Daniele Alessandri c47b009b69 Update documentation for master / slave replication. 2011-12-21 21:33:30 +01:00
Daniele Alessandri 2cff6f0886 Merge branch 'replication' into v0.7
See ISSUE #21 for history and details about transparent master / slave
replication support in Predis.
2011-12-21 21:25:56 +01:00
Daniele Alessandri 63cb150fc2 Switch to the master server right before the execution of a pipeline.
This is actually a temporary solution since we should be using a dedicated
pipeline executor to handle the master/slave replication connection.

Ideally we should stick with a slave if no write operations are present in
the buffer, but forcing a switch to the master connection is the easiest
solution for now and users can still create a new client instance from one
of the slaves when they are sure that their pipelines contain read-only
operations.
2011-12-21 21:15:46 +01:00
Daniele Alessandri 466c666547 Add example for master / slave replication. 2011-12-21 21:01:48 +01:00
Daniele Alessandri 8d5c794efe [tests] Add more tests for Predis\Network\MasterSlaveReplication. 2011-12-21 18:34:53 +01:00
Daniele Alessandri e3fba5d4b7 [tests] Add more tests for Predis\Network\MasterSlaveReplication. 2011-12-21 16:50:03 +01:00
Daniele Alessandri a8400f2ed8 Rename default replication class to Predis\Network\MasterSlaveReplication. 2011-12-21 16:50:03 +01:00
Daniele Alessandri 44629a33db [tests] Add tests for Predis\Network\PredisReplication. 2011-12-21 16:50:01 +01:00
Daniele Alessandri d38f164d3f Minor fixes in documentation. 2011-12-21 11:26:26 +01:00
Daniele Alessandri 9bc5685959 Make the "profile" option accept a callable object as initializer.
This can be useful to set up the profile with additional commands,
e.g. when injecting new "virtual" commands based upon the EVAL and
the Predis\Commands\ScriptedCommand class.
2011-12-21 10:56:10 +01:00
Daniele Alessandri 565690a5b1 Disallow certain commands when in replication mode.
Some of these commands do not actually pose any threat, but their replies can
be misleading since users do not know on which server they are connected to.
We prefer to make things explicit so users can get a new client object out of
a specific connection in the replication pool to issue these commands.
2011-12-20 12:10:06 +01:00
Daniele Alessandri bdbbe18e6c Add the ability to get a connection by alias from aggregated connections.
Previously it was possible to create a new instance of Predis\Client using
the alias of a single connection in a cluster of connections. Now we added
the ability to do this also when using master/slave replication.
2011-12-18 15:07:21 +01:00
Daniele Alessandri 78027f0498 Use our own channel to distribute Predis via PEAR instead of PearHub.
The new PEAR channel is self-hosted using Pirum.
2011-12-18 13:28:45 +01:00
Daniele Alessandri 104cd1eae7 Use Onion to build PEAR packages of Predis.
Since Pearhub is currently broken (unfortunately the project does not seem to
be actively mantained anymore) but we still want to have PEAR packages for our
library, we need to automate the creation of a package.xml definition file to
build the actual packages that will be uploaded on a self-hosted PEAR channel.

Onion, while still in its alpha stage right now, works perfectly fine for our
needs and its own package.ini file looks simple enough to edit and maintain.

See http://c9s.github.com/Onion for more details about Onion and thank @c9s
for making it available.
2011-12-18 12:50:10 +01:00
Daniele Alessandri 5148ce16c6 Add the ability to mark specific commands or scripts for EVAL as read-only.
As suggested by @Seldaek in ISSUE #21, it would be desiderable to have the
ability to mark certain scripts for EVAL (and EVALSHA) as read-only to avoid
switching to the master server when this is not really needed.

When marking a script as read-only, only its SHA1 hash is saved to use less
memory and to make this work transparently for both EVAL and EVALSHA.
2011-12-17 10:58:37 +01:00
Daniele Alessandri 4b9ab0bcce Revert to development version for the next patch release.
Forgot to do this early. [ci skip]
2011-12-16 22:04:21 +01:00
Daniele Alessandri 39ed8e3e8f Fix wrong usage of single-quotes for a string expecting interpolation. 2011-12-16 20:36:01 +01:00
Daniele Alessandri 6f4347010d Add new "replication" client option for master / slave replication.
When the "replication" option is specified and evaluates to true, the array of
connection parameters passed to the client constructor os be used to initialize
a new instance of Predis\Network\IConnectionReplication. This option can also
accept a callable object used as an initializer or the fully-qualified name of
a class.
2011-12-16 19:30:07 +01:00
Daniele Alessandri c43278eceb Implement support for master / slave replication configurations.
We used a new kind of virtual connection that, just like with cluster, groups
multiple connection objects and handles the logic needed to switch among them
depending on the kind of operation performed by commands..

Our default implementation starts by picking up a random slave and switches to
the master as soon as a command performing a write operation against a key is
detected. The master server will then be used for all the subsequent requests
unless a manual switch to a different connection object is performed.

Redis transactions are always performed on the master server, which means that
the switch to master is done as soon as the client issues a WATCH, MULTI or
any other command related to transactions.

See also https://github.com/nrk/predis/issues/21 for more details.
2011-12-16 16:28:36 +01:00
25 changed files with 1487 additions and 32 deletions
+3
View File
@@ -1,2 +1,5 @@
*.tgz
*.phar
phpunit.xml
package.xml
experiments/
+20 -1
View File
@@ -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`.
+8 -5
View File
@@ -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/)
+1 -1
View File
@@ -1 +1 @@
0.7.0
0.7.1
+52
View File
@@ -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
View File
@@ -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);
+14 -10
View File
@@ -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);
}
}
+13
View File
@@ -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.
*
+12
View File
@@ -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.
*
+10
View File
@@ -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);
}
+3 -2
View File
@@ -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';
},
);
}
}
+1 -1
View File
@@ -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();
+1
View File
@@ -42,6 +42,7 @@ class ClientOptions implements IClientOptions
'profile' => new ClientProfile(),
'connections' => new ClientConnectionFactory(),
'cluster' => new ClientCluster(),
'replication' => new ClientReplication(),
'prefix' => new ClientPrefix(),
);
}
+4
View File
@@ -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');
}
+74
View File
@@ -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();
}
}
+8
View File
@@ -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);
}
+19 -7
View File
@@ -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
View File
@@ -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
+32 -1
View File
@@ -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()
{
+23
View File
@@ -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);
}
}