Compare commits

..

52 Commits

Author SHA1 Message Date
Daniele Alessandri 74cc0e3225 Bump version in files for release. 2012-04-01 10:09:20 +02:00
Daniele Alessandri b5ae1b9a2b Update README. 2012-03-31 11:58:38 +02:00
Daniele Alessandri 31c47362fb Add a couple of paragraphs in the FAQ. 2012-03-31 11:29:59 +02:00
Daniele Alessandri 871f1a3a1d Update TODO. 2012-03-31 10:45:16 +02:00
Daniele Alessandri ba0338e3d8 Update CHANGELOG. 2012-03-31 10:44:29 +02:00
Daniele Alessandri a7c906ab4c Apparently the next major version or Redis will be 2.8. 2012-03-31 10:43:50 +02:00
Daniele Alessandri 7675bb040a Make it possible to prepend the autoloader. 2012-03-19 11:31:39 +01:00
Daniele Alessandri 930af24c7f Make Predis\Autoloader faster. 2012-03-18 18:00:00 +01:00
Daniele Alessandri 23ed563e45 Add missing tests for Predis\Profiles\ServerProfile::defineCommand(). 2012-03-18 12:58:19 +01:00
Daniele Alessandri f00bf6443d Update CHANGELOG.
[ci skip]
2012-03-11 14:01:49 +01:00
Daniele Alessandri 6d73f4a538 Promote 2.6 as an independent server profile for Redis 2.6.
The development server profile now targets Redis 3.0.
2012-03-11 13:53:28 +01:00
Daniele Alessandri f0f3d7814c Fix MONITOR and Predis\MonitorContext with Redis 2.6.
Starting with 2.6, Redis uses a slightly different format for the payload
returned by MONITOR for each command. Predis can now adapt to this difference
and returns a new `client` field in the payload object. This new field is
always defined but is set to NULL when connected to Redis < 2.6.

Please note that Redis 2.6 now does not echo the `MONITOR` command as the
first payload when opening a new MONITOR context.
2012-03-11 13:30:30 +01:00
Daniele Alessandri 5c2704c9d4 Add a not about MONITOR failing the test with Redis unstable. 2012-03-07 13:04:28 +01:00
Daniele Alessandri effefcee34 New command: TIME (Redis v2.6-dev). 2012-03-07 12:30:39 +01:00
Daniele Alessandri 86bf223ec0 Enable integration tests for Redis commands on Travis CI.
This was not possible before since our test suite requires Redis >= 2.4 but
their environment was running Redis 2.2 until a month ago or so.
2012-02-25 11:04:31 +01:00
Daniele Alessandri 46d293a716 Fix phpdocs.
[ci skip]
2012-01-14 23:35:39 +01:00
Daniele Alessandri fcd41a4787 Fix CHANGELOG. 2012-01-14 23:33:39 +01:00
Daniele Alessandri 8dc9450f39 Bump year in LICENSE.
[ci skip]
2012-01-14 15:29:12 +01:00
Daniele Alessandri 6ad522f492 Update CHANGELOG. 2012-01-14 15:11:27 +01:00
Daniele Alessandri 317fb6c398 Make Predis\Network\MasterSlaveReplication serializable.
Since PHP cannot serialize closures we switched to a private method to check
if a SORT command is a read-only operation, but closures are still supported
even if they will make the connection unserializable.
2012-01-14 14:53:52 +01:00
Daniele Alessandri 93163bdfb3 Make it possible to serialize and unserialize connection instances. 2012-01-14 14:53:47 +01:00
Daniele Alessandri 626520b836 Add specific server profile alias for Redis 2.6. 2012-01-14 13:01:38 +01:00
Daniele Alessandri aaea628c55 Back to development for the next patch release. 2012-01-14 12:51:01 +01:00
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
50 changed files with 2342 additions and 85 deletions
+3
View File
@@ -1,2 +1,5 @@
*.tgz
*.phar
phpunit.xml
package.xml
experiments/
+40 -3
View File
@@ -1,3 +1,40 @@
v0.7.2 (2012-04-01)
===============================================================================
- Added `2.6` in the server profiles aliases list for the upcoming Redis 2.6.
`2.4` is still the default server profile. `dev` now targets Redis 2.8.
- Connection instances can be serialized and unserialized using `serialize()`
and `unserialize()`. This is handy in certain scenarios such as client-side
clustering or replication to lower the overhead of initializing a connection
object with many sub-connections since unserializing them can be up to 5x
times faster.
- Reworked the default autoloader to make it faster. It is also possible to
prepend it in PHP's autoload stack.
- __FIX__: fixed parsing of the payload returned by `MONITOR` with Redis 2.6.
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 +103,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`.
@@ -78,7 +115,7 @@ v0.6.6 (2011-04-01)
- Switched to Redis 2.2 as the default server profile (there are no changes
that would break compatibility with previous releases). Long command names
are no more supported by default but if you need them you can still require
Predis_Compatibility.php to avoid breaking compatibility.
`Predis_Compatibility.php` to avoid breaking compatibility.
- Added a `VERSION` constant to `Predis\Client`.
@@ -88,7 +125,7 @@ v0.6.6 (2011-04-01)
- Predis now uses by default a new protocol reader, more lightweight and
faster than the default handler-based one. Users can revert to the old
protocol reader with the 'reader' client option set to `composable`.
protocol reader with the `reader` client option set to `composable`.
This client option can also accept custom reader classes implementing the
new `Predis\IResponseReader` interface.
+42
View File
@@ -56,3 +56,45 @@ generalized when using Redis because of the many possible access patterns for th
mean that it is impossible to have such a feature, you can leverage Predis' extensibility to define your
own serialization-aware commands. See [here](http://github.com/nrk/predis/issues/29#issuecomment-1202624)
for more details on how to implement such a feature with a practical example.
### How can I force Predis to connect to Redis before sending any command? ###
Explicitly connecting to Redis is usually not needed since the client library relies on lazily initialized
connections to the server, but this behavior can be inconvenient in certain scenarios when you absolutely
need to do an upfront check to detect if the server is up and running and eventually catch exceptions on
failures. In this case developers can use `Predis\Client::connect()` to explicitly connect to the server:
```
$client = new Predis\Client();
try {
$client->connect();
}
catch (Predis\Network\ConnectionException $exception) {
// We could not connect to Redis! Your handling code goes here.
}
$client->info();
```
### How Predis implements abstraction of Redis commands? ###
The approach used in Predis to implement the abstraction of Redis commands is quite simple. By default
every command in the library follows exactly the same argument list as defined in the great online
[Redis documentation](http://redis.io/commands) which makes things pretty easy if you already know how
Redis works or if you need to look up how to use certain commands. Alternatively, variadic commands can
accept an array for keys or values (depending on the command) instead of a list of arguments. See for
example how [RPUSH](http://redis.io/commands/rpush) or [HMSET](http://redis.io/commands/hmset) work:
```
$client->rpush('my:list', 'value1', 'value2', 'value3'); // values as arguments
$client->rpush('my:list', array('value1', 'value2', 'value3')); // values as single argument array
$client->hmset('my:hash', 'field1', 'value1', 'field2', 'value2'); // values as arguments
$client->hmset('my:hash', array('field1'=>'value1', 'field2'=>'value2'); // values as single named array
```
The only exception to this _rule_ is the [SORT](http://redis.io/commands/sort) command for which modifiers are
[passed using a named array](https://github.com/nrk/predis/blob/v0.7.1/tests/Predis/Commands/KeySortTest.php#L56-77).
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009-2011 Daniele Alessandri
Copyright (c) 2009-2012 Daniele Alessandri
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
+11 -9
View File
@@ -5,14 +5,14 @@ Predis is a flexible and feature-complete PHP (>= 5.3) client library for the Re
For a list of frequently asked questions about Predis, see the __FAQ__ file in the root of the repository.
For a version compatible with PHP 5.2 you must use the backported version from the latest release in the
0.6.x series. More details are available on the [official wiki](http://wiki.github.com/nrk/predis) of the
project,
project.
## Main features ##
- Complete support for Redis from __1.2__ to __2.4__ and the current development versions using different
server profiles.
- Complete support for Redis from __1.2__ to __2.6__ and unstable 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,14 +27,14 @@ 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 ###
To automatically load all of its files, Predis relies on the autoloading features of PHP and complies
Predis relies on the autoloading features of PHP to automatically load the needed files and complies
with the [PSR-0 standard](http://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) for
interoperability with most of the major frameworks and libraries. Everything is transparently handled
for you when installing the library using Composer, but you can also leverage its own autoloader class
@@ -42,7 +42,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 +54,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 +198,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/)
-2
View File
@@ -1,4 +1,2 @@
* Documentation! The README is obviously not enought to show how to use Predis
as it does not cover all of its features.
* [v0.8] Implement smart and transparent support for redis-cluster.
+1 -1
View File
@@ -1 +1 @@
0.7.0
0.7.2
+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('2.6');
$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);
+1 -1
View File
@@ -39,7 +39,7 @@ LUA;
}
}
$client = new Predis\Client($single_server, 'dev');
$client = new Predis\Client($single_server, '2.6');
$client->getProfile()->defineCommand('increx', 'IncrementExistingKey');
+13 -12
View File
@@ -15,27 +15,32 @@ namespace Predis;
* Implements a lightweight PSR-0 compliant autoloader.
*
* @author Eric Naeseth <eric@thumbtack.com>
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class Autoloader
{
private $baseDir;
private $directory;
private $prefix;
private $prefixLength;
/**
* @param string $baseDirectory Base directory where the source files are located.
*/
public function __construct($baseDirectory = null)
public function __construct($baseDirectory = __DIR__)
{
$this->baseDir = $baseDirectory ?: dirname(__FILE__);
$this->directory = $baseDirectory;
$this->prefix = __NAMESPACE__ . '\\';
$this->prefixLength = strlen($this->prefix);
}
/**
* Registers the autoloader class with the PHP SPL autoloader.
*
* @param boolean $prepend Prepend the autoloader on the stack instead of appending it.
*/
public static function register()
public static function register($prepend = false)
{
spl_autoload_register(array(new self, 'autoload'));
spl_autoload_register(array(new self, 'autoload'), true, $prepend);
}
/**
@@ -45,13 +50,9 @@ class Autoloader
*/
public function autoload($className)
{
if (0 !== strpos($className, $this->prefix)) {
return;
if (0 === strpos($className, $this->prefix)) {
$parts = explode('\\', substr($className, $this->prefixLength));
require($this->directory.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts).'.php');
}
$relativeClassName = substr($className, strlen($this->prefix));
$classNameParts = explode('\\', $relativeClassName);
require_once $this->baseDir.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $classNameParts).'.php';
}
}
+10 -4
View File
@@ -29,7 +29,7 @@ use Predis\Transaction\MultiExecContext;
*/
class Client
{
const VERSION = '0.7.0';
const VERSION = '0.7.2';
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);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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\Commands;
/**
* @link http://redis.io/commands/time
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ServerTime extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'TIME';
}
/**
* {@inheritdoc}
*/
protected function canBeHashed()
{
return false;
}
/**
* {@inheritdoc}
*/
public function parseResponse($data)
{
return $data instanceof \Iterator ? iterator_to_array($data) : $data;
}
}
+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);
}
+12 -4
View File
@@ -132,21 +132,29 @@ class MonitorContext implements \Iterator
private function getValue()
{
$database = 0;
$client = null;
$event = $this->client->getConnection()->read();
$callback = function($matches) use (&$database) {
if (isset($matches[1])) {
$callback = function($matches) use (&$database, &$client) {
if (2 === $count = count($matches)) {
// Redis <= 2.4
$database = (int) $matches[1];
}
if (4 === $count) {
// Redis >= 2.6
$database = (int) $matches[2];
$client = $matches[3];
}
return ' ';
};
$event = preg_replace_callback('/ \(db (\d+)\) /', $callback, $event, 1);
@list($timestamp, $command, $arguments) = split(' ', $event, 3);
$event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $callback, $event, 1);
@list($timestamp, $command, $arguments) = explode(' ', $event, 3);
return (object) array(
'timestamp' => (float) $timestamp,
'database' => $database,
'client' => $client,
'command' => substr($command, 1, -1),
'arguments' => $arguments,
);
@@ -132,4 +132,12 @@ class ComposableStreamConnection extends StreamConnection implements IConnection
{
return $this->protocol->read($this);
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return array_merge(parent::__sleep(), array('protocol'));
}
}
+11 -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);
}
/**
@@ -231,4 +232,12 @@ abstract class ConnectionBase implements IConnectionSingle
return $this->cachedId;
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return array('parameters', 'initCmds');
}
}
@@ -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,423 @@
<?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 call_user_func($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 call_user_func($readonly, $command);
}
}
return false;
}
/**
* Checks if a SORT command is a readable operation by parsing the arguments
* array of the specified commad instance.
*
* @param ICommand $command Instance of Redis command.
* @return Boolean
*/
private function isSortReadOnly(ICommand $command)
{
$arguments = $command->getArguments();
return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE';
}
/**
* 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' => array($this, 'isSortReadOnly'),
);
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return array('master', 'slaves', 'disallowed', 'readonly', 'readonlySHA1');
}
}
@@ -384,4 +384,12 @@ class PhpiredisConnection extends ConnectionBase
array_unshift($cmdargs, $command->getId());
$this->write(phpiredis_format_command($cmdargs));
}
/**
* {@inheritdoc}
*/
public function __wakeup()
{
$this->initializeProtocol($this->getParameters());
}
}
+8
View File
@@ -284,4 +284,12 @@ class StreamConnection extends ConnectionBase
$this->writeBytes($buffer);
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return array_merge(parent::__sleep(), array('mbiterable', 'throwErrors'));
}
}
+20
View File
@@ -321,4 +321,24 @@ class WebdisConnection implements IConnectionSingle
{
return "{$this->parameters->host}:{$this->parameters->port}";
}
/**
* {@inheritdoc}
*/
public function __sleep()
{
return array('parameters');
}
/**
* {@inheritdoc}
*/
public function __wakeup()
{
$this->checkExtensions();
$parameters = $this->getParameters();
$this->resource = $this->initializeCurl($parameters);
$this->reader = $this->initializeReader($parameters);
}
}
+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);
}
+1
View File
@@ -76,6 +76,7 @@ abstract class ServerProfile implements IServerProfile, IProcessingSupport
'2.0' => 'Predis\Profiles\ServerVersion20',
'2.2' => 'Predis\Profiles\ServerVersion22',
'2.4' => 'Predis\Profiles\ServerVersion24',
'2.6' => 'Predis\Profiles\ServerVersion26',
'default' => 'Predis\Profiles\ServerVersion24',
'dev' => 'Predis\Profiles\ServerVersionNext',
);
+234
View File
@@ -0,0 +1,234 @@
<?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\Profiles;
/**
* Server profile for Redis v2.6.x.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ServerVersion26 extends ServerProfile
{
/**
* {@inheritdoc}
*/
public function getVersion()
{
return '2.6';
}
/**
* {@inheritdoc}
*/
public function getSupportedCommands()
{
return array(
/* ---------------- Redis 1.2 ---------------- */
/* commands operating on the key space */
'exists' => 'Predis\Commands\KeyExists',
'del' => 'Predis\Commands\KeyDelete',
'type' => 'Predis\Commands\KeyType',
'keys' => 'Predis\Commands\KeyKeys',
'randomkey' => 'Predis\Commands\KeyRandom',
'rename' => 'Predis\Commands\KeyRename',
'renamenx' => 'Predis\Commands\KeyRenamePreserve',
'expire' => 'Predis\Commands\KeyExpire',
'expireat' => 'Predis\Commands\KeyExpireAt',
'ttl' => 'Predis\Commands\KeyTimeToLive',
'move' => 'Predis\Commands\KeyMove',
'sort' => 'Predis\Commands\KeySort',
/* commands operating on string values */
'set' => 'Predis\Commands\StringSet',
'setnx' => 'Predis\Commands\StringSetPreserve',
'mset' => 'Predis\Commands\StringSetMultiple',
'msetnx' => 'Predis\Commands\StringSetMultiplePreserve',
'get' => 'Predis\Commands\StringGet',
'mget' => 'Predis\Commands\StringGetMultiple',
'getset' => 'Predis\Commands\StringGetSet',
'incr' => 'Predis\Commands\StringIncrement',
'incrby' => 'Predis\Commands\StringIncrementBy',
'decr' => 'Predis\Commands\StringDecrement',
'decrby' => 'Predis\Commands\StringDecrementBy',
/* commands operating on lists */
'rpush' => 'Predis\Commands\ListPushTail',
'lpush' => 'Predis\Commands\ListPushHead',
'llen' => 'Predis\Commands\ListLength',
'lrange' => 'Predis\Commands\ListRange',
'ltrim' => 'Predis\Commands\ListTrim',
'lindex' => 'Predis\Commands\ListIndex',
'lset' => 'Predis\Commands\ListSet',
'lrem' => 'Predis\Commands\ListRemove',
'lpop' => 'Predis\Commands\ListPopFirst',
'rpop' => 'Predis\Commands\ListPopLast',
'rpoplpush' => 'Predis\Commands\ListPopLastPushHead',
/* commands operating on sets */
'sadd' => 'Predis\Commands\SetAdd',
'srem' => 'Predis\Commands\SetRemove',
'spop' => 'Predis\Commands\SetPop',
'smove' => 'Predis\Commands\SetMove',
'scard' => 'Predis\Commands\SetCardinality',
'sismember' => 'Predis\Commands\SetIsMember',
'sinter' => 'Predis\Commands\SetIntersection',
'sinterstore' => 'Predis\Commands\SetIntersectionStore',
'sunion' => 'Predis\Commands\SetUnion',
'sunionstore' => 'Predis\Commands\SetUnionStore',
'sdiff' => 'Predis\Commands\SetDifference',
'sdiffstore' => 'Predis\Commands\SetDifferenceStore',
'smembers' => 'Predis\Commands\SetMembers',
'srandmember' => 'Predis\Commands\SetRandomMember',
/* commands operating on sorted sets */
'zadd' => 'Predis\Commands\ZSetAdd',
'zincrby' => 'Predis\Commands\ZSetIncrementBy',
'zrem' => 'Predis\Commands\ZSetRemove',
'zrange' => 'Predis\Commands\ZSetRange',
'zrevrange' => 'Predis\Commands\ZSetReverseRange',
'zrangebyscore' => 'Predis\Commands\ZSetRangeByScore',
'zcard' => 'Predis\Commands\ZSetCardinality',
'zscore' => 'Predis\Commands\ZSetScore',
'zremrangebyscore' => 'Predis\Commands\ZSetRemoveRangeByScore',
/* connection related commands */
'ping' => 'Predis\Commands\ConnectionPing',
'auth' => 'Predis\Commands\ConnectionAuth',
'select' => 'Predis\Commands\ConnectionSelect',
'echo' => 'Predis\Commands\ConnectionEcho',
'quit' => 'Predis\Commands\ConnectionQuit',
/* remote server control commands */
'info' => 'Predis\Commands\ServerInfo',
'slaveof' => 'Predis\Commands\ServerSlaveOf',
'monitor' => 'Predis\Commands\ServerMonitor',
'dbsize' => 'Predis\Commands\ServerDatabaseSize',
'flushdb' => 'Predis\Commands\ServerFlushDatabase',
'flushall' => 'Predis\Commands\ServerFlushAll',
'save' => 'Predis\Commands\ServerSave',
'bgsave' => 'Predis\Commands\ServerBackgroundSave',
'lastsave' => 'Predis\Commands\ServerLastSave',
'shutdown' => 'Predis\Commands\ServerShutdown',
'bgrewriteaof' => 'Predis\Commands\ServerBackgroundRewriteAOF',
/* ---------------- Redis 2.0 ---------------- */
/* commands operating on string values */
'setex' => 'Predis\Commands\StringSetExpire',
'append' => 'Predis\Commands\StringAppend',
'substr' => 'Predis\Commands\StringSubstr',
/* commands operating on lists */
'blpop' => 'Predis\Commands\ListPopFirstBlocking',
'brpop' => 'Predis\Commands\ListPopLastBlocking',
/* commands operating on sorted sets */
'zunionstore' => 'Predis\Commands\ZSetUnionStore',
'zinterstore' => 'Predis\Commands\ZSetIntersectionStore',
'zcount' => 'Predis\Commands\ZSetCount',
'zrank' => 'Predis\Commands\ZSetRank',
'zrevrank' => 'Predis\Commands\ZSetReverseRank',
'zremrangebyrank' => 'Predis\Commands\ZSetRemoveRangeByRank',
/* commands operating on hashes */
'hset' => 'Predis\Commands\HashSet',
'hsetnx' => 'Predis\Commands\HashSetPreserve',
'hmset' => 'Predis\Commands\HashSetMultiple',
'hincrby' => 'Predis\Commands\HashIncrementBy',
'hget' => 'Predis\Commands\HashGet',
'hmget' => 'Predis\Commands\HashGetMultiple',
'hdel' => 'Predis\Commands\HashDelete',
'hexists' => 'Predis\Commands\HashExists',
'hlen' => 'Predis\Commands\HashLength',
'hkeys' => 'Predis\Commands\HashKeys',
'hvals' => 'Predis\Commands\HashValues',
'hgetall' => 'Predis\Commands\HashGetAll',
/* transactions */
'multi' => 'Predis\Commands\TransactionMulti',
'exec' => 'Predis\Commands\TransactionExec',
'discard' => 'Predis\Commands\TransactionDiscard',
/* publish - subscribe */
'subscribe' => 'Predis\Commands\PubSubSubscribe',
'unsubscribe' => 'Predis\Commands\PubSubUnsubscribe',
'psubscribe' => 'Predis\Commands\PubSubSubscribeByPattern',
'punsubscribe' => 'Predis\Commands\PubSubUnsubscribeByPattern',
'publish' => 'Predis\Commands\PubSubPublish',
/* remote server control commands */
'config' => 'Predis\Commands\ServerConfig',
/* ---------------- Redis 2.2 ---------------- */
/* commands operating on the key space */
'persist' => 'Predis\Commands\KeyPersist',
/* commands operating on string values */
'strlen' => 'Predis\Commands\StringStrlen',
'setrange' => 'Predis\Commands\StringSetRange',
'getrange' => 'Predis\Commands\StringGetRange',
'setbit' => 'Predis\Commands\StringSetBit',
'getbit' => 'Predis\Commands\StringGetBit',
/* commands operating on lists */
'rpushx' => 'Predis\Commands\ListPushTailX',
'lpushx' => 'Predis\Commands\ListPushHeadX',
'linsert' => 'Predis\Commands\ListInsert',
'brpoplpush' => 'Predis\Commands\ListPopLastPushHeadBlocking',
/* commands operating on sorted sets */
'zrevrangebyscore' => 'Predis\Commands\ZSetReverseRangeByScore',
/* transactions */
'watch' => 'Predis\Commands\TransactionWatch',
'unwatch' => 'Predis\Commands\TransactionUnwatch',
/* remote server control commands */
'object' => 'Predis\Commands\ServerObject',
'slowlog' => 'Predis\Commands\ServerSlowlog',
/* ---------------- Redis 2.4 ---------------- */
/* remote server control commands */
'client' => 'Predis\Commands\ServerClient',
/* ---------------- Redis 2.6 ---------------- */
/* commands operating on the key space */
'pttl' => 'Predis\Commands\KeyPreciseTimeToLive',
'pexpire' => 'Predis\Commands\KeyPreciseExpire',
'pexpireat' => 'Predis\Commands\KeyPreciseExpireAt',
/* commands operating on string values */
'psetex' => 'Predis\Commands\StringPreciseSetExpire',
'incrbyfloat' => 'Predis\Commands\StringIncrementByFloat',
/* commands operating on hashes */
'hincrbyfloat' => 'Predis\Commands\HashIncrementByFloat',
/* scripting */
'eval' => 'Predis\Commands\ServerEval',
'evalsha' => 'Predis\Commands\ServerEvalSHA',
'script' => 'Predis\Commands\ServerScript',
/* remote server control commands */
'info' => 'Predis\Commands\ServerInfoV26x',
'time' => 'Predis\Commands\ServerTime',
);
}
}
+3 -22
View File
@@ -12,18 +12,18 @@
namespace Predis\Profiles;
/**
* Server profile for the current development version of Redis.
* Server profile for the current unstable version of Redis.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ServerVersionNext extends ServerVersion24
class ServerVersionNext extends ServerVersion26
{
/**
* {@inheritdoc}
*/
public function getVersion()
{
return '2.6';
return '2.8';
}
/**
@@ -32,25 +32,6 @@ class ServerVersionNext extends ServerVersion24
public function getSupportedCommands()
{
return array_merge(parent::getSupportedCommands(), array(
/* commands operating on the key space */
'pttl' => 'Predis\Commands\KeyPreciseTimeToLive',
'pexpire' => 'Predis\Commands\KeyPreciseExpire',
'pexpireat' => 'Predis\Commands\KeyPreciseExpireAt',
/* commands operating on string values */
'psetex' => 'Predis\Commands\StringPreciseSetExpire',
'incrbyfloat' => 'Predis\Commands\StringIncrementByFloat',
/* commands operating on hashes */
'hincrbyfloat' => 'Predis\Commands\HashIncrementByFloat',
/* scripting */
'eval' => 'Predis\Commands\ServerEval',
'evalsha' => 'Predis\Commands\ServerEvalSHA',
'script' => 'Predis\Commands\ServerScript',
/* remote server control commands */
'info' => 'Predis\Commands\ServerInfoV26x',
));
}
}
+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.2"
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
+1 -1
View File
@@ -12,7 +12,7 @@
<group>ext-phpiredis</group>
<group>ext-curl</group>
<group>realm-webdis</group>
<group>connected</group>
<!-- <group>connected</group> -->
<!-- <group>disconnected</group> -->
<!-- <group>commands</group> -->
<!-- <group>slow</group> -->
+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()
{
+6 -1
View File
@@ -64,6 +64,11 @@ class ServerMonitorTest extends CommandTestCase
$command = $this->getCommand();
$this->assertTrue($connection->executeCommand($command));
$this->assertRegExp('/\d+.\d+(\s?\(db \d+\))? "MONITOR"/', $connection->read());
// NOTE: Starting with 2.6 Redis does not return the "MONITOR" message after
// +OK to the client that issued the MONITOR command.
if (version_compare($this->getProfile()->getVersion(), '2.4', '<=')) {
$this->assertRegExp('/\d+.\d+(\s?\(db \d+\))? "MONITOR"/', $connection->read());
}
}
}
+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\Commands;
use \PHPUnit_Framework_TestCase as StandardTestCase;
/**
* @group commands
* @group realm-server
*/
class ServerTimeTest extends CommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Commands\ServerTime';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'TIME';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array();
$expected = array();
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse()
{
$expected = array(1331114908, 453990);
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($expected));
}
/**
* @group connected
*/
public function testReturnsServerTime()
{
$redis = $this->getClient();
$this->assertInternalType('array', $time = $redis->time());
$this->assertInternalType('string', $time[0]);
$this->assertInternalType('string', $time[1]);
}
}
+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 ------------------------------------------------ //
// ******************************************************************** //
+25 -1
View File
@@ -105,7 +105,7 @@ class MonitorContextTest extends StandardTestCase
/**
* @group disconnected
*/
public function testCurrentReadsMessageFromConnection()
public function testReadsMessageFromConnectionToRedis24()
{
$message = '1323367530.939137 (db 15) "MONITOR"';
@@ -120,6 +120,30 @@ class MonitorContextTest extends StandardTestCase
$payload = $monitor->current();
$this->assertSame(1323367530, (int) $payload->timestamp);
$this->assertSame(15, $payload->database);
$this->assertNull($payload->client);
$this->assertSame('MONITOR', $payload->command);
$this->assertNull($payload->arguments);
}
/**
* @group disconnected
*/
public function testReadsMessageFromConnectionToRedis26()
{
$message = '1323367530.939137 [15 127.0.0.1:37265] "MONITOR"';
$connection = $this->getMock('Predis\Network\IConnectionSingle');
$connection->expects($this->once())
->method('read')
->will($this->returnValue($message));
$client = new Client($connection);
$monitor = new MonitorContext($client);
$payload = $monitor->current();
$this->assertSame(1323367530, (int) $payload->timestamp);
$this->assertSame(15, $payload->database);
$this->assertSame('127.0.0.1:37265', $payload->client);
$this->assertSame('MONITOR', $payload->command);
$this->assertNull($payload->arguments);
}
@@ -0,0 +1,580 @@
<?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
*/
public function testSortTriggersSwitchToMasterConnectionOnStoreModifier()
{
$profile = ServerProfile::get('dev');
$cmdSortNormal = $profile->createCommand('sort', array('key'));
$cmdSortStore = $profile->createCommand('sort', array('key', array('store' => 'key:store')));
$master = $this->getMockConnection('tcp://host1?alias=master');
$master->expects($this->once())->method('executeCommand')->with($cmdSortStore);
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
$slave1->expects($this->once())->method('executeCommand')->with($cmdSortNormal);
$replication = new MasterSlaveReplication();
$replication->add($master);
$replication->add($slave1);
$replication->executeCommand($cmdSortNormal);
$replication->executeCommand($cmdSortStore);
}
/**
* @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);
}
/**
* @group disconnected
*/
public function testCanBeSerialized()
{
$master = $this->getMockConnection('tcp://host1?alias=master');
$slave1 = $this->getMockConnection('tcp://host2?alias=slave1');
$replication = new MasterSlaveReplication();
$replication->add($master);
$replication->add($slave1);
$unserialized = unserialize(serialize($replication));
$this->assertEquals($master, $unserialized->getConnectionById('master'));
$this->assertEquals($slave1, $unserialized->getConnectionById('slave1'));
}
// ******************************************************************** //
// ---- 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;
}
}
@@ -53,6 +53,20 @@ class PhpiredisConnectionTest extends ConnectionTestCase
$connection = new PhpiredisConnection($parameters);
}
/**
* @group disconnected
*/
public function testCanBeSerialized()
{
$parameters = $this->getParameters(array('alias' => 'redis', 'read_write_timeout' => 10));
$connection = new PhpiredisConnection($parameters);
$unserialized = unserialize(serialize($connection));
$this->assertInstanceOf('Predis\Network\PhpiredisConnection', $unserialized);
$this->assertEquals($parameters, $unserialized->getParameters());
}
// ******************************************************************** //
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //
@@ -321,6 +321,25 @@ class PredisClusterTest extends StandardTestCase
$cluster->executeCommand($command);
}
/**
* @group disconnected
*/
public function testCanBeSerialized()
{
$connection1 = $this->getMockConnection('tcp://host1?alias=first');
$connection2 = $this->getMockConnection('tcp://host2?alias=second');
$cluster = new PredisCluster();
$cluster->add($connection1);
$cluster->add($connection2);
// We use the following line to initialize the underlying hashring.
$cluster->getConnectionByKey('foo');
$unserialized = unserialize(serialize($cluster));
$this->assertEquals($cluster, $unserialized);
}
// ******************************************************************** //
// ---- HELPER METHODS ------------------------------------------------ //
// ******************************************************************** //
@@ -53,6 +53,19 @@ class StreamConnectionTest extends ConnectionTestCase
$connection = new StreamConnection($parameters);
}
/**
* @group disconnected
*/
public function testCanBeSerialized()
{
$parameters = $this->getParameters(array('alias' => 'redis', 'read_write_timeout' => 10));
$connection = new StreamConnection($parameters);
$unserialized = unserialize(serialize($connection));
$this->assertEquals($connection, $unserialized);
}
// ******************************************************************** //
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //
@@ -100,6 +100,20 @@ class WebdisConnectionTest extends StandardTestCase
$connection->executeCommand($this->getProfile()->createCommand('auth', array('foobar')));
}
/**
* @group disconnected
*/
public function testCanBeSerialized()
{
$parameters = $this->getParameters(array('alias' => 'webdis'));
$connection = new WebdisConnection($parameters);
$unserialized = unserialize(serialize($connection));
$this->assertInstanceOf('Predis\Network\WebdisConnection', $unserialized);
$this->assertEquals($parameters, $unserialized->getParameters());
}
// ******************************************************************** //
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //
@@ -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);
}
}
+29 -1
View File
@@ -20,7 +20,7 @@ use Predis\Commands\Processors\ProcessorChain;
class ServerProfileTest extends StandardTestCase
{
const DEFAULT_PROFILE_VERSION = '2.4';
const DEVELOPMENT_PROFILE_VERSION = '2.6';
const DEVELOPMENT_PROFILE_VERSION = '2.8';
/**
* @group disconnected
@@ -145,6 +145,34 @@ class ServerProfileTest extends StandardTestCase
$this->assertNull($profile->getCommandClass('UNKNOWN'));
}
/**
* @group disconnected
*/
public function testDefineCommand()
{
$profile = ServerProfile::getDefault();
$command = $this->getMock('Predis\Commands\ICommand');
$profile->defineCommand('mock', get_class($command));
$this->assertTrue($profile->supportsCommand('mock'));
$this->assertTrue($profile->supportsCommand('MOCK'));
$this->assertSame(get_class($command), $profile->getCommandClass('mock'));
}
/**
* @group disconnected
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Cannot register 'stdClass' as it is not a valid Redis command
*/
public function testDefineInvalidCommand()
{
$profile = ServerProfile::getDefault();
$profile->defineCommand('mock', 'stdClass');
}
/**
* @group disconnected
*/
@@ -0,0 +1,174 @@
<?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\Profiles;
/**
*
*/
class ServerVersion26Test extends ServerVersionTestCase
{
/**
* {@inheritdoc}
*/
public function getProfileInstance()
{
return new ServerVersion26();
}
/**
* {@inheritdoc}
*/
public function getExpectedVersion()
{
return '2.6';
}
/**
* {@inheritdoc}
*/
public function getExpectedCommands()
{
return array(
0 => 'exists',
1 => 'del',
2 => 'type',
3 => 'keys',
4 => 'randomkey',
5 => 'rename',
6 => 'renamenx',
7 => 'expire',
8 => 'expireat',
9 => 'ttl',
10 => 'move',
11 => 'sort',
12 => 'set',
13 => 'setnx',
14 => 'mset',
15 => 'msetnx',
16 => 'get',
17 => 'mget',
18 => 'getset',
19 => 'incr',
20 => 'incrby',
21 => 'decr',
22 => 'decrby',
23 => 'rpush',
24 => 'lpush',
25 => 'llen',
26 => 'lrange',
27 => 'ltrim',
28 => 'lindex',
29 => 'lset',
30 => 'lrem',
31 => 'lpop',
32 => 'rpop',
33 => 'rpoplpush',
34 => 'sadd',
35 => 'srem',
36 => 'spop',
37 => 'smove',
38 => 'scard',
39 => 'sismember',
40 => 'sinter',
41 => 'sinterstore',
42 => 'sunion',
43 => 'sunionstore',
44 => 'sdiff',
45 => 'sdiffstore',
46 => 'smembers',
47 => 'srandmember',
48 => 'zadd',
49 => 'zincrby',
50 => 'zrem',
51 => 'zrange',
52 => 'zrevrange',
53 => 'zrangebyscore',
54 => 'zcard',
55 => 'zscore',
56 => 'zremrangebyscore',
57 => 'ping',
58 => 'auth',
59 => 'select',
60 => 'echo',
61 => 'quit',
62 => 'info',
63 => 'slaveof',
64 => 'monitor',
65 => 'dbsize',
66 => 'flushdb',
67 => 'flushall',
68 => 'save',
69 => 'bgsave',
70 => 'lastsave',
71 => 'shutdown',
72 => 'bgrewriteaof',
73 => 'setex',
74 => 'append',
75 => 'substr',
76 => 'blpop',
77 => 'brpop',
78 => 'zunionstore',
79 => 'zinterstore',
80 => 'zcount',
81 => 'zrank',
82 => 'zrevrank',
83 => 'zremrangebyrank',
84 => 'hset',
85 => 'hsetnx',
86 => 'hmset',
87 => 'hincrby',
88 => 'hget',
89 => 'hmget',
90 => 'hdel',
91 => 'hexists',
92 => 'hlen',
93 => 'hkeys',
94 => 'hvals',
95 => 'hgetall',
96 => 'multi',
97 => 'exec',
98 => 'discard',
99 => 'subscribe',
100 => 'unsubscribe',
101 => 'psubscribe',
102 => 'punsubscribe',
103 => 'publish',
104 => 'config',
105 => 'persist',
106 => 'strlen',
107 => 'setrange',
108 => 'getrange',
109 => 'setbit',
110 => 'getbit',
111 => 'rpushx',
112 => 'lpushx',
113 => 'linsert',
114 => 'brpoplpush',
115 => 'zrevrangebyscore',
116 => 'watch',
117 => 'unwatch',
118 => 'object',
119 => 'slowlog',
120 => 'client',
121 => 'pttl',
122 => 'pexpire',
123 => 'pexpireat',
124 => 'psetex',
125 => 'incrbyfloat',
126 => 'hincrbyfloat',
127 => 'eval',
128 => 'evalsha',
129 => 'script',
130 => 'time',
);
}
}
@@ -29,7 +29,7 @@ class ServerVersionNextTest extends ServerVersionTestCase
*/
public function getExpectedVersion()
{
return '2.6';
return '2.8';
}
/**
@@ -168,6 +168,7 @@ class ServerVersionNextTest extends ServerVersionTestCase
127 => 'eval',
128 => 'evalsha',
129 => 'script',
130 => 'time',
);
}
}