Compare commits

..

65 Commits

Author SHA1 Message Date
Daniele Alessandri 9ead747663 Update CHANGELOG and bump VERSION. 2016-05-30 17:25:52 +02:00
Daniele Alessandri 39cf6abf82 [tests] Do not allow failures for HHVM on Travis CI. 2016-05-30 17:00:46 +02:00
Daniele Alessandri dbae384a64 [tests] Troubles with HHVM <= 3.6.6 and float timeouts.
HHVM is still being used by Travis CI but this bug makes the build to
take 14 minutes to complete, which is unacceptable.
2016-05-30 17:00:14 +02:00
Daniele Alessandri d9ea0ce14f Update README of test directory.
[ci skip]
2016-05-27 22:01:45 +02:00
Daniele Alessandri 55f940e400 Update README.
[ci skip]
2016-05-27 21:32:47 +02:00
Daniele Alessandri 648be824d3 Fix fetching slots map from unreachable nodes.
When various nodes in the configuration are unreachable while trying
to send a command, we should attempt to contact a reachable node to
fetch an updated slots map up to $retryLimit times or until there are
no more servers in the pool before giving up.

It is possible that the slots map fetched from Redis contains stale
data and points to a dead server, this happens when the nodes still
have to agree that a master server is down before promoting a slave
to the role of master. In this case no further attempts to execute
the command are performed and an exception is thrown.

This still needs some more testing and will delay v1.0.4 a few days
past its scheduled release.
2016-05-27 14:28:12 +02:00
Daniele Alessandri c4e0f2f037 Run php-cs-fixer. 2016-05-26 09:52:18 +02:00
Daniele Alessandri 62f5ce6baf Update CHANGELOG. 2016-05-25 10:57:01 +02:00
Daniele Alessandri 9cd4ed9e1e Update SPOP's @method signature in phpdoc.
SPOP accepts the optional "count" argument since Redis 3.2.
2016-05-24 23:28:40 +02:00
Daniele Alessandri 8491b1d880 Add new command: GEORADIUSBYMEMBER (Redis 3.2.0). 2016-05-24 23:28:23 +02:00
Daniele Alessandri 8c8261432f Add new command: GEORADIUS (Redis 3.2.0). 2016-05-24 23:28:11 +02:00
Daniele Alessandri 5eccd6ee3c Add new command: GEODIST (Redis 3.2.0). 2016-05-24 23:28:01 +02:00
Daniele Alessandri 3d9588ec1a Add new command: GEOPOS (Redis 3.2.0). 2016-05-24 23:27:50 +02:00
Daniele Alessandri e96f7f748a Add new command: GEOHASH (Redis 3.2.0). 2016-05-24 23:27:33 +02:00
Daniele Alessandri dd0edf761a Add new command: GEOADD (Redis 3.2.0). 2016-05-24 23:27:10 +02:00
Daniele Alessandri 8cbbd073cb Add new command: BITFIELD (Redis 3.2.0). 2016-05-24 23:26:48 +02:00
Daniele Alessandri 1e521b56cd [tests] Test count argument for SPOP in Redis 3.2. 2016-05-24 23:25:46 +02:00
Daniele Alessandri fcacd65576 Add new server profile for Redis 3.2 (new stable). 2016-05-24 23:25:31 +02:00
Daniele Alessandri d5488f81f7 Check that STORE key in SORT ends up in same slot. 2016-05-24 22:09:13 +02:00
Daniele Alessandri 188dc6bf62 Improve detection of STORE in SORT command arguments. 2016-05-24 21:39:26 +02:00
Daniele Alessandri 0bd57ab744 [tests] Add missing test for key prefix in HSTRLEN. 2016-05-24 15:28:39 +02:00
Daniele Alessandri b996b005cf Fix cluster strategy to handle variadic EXISTS (Redis >= 3.0.3). 2016-05-24 15:26:35 +02:00
Daniele Alessandri 82116b0366 Fix prefix processor to handle variadic EXISTS (Redis >= 3.0.3).
Backported from master.

Conflicts:
	src/Command/Processor/KeyPrefixProcessor.php
2016-05-24 15:26:01 +02:00
Daniele Alessandri 5c1ddc9cfa Add missing @method tag for HSTRLEN in phpdocs.
[ci skip]
2016-05-24 12:26:50 +02:00
Daniele Alessandri ea2fd9700d [tests] Missing @requiresRedisVersion for HSTRLEN. 2016-05-24 11:50:15 +02:00
Daniele Alessandri b2ec264ff9 Update description in package.ini. 2016-05-24 10:27:37 +02:00
Daniele Alessandri bf7a70d6f0 Update phpunit.xml.* 2016-05-22 20:21:52 +02:00
Daniele Alessandri 4ecc44e1ee Update description in composer.json. 2016-05-22 20:05:31 +02:00
Daniele Alessandri 52d5b23db7 Fix README.
[ci skip]
2016-05-22 15:32:08 +02:00
Daniele Alessandri d31a4c71ba Update CHANGELOG.
[ci skip]
2016-05-22 15:24:39 +02:00
Daniele Alessandri c92e305852 Update README.
Blame copy&paste practices for the noise (or just blame me).

[ci skip]
2016-05-22 15:21:09 +02:00
Daniele Alessandri b075564278 Update and improve README.
[ci skip]
2016-05-22 15:12:14 +02:00
Daniele Alessandri f9e817f14c Run php-cs-fixer. 2016-05-21 15:49:25 +02:00
Daniele Alessandri 9e568396c6 Remove old references about HHVM being unstable. 2016-05-20 20:35:58 +02:00
Daniele Alessandri 2a91ebaba9 [tests] Rename old branch for exclusion.
[ci skip]
2016-05-20 20:15:12 +02:00
Daniele Alessandri ce15bd22b9 Update badges in CHANGELOG.
[ci skip]
2016-05-20 18:33:28 +02:00
Daniele Alessandri b0439e503e Update CHANGELOG. 2016-05-20 16:28:19 +02:00
Daniele Alessandri ffc1c25baa Try again on connection failure to node in cluster.
When the connection to a node in the cluster fails in the attempt to
execute a command, Predis now removes the failed connection from the
cluster pool and contacts a random node to ask for a fresh slots map
and tries to execute the command once again.

When the cluster is configured to have each master replicated to one
or more slaves, one the slaves is automatically promoted to the role
of master by redis-cluster with this change being reflected in the
output of CLUSTER SLOTS, so the next execution should run just fine.

Our current approach is relatively naive as CLUSTER SLOTS is executed
against a random master node, meaning that the client must open a new
connection and execute one more roundtrip only to fetch the new slots
map. For now it is enough, it is still better than having the client
fail when you actually have somes slaves in your redis-cluster setup,
but one improvement could consist in caching the list of slaves for
each master returned in the response of CLUSTER SLOTS so that when a
connection fails the client can try to guess which connection should
use for the next attempt.

This commit closes #173, closes #215, and closes #314.
2016-05-20 13:18:25 +02:00
Daniele Alessandri ac165194f5 Evict connection from slots cache when removed. 2016-05-20 11:43:15 +02:00
Daniele Alessandri 18ae294864 [tests] Fix assertion.
PHPUnit_Util_Type::export() has been removed a while ago...
2016-05-19 11:46:56 +02:00
Daniele Alessandri 47de667aea Bump min. version of PHPUnit (require-dev). 2016-05-19 11:46:52 +02:00
Daniele Alessandri 588940c5cb Update CHANGELOG. 2016-05-18 21:27:37 +02:00
Daniele Alessandri a1fe4013e1 Improve handling of slots mapping via parameters.
When using redis-cluster it is now also possible to pass one slot or
non-contiguous ranges of slots via connection parameters in order to
improve the ability to pre-configure the slots map on the client.

Here is an example:

  $parameters = [
    'tcp://10.0.0.1:6379?slots=0-5460,5500-5600,11000',
    'tcp://10.0.0.2:6379?slots=5461-5499,5600-10921',
    'tcp://10.0.0.3:6379?slots=10922-10999,11001-16383',
  ];

This commit fixes #312 (props to @kenotr0n for the original PR).
2016-05-18 21:16:42 +02:00
Daniele Alessandri 3328ad76be [tests] Fix wrong method in mock. 2016-05-16 15:38:42 +02:00
Daniele Alessandri 17b717a8f5 Fix CHANGELOG. 2016-05-15 21:42:00 +02:00
Daniele Alessandri 95dbc2bb34 Bump year in LICENSE. 2016-05-15 21:40:46 +02:00
Daniele Alessandri 9bb5a1c71f Apply minor styling fix. 2016-05-15 21:40:14 +02:00
Daniele Alessandri 8eb4804a5c Move member variable initialization. 2016-05-15 21:39:51 +02:00
Daniele Alessandri 5d99914939 Prevent warnings picking slave from empty pool. 2016-05-15 21:39:34 +02:00
Ante Lucic c45bb15767 add docblocks to MasterSlaveReplication 2016-05-13 17:00:27 +02:00
Ante Braovic e0329d6c74 added zrevrangebylex in the list of available methods 2016-05-13 16:43:50 +02:00
Daniele Alessandri 2175d87a96 [tests] Fix test executed on current Redis unstable. 2016-05-10 10:59:40 +02:00
Daniele Alessandri ce85fdadd1 [tests] Fix test executed on current Redis unstable. 2016-05-10 10:59:24 +02:00
Daniele Alessandri b15c0a02de [tests] Fix wrong @group annotation 2016-05-09 17:50:22 +02:00
Daniele Alessandri d2fbcc6b8c Prevent failures serializing commands with "holes" in arguments array.
This could be triggered when passing an array with "holes" to variadic commands.
Connection classes based on the protocol serialized exposed by phpiredis were
not affected by this bug.

Fixes #316.
2016-05-08 18:35:06 +02:00
Daniele Alessandri 6596460a57 Merge branch 'issue-tests' into v1.0 2016-05-08 16:50:28 +02:00
Daniele Alessandri 9ab0e31a75 Merge branch 'v1.0-fix-phpDoc-in-ClientInterface' into v1.0 2016-05-08 16:45:49 +02:00
John Maguire 11adccdaeb Update useClusterNodes() docs to indicate default 2016-05-08 15:21:01 +02:00
Daniele Alessandri f465792b27 Update CHANGELOG. 2016-05-08 15:15:48 +02:00
Daniele Alessandri 1399d3863c Fix bug in HSCAN-based iterator when hash have integer fields.
When iterating a hash containing integer fields our iterator abstraction
based on HSCAN was always returning "0" as a field name after the first
$field => $value pair due to a wrong assumption on how the PHP function
array_shift() (which is used internally to advance to the next pair in
our buffered response to HSCAN) works.

The ZSCAN-based iterator had this very same bug which was already fixed
in 24e19a9 so I am not sure how this one went unnoticed until now.
2016-05-08 15:10:09 +02:00
Daniele Alessandri 1f1c9cf1a4 Run php-cs-fixer. 2016-05-08 14:54:29 +02:00
Daniele Alessandri dae2df0ae9 Merge remote-tracking branch 'marvin255/fix_namespace_typo' into v1.0 2016-05-08 14:49:06 +02:00
postbox433@gmail.com 9338eaf573 Fix namespace typo in StreamConnection.php 2016-04-12 15:20:26 +05:00
Remi Collet f378faaf22 fix tests, list of allowed commands have changed (redis 3.0.6) 2015-12-28 10:34:24 +01:00
Alexander Cheprasov 8b150fcbf5 Update ClientInterface.php 2015-10-27 16:37:58 +00:00
65 changed files with 2952 additions and 192 deletions
+1 -3
View File
@@ -11,7 +11,7 @@ branches:
except:
- v0.5
- v0.6
- php5.2_backport
- v0.6-PHP_5.2
- documentation
services: redis-server
before_script:
@@ -20,6 +20,4 @@ before_script:
script:
- vendor/bin/phpunit -c phpunit.xml.travisci
matrix:
allow_failures:
- php: hhvm
fast_finish: true
+28
View File
@@ -1,3 +1,31 @@
v1.0.4 (2016-05-30)
================================================================================
- Added new profile for Redis 3.2 with its new commands: `HSTRLEN`, `BITFIELD`,
`GEOADD`, `GEOHASH`, `GEOPOS`, `GEODIST`, `GEORADIUS`, `GEORADIUSBYMEMBER`.
The default server profile for Predis is still the one for Redis 3.0 you must
set the `profile` client option to `3.2` when initializing the client in order
to be able to use them when connecting to Redis 3.2.
- Various improvements in the handling of redis-cluster:
- If the connection to a specific node fails when executing a command, the
client tries to connect to another node in order to refresh the slots map
and perform a new attempt to execute the command.
- Connections to nodes can be preassigned to non-contiguous slot ranges via
the `slots` parameter using a comma separator. This is how it looks like
in practice: `tcp://127.0.0.1:6379?slots=0-5460,5500-5600,11000`.
- __FIX__: broken values returned by `Predis\Collection\Iterator\HashKey` when
iterating hash keys containing integer fields (PR #330, ISSUE #331).
- __FIX__: prevent failures when `Predis\Connection\StreamConnection` serializes
commands with holes in their arguments (e.g. `[0 => 'key:0', 2 => 'key:2']`).
The same fix has been applied to `Predis\Protocol\Text\RequestSerializer`.
(ISSUE #316).
v1.0.3 (2015-07-30)
================================================================================
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009-2015 Daniele Alessandri
Copyright (c) 2009-2016 Daniele Alessandri
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
+114 -87
View File
@@ -1,50 +1,46 @@
# Predis #
[![Latest Stable Version](https://poser.pugx.org/predis/predis/v/stable.png)](https://packagist.org/packages/predis/predis)
[![Total Downloads](https://poser.pugx.org/predis/predis/downloads.png)](https://packagist.org/packages/predis/predis)
[![License](https://poser.pugx.org/predis/predis/license.svg)](https://packagist.org/packages/predis/predis)
[![Build Status](https://travis-ci.org/nrk/predis.svg?branch=v1.0)](https://travis-ci.org/nrk/predis)
[![HHVM Status](http://hhvm.h4cc.de/badge/predis/predis.png)](http://hhvm.h4cc.de/package/predis/predis)
[![Latest stable][ico-version-stable]][link-packagist]
[![Latest development][ico-version-dev]][link-packagist]
[![Software license][ico-license]](LICENSE)
[![Monthly installs][ico-downloads-monthly]][link-downloads]
[![Build status][ico-travis]][link-travis]
[![HHVM support][ico-hhvm]][link-hhvm]
Predis is a flexible and feature-complete [Redis](http://redis.io) client library for PHP >= 5.3.
Flexible and feature-complete [Redis](http://redis.io) client for PHP >= 5.3 and HHVM >= 2.3.0.
By default this library does not require any additional C extension, but it can be optionally paired
with [phpiredis](https://github.com/nrk/phpiredis) to lower the overhead of serializing and parsing
the [Redis RESP Protocol](http://redis.io/topics/protocol). An asynchronous implementation of Predis
is available through [Predis\Async](https://github.com/nrk/predis-async) (__experimental__).
Predis does not require any additional C extension by default, but it can be optionally paired with
[phpiredis](https://github.com/nrk/phpiredis) to lower the overhead of the serialization and parsing
of the [Redis RESP Protocol](http://redis.io/topics/protocol). For an __experimental__ asynchronous
implementation of the client you can refer to [Predis\Async](https://github.com/nrk/predis-async).
Predis can be used with [HHVM](http://www.hhvm.com) >= 2.3.0 but there are no guarantees you will
not run into unexpected issues (especially when the JIT compiler is enabled via `Eval.Jit = true`)
due to HHVM being still under heavy development and not yet 100% compatible with the _standard_ PHP.
More details about this project can be found on the [frequently asked questions](FAQ.md) and on the
[wiki](https://github.com/nrk/predis/wiki).
More details about this project can be found on the [frequently asked questions](FAQ.md).
## Main features ##
- Support for a wide range of Redis versions (from __2.0__ to __3.0__) using profiles.
- Clustering via client-side sharding using consistent hashing or custom distributors.
- Smart support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0).
- Support for master-slave replication (write operations on master, read operations on slaves).
- Transparent key prefixing for all known Redis commands using a customizable prefixing strategy.
- Command pipelining (works on both single nodes and aggregate connections).
- Abstraction for Redis transactions (Redis >= 2.0) supporting CAS operations (Redis >= 2.2).
- Abstraction for Lua scripting (Redis >= 2.6) with automatic switching between `EVALSHA` or `EVAL`.
- Support for different versions of Redis (from __2.0__ to __3.2__) using profiles.
- Support for clustering using client-side sharding and pluggable keyspace distributors.
- Support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0).
- Support for standalone master-slave replication setups.
- Transparent key prefixing of keys using a customizable prefix strategy.
- Command pipelining on both single nodes and clusters (client-side sharding only).
- Abstraction for Redis transactions (Redis >= 2.0) and CAS operations (Redis >= 2.2).
- Abstraction for Lua scripting (Redis >= 2.6) and automatic switching between `EVALSHA` or `EVAL`.
- Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators.
- Connections to Redis are established lazily by the client upon the first command.
- Support for both TCP/IP and UNIX domain sockets and persistent connections.
- Connections are established lazily by the client upon the first command and can be persisted.
- Connections can be established via TCP/IP or UNIX domain sockets.
- Support for [Webdis](http://webd.is) (requires both `ext-curl` and `ext-phpiredis`).
- Support for custom connection classes for providing different network or protocol backends.
- Flexible system for defining custom commands and server profiles.
## How to use Predis ##
## How to _install_ and use Predis ##
Predis is available on [Packagist](http://packagist.org/packages/predis/predis) which allows a quick
_installation_ using [Composer](http://packagist.org/about-composer). Alternatively, the library can
be found on our [own PEAR channel](http://pear.nrk.io) for a more traditional installation via PEAR.
Ultimately, archives of each release are [available on GitHub](https://github.com/nrk/predis/tags).
This library can be found on [Packagist](http://packagist.org/packages/predis/predis) for an easier
management of projects dependencies using [Composer](http://packagist.org/about-composer) or on our
[own PEAR channel](http://pear.nrk.io) for a more traditional installation using PEAR. Ultimately,
compressed archives of each release are [available on GitHub](https://github.com/nrk/predis/tags).
### Loading the library ###
@@ -52,7 +48,7 @@ Ultimately, archives of each release are [available on GitHub](https://github.co
Predis relies on the autoloading features of PHP to load its files when needed and complies with the
[PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md).
Autoloading is handled automatically when dependencies are managed through Composer, but it is also
possible to leverage its own autoloader in projects or scripts not having any autoload facility:
possible to leverage its own autoloader in projects or scripts lacking any autoload facility:
```php
// Prepend a base path if Predis is not available in your "include_path".
@@ -61,11 +57,9 @@ require 'Predis/Autoloader.php';
Predis\Autoloader::register();
```
It is possible to create a [phar](http://www.php.net/manual/en/intro.phar.php) archive directly from
the repository by launching `bin/create-phar`. The phar contains a stub defining its own autoloader
so you just need to `require()` it to start using the library. Ultimately it is possible to generate
a single big PHP file containing all the source code simply by launching `bin/create-single-file`,
but this practice __is not__ encouraged.
It is also possible to create a [phar](http://www.php.net/manual/en/intro.phar.php) archive directly
from the repository by launching the `bin/create-phar` script. The generated phar already contains a
stub defining its own autoloader, so you just need to `require()` it to start using the library.
### Connecting to Redis ###
@@ -95,8 +89,17 @@ $client = new Predis\Client([
$client = new Predis\Client('tcp://10.0.0.1:6379');
```
Starting with Predis v1.0.2 the client also understands the `redis` scheme in URI strings as defined
by the [provisional IANA registration](http://www.iana.org/assignments/uri-schemes/prov/redis).
It is also possible to connect to local instances of Redis using UNIX domain sockets, in this case
the parameters must use the `unix` scheme and specify a path for the socket file:
```php
$client = new Predis\Client(['scheme' => 'unix', 'path' => '/path/to/redis.sock']);
$client = new Predis\Client('unix:///path/to/redis.sock');
```
The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/prov/redis) (alias of
`tcp`) is also supported, with the difference that URI strings containing these schemes are parsed
following the rules described on the IANA provisional registration.
The actual list of supported connection parameters can vary depending on each connection backend so
it is recommended to refer to their specific documentation or implementation for details.
@@ -117,46 +120,79 @@ See the [aggregate connections](#aggregate-connections) section of this document
### Client configuration ###
Various aspects of the client can be configured simply by passing options to the second argument of
`Predis\Client::__construct()`:
Many aspects and behaviors of the client can be configured by passing specific client options to the
second argument of `Predis\Client::__construct()`:
```php
$client = new Predis\Client($parameters, ['profile' => '2.8', 'prefix' => 'sample:']);
```
Options are managed through a mini DI-alike container while their values can be lazily initialized
only when needed. This is a list of the options supported by default:
Options are managed using a mini DI-alike container and their values can be lazily initialized only
when needed. The client options supported by default in Predis are:
- `profile`: which profile to use in order to match a specific version of Redis.
- `prefix`: a prefix string that is automatically applied to keys found in commands.
- `profile`: specifies the profile to use to match a specific version of Redis.
- `prefix`: prefix string automatically applied to keys found in commands.
- `exceptions`: whether the client should throw or return responses upon Redis errors.
- `connections`: connection backends or a connection factory to be used by the client.
- `cluster`: which backend to use for clustering (`predis`, `redis` or custom configuration).
- `replication`: which backend to use for replication (predis or custom configuration).
- `aggregate`: custom connections aggregator (overrides both `cluster` and `replication`).
- `connections`: list of connection backends or a connection factory instance.
- `cluster`: specifies a cluster backend (`predis`, `redis` or callable object).
- `replication`: specifies a replication backend (`TRUE` or callable object).
- `aggregate`: overrides `cluster` and `replication` to provide a custom connections aggregator.
Users can provide custom options with their values or lazy callable initializers that are stored in
the options container for later use through the library.
Users can also provide custom options with values or callable objects (for lazy initialization) that
are stored in the options container for later use through the library.
### Aggregate connections ###
Predis is able to aggregate multiple connections which is the base for clustering and replication.
By default the client implements a cluster of nodes using either client-side sharding (default) or
a Redis-backed solution using [redis-cluster](http://redis.io/topics/cluster-tutorial).
As for replication, Predis can handle a single-master and multiple-slaves setup by executing read
operations on slaves and switching to the master only for write operations. The replication behavior
is fully configurable.
Aggregate connections are the foundation upon which Predis implements clustering and replication and
they are used to group multiple connections to single Redis nodes and hide the specific logic needed
to handle them properly depending on the context. Aggregate connections usually require an array of
connection parameters when creating a new client instance.
#### Cluster ####
By default, when no specific client options are set and an array of connection parameters is passed
to the client's constructor, Predis configures itself to work in clustering mode using a traditional
client-side sharding approach to create a cluster of independent nodes and distribute the keyspace
among them. This approach needs some form of external health monitoring of nodes and requires manual
operations to rebalance the keyspace when changing its configuration by adding or removing nodes:
```php
$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$client = new Predis\Client($parameters);
```
Along with Redis 3.0, a new supervised and coordinated type of clustering was introduced in the form
of [redis-cluster](http://redis.io/topics/cluster-tutorial). This kind of approach uses a different
algorithm to distribute the keyspaces, with Redis nodes coordinating themselves by communicating via
a gossip protocol to handle health status, rebalancing, nodes discovery and request redirection. In
order to connect to a cluster managed by redis-cluster, the client requires a list of its nodes (not
necessarily complete since it will automatically discover new nodes if necessary) and the `cluster`
client options set to `redis`:
```php
$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['cluster' => 'redis'];
$client = new Predis\Client($parameters, $options);
```
#### Replication ####
The client can be configured to operate in a master-slave setup by executing read-only commands on
slave nodes and automatically switch to the master node as soon as it detects a command that will
perform a write operation. This is the basic configuration needed to work with replication:
The client can be configured to operate in a single master / multiple slaves setup to provide better
service availability. When using replication, Predis recognizes read-only commands and sends them to
a random slave in order to provide some sort of load-balancing and switches to the master as soon as
it detects a command that performs any kind of operation that would end up modifying the keyspace or
the value of a key. Instead of raising a connection error when a slave fails, the client attempts to
fall back to a different slave among the ones provided in the configuration.
The basic configuration needed to use the client in replication mode requires one Redis server to be
identified as the master (this can be done via connection parameters using the `alias` parameter set
to `master`) and one or more servers acting as slaves:
```php
// Parameters require one master node specifically marked with `alias=master`.
$parameters = ['tcp://10.0.0.1?alias=master', 'tcp://10.0.0.2?alias=slave-01'];
$parameters = ['tcp://10.0.0.1?alias=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['replication' => true];
$client = new Predis\Client($parameters, $options);
@@ -169,7 +205,7 @@ when certain Lua scripts do not perform write operations it is possible to provi
the client to stick with slaves for their execution:
```php
$parameters = ['tcp://10.0.0.1?alias=master', 'tcp://10.0.0.2?alias=slave-01'];
$parameters = ['tcp://10.0.0.1?alias=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['replication' => function () {
// Set scripts that won't trigger a switch from a slave to the master node.
$strategy = new Predis\Replication\ReplicationStrategy();
@@ -183,29 +219,8 @@ $client->eval($LUA_SCRIPT, 0); // Sticks to slave using `eval`...
$client->evalsha(sha1($LUA_SCRIPT), 0); // ... and `evalsha`, too.
```
The `examples` directory contains two complete scripts showing how replication can be configured for
[basic](examples/replication_simple.php) and [complex](examples/replication_complex.php) scenarios.
#### Cluster ####
Simply passing an array of connection parameters to the client constructor configures Predis to work
in cluster mode using client-side sharding. If you, on the other hand, want to leverage Redis >= 3.0
nodes coordinated by redis-cluster, then the client must be initialized like this:
```php
$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2'];
$options = ['cluster' => 'redis'];
$client = new Predis\Client($parameters, $options);
```
When using redis-cluster it is not necessary to pass all of the nodes that compose your cluster, you
can specify only a few nodes and the client will automatically fetch the full and updated slots map
directly from Redis by contacting one of the servers.
__NOTE__: our support for redis-cluster does not currently consider master / slave replication but
this feature will be added in a future release of the library.
The [`examples`](examples/) directory contains a few scripts that demonstrate how the client can be
configured and used to leverage replication in both basic and complex scenarios.
### Command pipelines ###
@@ -286,7 +301,7 @@ $response = $client->executeRaw(['SET', 'foo', 'bar']);
### Script commands ###
While it is possible to leverage [Lua scripting](http://redis.io/commands/eval) on Redis 2.6+ using
[`EVAL`](http://redis.io/commands/eval) and [`EVALSHA`](http://redis.io/commands/evalsha) directly,
directly [`EVAL`](http://redis.io/commands/eval) and [`EVALSHA`](http://redis.io/commands/evalsha),
Predis offers script commands as an higher level abstraction built upon them to make things simple.
Script commands can be registered in the server profile used by the client and are accessible as if
they were plain Redis commands, but they define Lua scripts that get transmitted to the server for
@@ -407,3 +422,15 @@ found [on its project page](http://travis-ci.org/nrk/predis).
### License ###
The code for Predis is distributed under the terms of the MIT license (see [LICENSE](LICENSE)).
[ico-license]: https://img.shields.io/github/license/nrk/predis.svg?style=flat-square
[ico-version-stable]: https://img.shields.io/packagist/v/predis/predis.svg?style=flat-square
[ico-version-dev]: https://img.shields.io/packagist/vpre/predis/predis.svg?style=flat-square
[ico-downloads-monthly]: https://img.shields.io/packagist/dm/predis/predis.svg?style=flat-square
[ico-travis]: https://img.shields.io/travis/nrk/predis.svg?style=flat-square
[ico-hhvm]: https://img.shields.io/hhvm/predis/predis.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/predis/predis
[link-travis]: https://travis-ci.org/nrk/predis
[link-downloads]: https://packagist.org/packages/predis/predis/stats
[link-hhvm]: http://hhvm.h4cc.de/package/predis/predis
+1 -1
View File
@@ -1 +1 @@
1.0.3
1.0.4
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "predis/predis",
"type": "library",
"description": "Flexible and feature-complete PHP client library for Redis",
"description": "Flexible and feature-complete Redis client for PHP and HHVM",
"keywords": ["nosql", "redis", "predis"],
"homepage": "http://github.com/nrk/predis",
"license": "MIT",
@@ -19,7 +19,7 @@
"php": ">=5.3.2"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
"phpunit/phpunit": "~4.8"
},
"suggest": {
"ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol",
+2 -2
View File
@@ -7,10 +7,10 @@
[package]
name = "Predis"
desc = "Flexible and feature-complete PHP client library for Redis"
desc = "Flexible and feature-complete Redis client for PHP and HHVM"
homepage = "http://github.com/nrk/predis"
license = "MIT"
version = "1.0.3"
version = "1.0.4"
stability = "stable"
channel = "pear.nrk.io"
+10 -4
View File
@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
beStrictAboutTestSize="true"
checkForUnintentionallyCoveredCode="true"
beStrictAboutTestsThatDoNotTestAnything="true">
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
syntaxCheck="true"
beStrictAboutTestSize="true"
beStrictAboutTestsThatDoNotTestAnything="true">
<testsuites>
<testsuite name="Predis Test Suite">
+10 -4
View File
@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
beStrictAboutTestSize="true"
checkForUnintentionallyCoveredCode="true"
beStrictAboutTestsThatDoNotTestAnything="true">
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
syntaxCheck="true"
beStrictAboutTestSize="true"
beStrictAboutTestsThatDoNotTestAnything="true">
<testsuites>
<testsuite name="Predis Test Suite">
+1 -1
View File
@@ -40,7 +40,7 @@ use Predis\Transaction\MultiExec as MultiExecTransaction;
*/
class Client implements ClientInterface
{
const VERSION = '1.0.3';
const VERSION = '1.0.4';
protected $connection;
protected $options;
+10 -1
View File
@@ -38,6 +38,7 @@ use Predis\Command\CommandInterface;
* @method $this append($key, $value)
* @method $this bitcount($key, $start = null, $end = null)
* @method $this bitop($operation, $destkey, $key)
* @method $this bitfield($key, ...)
* @method $this decr($key)
* @method $this decrby($key, $decrement)
* @method $this get($key)
@@ -71,6 +72,7 @@ use Predis\Command\CommandInterface;
* @method $this hset($key, $field, $value)
* @method $this hsetnx($key, $field, $value)
* @method $this hvals($key)
* @method $this hstrlen($key, $field)
* @method $this blpop(array $keys, $timeout)
* @method $this brpop(array $keys, $timeout)
* @method $this brpoplpush($source, $destination, $timeout)
@@ -97,7 +99,7 @@ use Predis\Command\CommandInterface;
* @method $this sismember($key, $member)
* @method $this smembers($key)
* @method $this smove($source, $destination, $member)
* @method $this spop($key)
* @method $this spop($key, $count = null)
* @method $this srandmember($key, $count = null)
* @method $this srem($key, $member)
* @method $this sscan($key, $cursor, array $options = null)
@@ -121,6 +123,7 @@ use Predis\Command\CommandInterface;
* @method $this zscore($key, $member)
* @method $this zscan($key, $cursor, array $options = null)
* @method $this zrangebylex($key, $start, $stop, array $options = null)
* @method $this zrevrangebylex($key, $start, $stop, array $options = null)
* @method $this zremrangebylex($key, $min, $max)
* @method $this zlexcount($key, $min, $max)
* @method $this pfadd($key, array $elements)
@@ -154,6 +157,12 @@ use Predis\Command\CommandInterface;
* @method $this slowlog($subcommand, $argument = null)
* @method $this time()
* @method $this command()
* @method $this geoadd($key, $longitude, $latitude, $member)
* @method $this geohash($key, array $members)
* @method $this geopos($key, array $members)
* @method $this geodist($key, $member1, $member2, $unit = null)
* @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
* @method $this georadiusbymember($key, $member, $radius, $unit, array $options = null)
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
+11 -2
View File
@@ -46,6 +46,7 @@ use Predis\Profile\ProfileInterface;
* @method int append($key, $value)
* @method int bitcount($key, $start = null, $end = null)
* @method int bitop($operation, $destkey, $key)
* @method array bitfield($key, ...)
* @method int decr($key)
* @method int decrby($key, $decrement)
* @method string get($key)
@@ -79,6 +80,7 @@ use Predis\Profile\ProfileInterface;
* @method int hset($key, $field, $value)
* @method int hsetnx($key, $field, $value)
* @method array hvals($key)
* @method int hstrlen($key, $field)
* @method array blpop(array $keys, $timeout)
* @method array brpop(array $keys, $timeout)
* @method array brpoplpush($source, $destination, $timeout)
@@ -105,7 +107,7 @@ use Predis\Profile\ProfileInterface;
* @method int sismember($key, $member)
* @method array smembers($key)
* @method int smove($source, $destination, $member)
* @method string spop($key)
* @method string spop($key, $count = null)
* @method string srandmember($key, $count = null)
* @method int srem($key, $member)
* @method array sscan($key, $cursor, array $options = null)
@@ -123,12 +125,13 @@ use Predis\Profile\ProfileInterface;
* @method int zremrangebyrank($key, $start, $stop)
* @method int zremrangebyscore($key, $min, $max)
* @method array zrevrange($key, $start, $stop, array $options = null)
* @method array zrevrangebyscore($key, $min, $max, array $options = null)
* @method array zrevrangebyscore($key, $max, $min, array $options = null)
* @method int zrevrank($key, $member)
* @method int zunionstore($destination, array $keys, array $options = null)
* @method string zscore($key, $member)
* @method array zscan($key, $cursor, array $options = null)
* @method array zrangebylex($key, $start, $stop, array $options = null)
* @method array zrevrangebylex($key, $start, $stop, array $options = null)
* @method int zremrangebylex($key, $min, $max)
* @method int zlexcount($key, $min, $max)
* @method int pfadd($key, array $elements)
@@ -162,6 +165,12 @@ use Predis\Profile\ProfileInterface;
* @method mixed slowlog($subcommand, $argument = null)
* @method array time()
* @method array command()
* @method int geoadd($key, $longitude, $latitude, $member)
* @method array geohash($key, array $members)
* @method array geopos($key, array $members)
* @method string geodist($key, $member1, $member2, $unit = null)
* @method array georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
* @method array georadiusbymember($key, $member, $radius, $unit, array $options = null)
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
+73 -2
View File
@@ -43,7 +43,7 @@ abstract class ClusterStrategy implements StrategyInterface
return array(
/* commands operating on the key space */
'EXISTS' => $getKeyFromFirstArgument,
'EXISTS' => $getKeyFromAllArguments,
'DEL' => $getKeyFromAllArguments,
'TYPE' => $getKeyFromFirstArgument,
'EXPIRE' => $getKeyFromFirstArgument,
@@ -53,7 +53,7 @@ abstract class ClusterStrategy implements StrategyInterface
'PEXPIREAT' => $getKeyFromFirstArgument,
'TTL' => $getKeyFromFirstArgument,
'PTTL' => $getKeyFromFirstArgument,
'SORT' => $getKeyFromFirstArgument, // TODO
'SORT' => array($this, 'getKeyFromSortCommand'),
'DUMP' => $getKeyFromFirstArgument,
'RESTORE' => $getKeyFromFirstArgument,
@@ -80,6 +80,7 @@ abstract class ClusterStrategy implements StrategyInterface
'SUBSTR' => $getKeyFromFirstArgument,
'BITOP' => array($this, 'getKeyFromBitOp'),
'BITCOUNT' => $getKeyFromFirstArgument,
'BITFIELD' => $getKeyFromFirstArgument,
/* commands operating on lists */
'LINSERT' => $getKeyFromFirstArgument,
@@ -164,6 +165,14 @@ abstract class ClusterStrategy implements StrategyInterface
/* scripting */
'EVAL' => array($this, 'getKeyFromScriptingCommands'),
'EVALSHA' => array($this, 'getKeyFromScriptingCommands'),
/* commands performing geospatial operations */
'GEOADD' => $getKeyFromFirstArgument,
'GEOHASH' => $getKeyFromFirstArgument,
'GEOPOS' => $getKeyFromFirstArgument,
'GEODIST' => $getKeyFromFirstArgument,
'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'),
'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'),
);
}
@@ -261,6 +270,35 @@ abstract class ClusterStrategy implements StrategyInterface
}
}
/**
* Extracts the key from SORT command.
*
* @param CommandInterface $command Command instance.
*
* @return string|null
*/
protected function getKeyFromSortCommand(CommandInterface $command)
{
$arguments = $command->getArguments();
$firstKey = $arguments[0];
if (1 === $argc = count($arguments)) {
return $firstKey;
}
$keys = array($firstKey);
for ($i = 1; $i < $argc; ++$i) {
if (strtoupper($arguments[$i]) === 'STORE') {
$keys[] = $arguments[++$i];
}
}
if ($this->checkSameSlotForKeys($keys)) {
return $firstKey;
}
}
/**
* Extracts the key from BLPOP and BRPOP commands.
*
@@ -293,6 +331,39 @@ abstract class ClusterStrategy implements StrategyInterface
}
}
/**
* Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands.
*
* @param CommandInterface $command Command instance.
*
* @return string|null
*/
protected function getKeyFromGeoradiusCommands(CommandInterface $command)
{
$arguments = $command->getArguments();
$argc = count($arguments);
$startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
if ($argc > $startIndex) {
$keys = array($arguments[0]);
for ($i = $startIndex; $i < $argc; ++$i) {
$argument = strtoupper($arguments[$i]);
if ($argument === 'STORE' || $argument === 'STOREDIST') {
$keys[] = $arguments[++$i];
}
}
if ($this->checkSameSlotForKeys($keys)) {
return $arguments[0];
} else {
return;
}
}
return $arguments[0];
}
/**
* Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
*
+6 -2
View File
@@ -50,7 +50,11 @@ class HashKey extends CursorBasedIterator
*/
protected function extractNext()
{
$this->position = key($this->elements);
$this->current = array_shift($this->elements);
if ($kv = each($this->elements)) {
$this->position = $kv[0];
$this->current = $kv[1];
unset($this->elements[$this->position]);
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?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\Command;
/**
* @link http://redis.io/commands/geoadd
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoAdd extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEOADD';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
foreach (array_pop($arguments) as $item) {
$arguments = array_merge($arguments, $item);
}
}
return $arguments;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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\Command;
/**
* @link http://redis.io/commands/geodist
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoDist extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEODIST';
}
}
+41
View File
@@ -0,0 +1,41 @@
<?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\Command;
/**
* @link http://redis.io/commands/geohash
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoHash extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEOHASH';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
$members = array_pop($arguments);
$arguments = array_merge($arguments, $members);
}
return $arguments;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?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\Command;
/**
* @link http://redis.io/commands/geopos
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoPos extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEOPOS';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
$members = array_pop($arguments);
$arguments = array_merge($arguments, $members);
}
return $arguments;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?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\Command;
/**
* @link http://redis.io/commands/georadius
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoRadius extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEORADIUS';
}
/**
* {@inheritdoc}
*/
protected function filterArguments(array $arguments)
{
if ($arguments && is_array(end($arguments))) {
$options = array_change_key_case(array_pop($arguments), CASE_UPPER);
if (isset($options['WITHCOORD']) && $options['WITHCOORD'] == true) {
$arguments[] = 'WITHCOORD';
}
if (isset($options['WITHDIST']) && $options['WITHDIST'] == true) {
$arguments[] = 'WITHDIST';
}
if (isset($options['WITHHASH']) && $options['WITHHASH'] == true) {
$arguments[] = 'WITHHASH';
}
if (isset($options['COUNT'])) {
$arguments[] = 'COUNT';
$arguments[] = $options['COUNT'];
}
if (isset($options['SORT'])) {
$arguments[] = strtoupper($options['SORT']);
}
if (isset($options['STORE'])) {
$arguments[] = 'STORE';
$arguments[] = $options['STORE'];
}
if (isset($options['STOREDIST'])) {
$arguments[] = 'STOREDIST';
$arguments[] = $options['STOREDIST'];
}
}
return $arguments;
}
}
@@ -0,0 +1,28 @@
<?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\Command;
/**
* @link http://redis.io/commands/georadiusbymember
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class GeospatialGeoRadiusByMember extends GeospatialGeoRadius
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'GEORADIUSBYMEMBER';
}
}
+37 -2
View File
@@ -33,7 +33,7 @@ class KeyPrefixProcessor implements ProcessorInterface
$this->prefix = $prefix;
$this->commands = array(
/* ---------------- Redis 1.2 ---------------- */
'EXISTS' => 'static::first',
'EXISTS' => 'static::all',
'DEL' => 'static::all',
'TYPE' => 'static::first',
'KEYS' => 'static::first',
@@ -159,6 +159,13 @@ class KeyPrefixProcessor implements ProcessorInterface
'BITPOS' => 'static::first',
/* ---------------- Redis 3.2 ---------------- */
'HSTRLEN' => 'static::first',
'BITFIELD' => 'static::first',
'GEOADD' => 'static::first',
'GEOHASH' => 'static::first',
'GEOPOS' => 'static::first',
'GEODIST' => 'static::first',
'GEORADIUS' => 'static::georadius',
'GEORADIUSBYMEMBER' => 'static::georadius',
);
}
@@ -338,7 +345,7 @@ class KeyPrefixProcessor implements ProcessorInterface
if (($count = count($arguments)) > 1) {
for ($i = 1; $i < $count; ++$i) {
switch ($arguments[$i]) {
switch (strtoupper($arguments[$i])) {
case 'BY':
case 'STORE':
$arguments[$i] = "$prefix{$arguments[++$i]}";
@@ -412,4 +419,32 @@ class KeyPrefixProcessor implements ProcessorInterface
$command->setRawArguments($arguments);
}
}
/**
* Applies the specified prefix to the key of a GEORADIUS command.
*
* @param CommandInterface $command Command instance.
* @param string $prefix Prefix string.
*/
public static function georadius(CommandInterface $command, $prefix)
{
if ($arguments = $command->getArguments()) {
$arguments[0] = "$prefix{$arguments[0]}";
$startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
if (($count = count($arguments)) > $startIndex) {
for ($i = $startIndex; $i < $count; ++$i) {
switch (strtoupper($arguments[$i])) {
case 'STORE':
case 'STOREDIST':
$arguments[$i] = "$prefix{$arguments[++$i]}";
break;
}
}
}
$command->setRawArguments($arguments);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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\Command;
/**
* @link http://redis.io/commands/bitfield
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class StringBitField extends Command
{
/**
* {@inheritdoc}
*/
public function getId()
{
return 'BITFIELD';
}
}
+5
View File
@@ -11,6 +11,11 @@
namespace Predis\Command;
/**
* @link http://redis.io/commands/zrevrangebylex
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class ZSetReverseRangeByLex extends ZSetRangeByLex
{
/**
+2 -2
View File
@@ -70,10 +70,10 @@ class Options implements OptionsInterface
*/
public function defined($option)
{
return (
return
array_key_exists($option, $this->options) ||
array_key_exists($option, $this->input)
);
;
}
/**
@@ -23,9 +23,24 @@ use Predis\Replication\ReplicationStrategy;
*/
class MasterSlaveReplication implements ReplicationInterface
{
/**
* @var ReplicationStrategy
*/
protected $strategy;
/**
* @var NodeConnectionInterface
*/
protected $master;
protected $slaves;
/**
* @var NodeConnectionInterface[]
*/
protected $slaves = array();
/**
* @var NodeConnectionInterface
*/
protected $current;
/**
@@ -33,7 +48,6 @@ class MasterSlaveReplication implements ReplicationInterface
*/
public function __construct(ReplicationStrategy $strategy = null)
{
$this->slaves = array();
$this->strategy = $strategy ?: new ReplicationStrategy();
}
@@ -144,6 +158,7 @@ class MasterSlaveReplication implements ReplicationInterface
if (!$connection instanceof NodeConnectionInterface) {
$connection = $this->getConnectionById($connection);
}
if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
throw new \InvalidArgumentException('Invalid connection or connection not found.');
}
@@ -192,7 +207,9 @@ class MasterSlaveReplication implements ReplicationInterface
*/
protected function pickSlave()
{
return $this->slaves[array_rand($this->slaves)];
if ($this->slaves) {
return $this->slaves[array_rand($this->slaves)];
}
}
/**
+132 -9
View File
@@ -11,10 +11,12 @@
namespace Predis\Connection\Aggregate;
use Predis\ClientException;
use Predis\Cluster\RedisStrategy as RedisClusterStrategy;
use Predis\Cluster\StrategyInterface;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\NotSupportedException;
@@ -51,6 +53,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
private $slotsMap;
private $strategy;
private $connections;
private $retryLimit = 5;
/**
* @param FactoryInterface $connections Optional connection factory.
@@ -64,6 +67,20 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
$this->strategy = $strategy ?: new RedisClusterStrategy();
}
/**
* Sets the maximum number of retries for commands upon server failure.
*
* -1 = unlimited retry attempts
* 0 = no retry attempts (fails immediatly)
* n = fail only after n retry attempts
*
* @param int $retry Number of retry attempts.
*/
public function setRetryLimit($retry)
{
$this->retryLimit = (int) $retry;
}
/**
* {@inheritdoc}
*/
@@ -118,6 +135,8 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
$this->slotsMap
);
$this->slots = array_diff($this->slots, array($connection));
return true;
}
@@ -165,11 +184,59 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
continue;
}
$slots = explode('-', $parameters->slots, 2);
$this->setSlots($slots[0], $slots[1], $connectionID);
foreach (explode(',', $parameters->slots) as $slotRange) {
$slots = explode('-', $slotRange, 2);
if (!isset($slots[1])) {
$slots[1] = $slots[0];
}
$this->setSlots($slots[0], $slots[1], $connectionID);
}
}
}
/**
* Queries the specified node of the cluster to fetch the updated slots map.
*
* When the connection fails, this method tries to execute the same command
* on a different connection picked at random from the pool of known nodes,
* up until the retry limit is reached.
*
* @param NodeConnectionInterface $connection Connection to a node of the cluster.
*
* @return mixed
*/
private function queryClusterNodeForSlotsMap(NodeConnectionInterface $connection)
{
$retries = 0;
$command = RawCommand::create('CLUSTER', 'SLOTS');
RETRY_COMMAND: {
try {
$response = $connection->executeCommand($command);
} catch (ConnectionException $exception) {
$connection = $exception->getConnection();
$connection->disconnect();
$this->remove($connection);
if ($retries === $this->retryLimit) {
throw $exception;
}
if (!$connection = $this->getRandomConnection()) {
throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`');
}
++$retries;
goto RETRY_COMMAND;
}
}
return $response;
}
/**
* Generates an updated slots map fetching the cluster configuration using
* the CLUSTER SLOTS command against the specified node or a random one from
@@ -185,8 +252,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
return array();
}
$command = RawCommand::create('CLUSTER', 'SLOTS');
$response = $connection->executeCommand($command);
$this->resetSlotsMap();
$response = $this->queryClusterNodeForSlotsMap($connection);
foreach ($response as $slots) {
// We only support master servers for now, so we ignore subsequent
@@ -203,9 +271,19 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
return $this->slotsMap;
}
/**
* Resets the slots map cache.
*/
public function resetSlotsMap()
{
$this->slotsMap = array();
}
/**
* Returns the current slots map for the cluster.
*
* The order of the returned $slot => $server dictionary is not guaranteed.
*
* @return array
*/
public function getSlotsMap()
@@ -252,6 +330,10 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
protected function guessNode($slot)
{
if (!$this->pool) {
throw new ClientException('No connections available in the pool');
}
if (!isset($this->slotsMap)) {
$this->buildSlotsMap();
}
@@ -446,12 +528,54 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
return $response;
}
/**
* Ensures that a command is executed one more time on connection failure.
*
* The connection to the node that generated the error is evicted from the
* pool before trying to fetch an updated slots map from another node. If
* the new slots map points to an unreachable server the client gives up and
* throws the exception as the nodes participating in the cluster may still
* have to agree that something changed in the configuration of the cluster.
*
* @param CommandInterface $command Command instance.
* @param string $method Actual method.
*
* @return mixed
*/
private function retryCommandOnFailure(CommandInterface $command, $method)
{
$failure = false;
RETRY_COMMAND: {
try {
$response = $this->getConnection($command)->$method($command);
} catch (ConnectionException $exception) {
$connection = $exception->getConnection();
$connection->disconnect();
$this->remove($connection);
if ($failure) {
throw $exception;
} elseif ($this->useClusterSlots) {
$this->askSlotsMap();
}
$failure = true;
goto RETRY_COMMAND;
}
}
return $response;
}
/**
* {@inheritdoc}
*/
public function writeRequest(CommandInterface $command)
{
$this->getConnection($command)->writeRequest($command);
$this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
@@ -459,7 +583,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function readResponse(CommandInterface $command)
{
return $this->getConnection($command)->readResponse($command);
return $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
@@ -467,8 +591,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function executeCommand(CommandInterface $command)
{
$connection = $this->getConnection($command);
$response = $connection->executeCommand($command);
$response = $this->retryCommandOnFailure($command, __FUNCTION__);
if ($response instanceof ErrorResponseInterface) {
return $this->onErrorResponse($command, $response);
@@ -517,7 +640,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
/**
* Enables automatic fetching of the current slots map from one of the nodes
* using the CLUSTER SLOTS command. This option is disabled by default but
* using the CLUSTER SLOTS command. This option is enabled by default as
* asking the current slots map to Redis upon -MOVED responses may reduce
* overhead by eliminating the trial-and-error nature of the node guessing
* procedure, mostly when targeting many keys that would end up in a lot of
+2 -3
View File
@@ -123,7 +123,7 @@ class StreamConnection extends AbstractConnection
protected function unixStreamInitializer(ParametersInterface $parameters)
{
if (!isset($parameters->path)) {
throw new InvalidArgumentException('Missing UNIX domain socket path.');
throw new \InvalidArgumentException('Missing UNIX domain socket path.');
}
$uri = "unix://{$parameters->path}";
@@ -281,8 +281,7 @@ class StreamConnection extends AbstractConnection
$buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";
for ($i = 0, $reqlen--; $i < $reqlen; ++$i) {
$argument = $arguments[$i];
foreach ($arguments as $argument) {
$arglen = strlen($argument);
$buffer .= "\${$arglen}\r\n{$argument}\r\n";
}
+2 -2
View File
@@ -99,7 +99,7 @@ class Consumer implements \Iterator
/**
* Returns the last message payload retrieved from the server.
*
* @return Object
* @return object
*/
public function current()
{
@@ -136,7 +136,7 @@ class Consumer implements \Iterator
* Waits for a new message from the server generated by MONITOR and returns
* it when available.
*
* @return Object
* @return object
*/
private function getValue()
{
+1
View File
@@ -27,6 +27,7 @@ final class Factory
'2.6' => 'Predis\Profile\RedisVersion260',
'2.8' => 'Predis\Profile\RedisVersion280',
'3.0' => 'Predis\Profile\RedisVersion300',
'3.2' => 'Predis\Profile\RedisVersion320',
'dev' => 'Predis\Profile\RedisUnstable',
'default' => 'Predis\Profile\RedisVersion300',
);
+2 -5
View File
@@ -16,7 +16,7 @@ namespace Predis\Profile;
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class RedisUnstable extends RedisVersion300
class RedisUnstable extends RedisVersion320
{
/**
* {@inheritdoc}
@@ -32,10 +32,7 @@ class RedisUnstable extends RedisVersion300
public function getSupportedCommands()
{
return array_merge(parent::getSupportedCommands(), array(
/* ---------------- Redis 3.2 ---------------- */
/* commands operating on hashes */
'HSTRLEN' => 'Predis\Command\HashStringLength',
// EMPTY
));
}
}
+281
View File
@@ -0,0 +1,281 @@
<?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\Profile;
/**
* Server profile for Redis 3.0.
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
class RedisVersion320 extends RedisProfile
{
/**
* {@inheritdoc}
*/
public function getVersion()
{
return '3.2';
}
/**
* {@inheritdoc}
*/
public function getSupportedCommands()
{
return array(
/* ---------------- Redis 1.2 ---------------- */
/* commands operating on the key space */
'EXISTS' => 'Predis\Command\KeyExists',
'DEL' => 'Predis\Command\KeyDelete',
'TYPE' => 'Predis\Command\KeyType',
'KEYS' => 'Predis\Command\KeyKeys',
'RANDOMKEY' => 'Predis\Command\KeyRandom',
'RENAME' => 'Predis\Command\KeyRename',
'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
'EXPIRE' => 'Predis\Command\KeyExpire',
'EXPIREAT' => 'Predis\Command\KeyExpireAt',
'TTL' => 'Predis\Command\KeyTimeToLive',
'MOVE' => 'Predis\Command\KeyMove',
'SORT' => 'Predis\Command\KeySort',
'DUMP' => 'Predis\Command\KeyDump',
'RESTORE' => 'Predis\Command\KeyRestore',
/* commands operating on string values */
'SET' => 'Predis\Command\StringSet',
'SETNX' => 'Predis\Command\StringSetPreserve',
'MSET' => 'Predis\Command\StringSetMultiple',
'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
'GET' => 'Predis\Command\StringGet',
'MGET' => 'Predis\Command\StringGetMultiple',
'GETSET' => 'Predis\Command\StringGetSet',
'INCR' => 'Predis\Command\StringIncrement',
'INCRBY' => 'Predis\Command\StringIncrementBy',
'DECR' => 'Predis\Command\StringDecrement',
'DECRBY' => 'Predis\Command\StringDecrementBy',
/* commands operating on lists */
'RPUSH' => 'Predis\Command\ListPushTail',
'LPUSH' => 'Predis\Command\ListPushHead',
'LLEN' => 'Predis\Command\ListLength',
'LRANGE' => 'Predis\Command\ListRange',
'LTRIM' => 'Predis\Command\ListTrim',
'LINDEX' => 'Predis\Command\ListIndex',
'LSET' => 'Predis\Command\ListSet',
'LREM' => 'Predis\Command\ListRemove',
'LPOP' => 'Predis\Command\ListPopFirst',
'RPOP' => 'Predis\Command\ListPopLast',
'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
/* commands operating on sets */
'SADD' => 'Predis\Command\SetAdd',
'SREM' => 'Predis\Command\SetRemove',
'SPOP' => 'Predis\Command\SetPop',
'SMOVE' => 'Predis\Command\SetMove',
'SCARD' => 'Predis\Command\SetCardinality',
'SISMEMBER' => 'Predis\Command\SetIsMember',
'SINTER' => 'Predis\Command\SetIntersection',
'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
'SUNION' => 'Predis\Command\SetUnion',
'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
'SDIFF' => 'Predis\Command\SetDifference',
'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
'SMEMBERS' => 'Predis\Command\SetMembers',
'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
/* commands operating on sorted sets */
'ZADD' => 'Predis\Command\ZSetAdd',
'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
'ZREM' => 'Predis\Command\ZSetRemove',
'ZRANGE' => 'Predis\Command\ZSetRange',
'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
'ZCARD' => 'Predis\Command\ZSetCardinality',
'ZSCORE' => 'Predis\Command\ZSetScore',
'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
/* connection related commands */
'PING' => 'Predis\Command\ConnectionPing',
'AUTH' => 'Predis\Command\ConnectionAuth',
'SELECT' => 'Predis\Command\ConnectionSelect',
'ECHO' => 'Predis\Command\ConnectionEcho',
'QUIT' => 'Predis\Command\ConnectionQuit',
/* remote server control commands */
'INFO' => 'Predis\Command\ServerInfoV26x',
'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
'MONITOR' => 'Predis\Command\ServerMonitor',
'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
'FLUSHALL' => 'Predis\Command\ServerFlushAll',
'SAVE' => 'Predis\Command\ServerSave',
'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
'LASTSAVE' => 'Predis\Command\ServerLastSave',
'SHUTDOWN' => 'Predis\Command\ServerShutdown',
'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
/* ---------------- Redis 2.0 ---------------- */
/* commands operating on string values */
'SETEX' => 'Predis\Command\StringSetExpire',
'APPEND' => 'Predis\Command\StringAppend',
'SUBSTR' => 'Predis\Command\StringSubstr',
/* commands operating on lists */
'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
'BRPOP' => 'Predis\Command\ListPopLastBlocking',
/* commands operating on sorted sets */
'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
'ZCOUNT' => 'Predis\Command\ZSetCount',
'ZRANK' => 'Predis\Command\ZSetRank',
'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
/* commands operating on hashes */
'HSET' => 'Predis\Command\HashSet',
'HSETNX' => 'Predis\Command\HashSetPreserve',
'HMSET' => 'Predis\Command\HashSetMultiple',
'HINCRBY' => 'Predis\Command\HashIncrementBy',
'HGET' => 'Predis\Command\HashGet',
'HMGET' => 'Predis\Command\HashGetMultiple',
'HDEL' => 'Predis\Command\HashDelete',
'HEXISTS' => 'Predis\Command\HashExists',
'HLEN' => 'Predis\Command\HashLength',
'HKEYS' => 'Predis\Command\HashKeys',
'HVALS' => 'Predis\Command\HashValues',
'HGETALL' => 'Predis\Command\HashGetAll',
/* transactions */
'MULTI' => 'Predis\Command\TransactionMulti',
'EXEC' => 'Predis\Command\TransactionExec',
'DISCARD' => 'Predis\Command\TransactionDiscard',
/* publish - subscribe */
'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
'PUBLISH' => 'Predis\Command\PubSubPublish',
/* remote server control commands */
'CONFIG' => 'Predis\Command\ServerConfig',
/* ---------------- Redis 2.2 ---------------- */
/* commands operating on the key space */
'PERSIST' => 'Predis\Command\KeyPersist',
/* commands operating on string values */
'STRLEN' => 'Predis\Command\StringStrlen',
'SETRANGE' => 'Predis\Command\StringSetRange',
'GETRANGE' => 'Predis\Command\StringGetRange',
'SETBIT' => 'Predis\Command\StringSetBit',
'GETBIT' => 'Predis\Command\StringGetBit',
/* commands operating on lists */
'RPUSHX' => 'Predis\Command\ListPushTailX',
'LPUSHX' => 'Predis\Command\ListPushHeadX',
'LINSERT' => 'Predis\Command\ListInsert',
'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
/* commands operating on sorted sets */
'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
/* transactions */
'WATCH' => 'Predis\Command\TransactionWatch',
'UNWATCH' => 'Predis\Command\TransactionUnwatch',
/* remote server control commands */
'OBJECT' => 'Predis\Command\ServerObject',
'SLOWLOG' => 'Predis\Command\ServerSlowlog',
/* ---------------- Redis 2.4 ---------------- */
/* remote server control commands */
'CLIENT' => 'Predis\Command\ServerClient',
/* ---------------- Redis 2.6 ---------------- */
/* commands operating on the key space */
'PTTL' => 'Predis\Command\KeyPreciseTimeToLive',
'PEXPIRE' => 'Predis\Command\KeyPreciseExpire',
'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt',
'MIGRATE' => 'Predis\Command\KeyMigrate',
/* commands operating on string values */
'PSETEX' => 'Predis\Command\StringPreciseSetExpire',
'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat',
'BITOP' => 'Predis\Command\StringBitOp',
'BITCOUNT' => 'Predis\Command\StringBitCount',
/* commands operating on hashes */
'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat',
/* scripting */
'EVAL' => 'Predis\Command\ServerEval',
'EVALSHA' => 'Predis\Command\ServerEvalSHA',
'SCRIPT' => 'Predis\Command\ServerScript',
/* remote server control commands */
'TIME' => 'Predis\Command\ServerTime',
'SENTINEL' => 'Predis\Command\ServerSentinel',
/* ---------------- Redis 2.8 ---------------- */
/* commands operating on the key space */
'SCAN' => 'Predis\Command\KeyScan',
/* commands operating on string values */
'BITPOS' => 'Predis\Command\StringBitPos',
/* commands operating on sets */
'SSCAN' => 'Predis\Command\SetScan',
/* commands operating on sorted sets */
'ZSCAN' => 'Predis\Command\ZSetScan',
'ZLEXCOUNT' => 'Predis\Command\ZSetLexCount',
'ZRANGEBYLEX' => 'Predis\Command\ZSetRangeByLex',
'ZREMRANGEBYLEX' => 'Predis\Command\ZSetRemoveRangeByLex',
'ZREVRANGEBYLEX' => 'Predis\Command\ZSetReverseRangeByLex',
/* commands operating on hashes */
'HSCAN' => 'Predis\Command\HashScan',
/* publish - subscribe */
'PUBSUB' => 'Predis\Command\PubSubPubsub',
/* commands operating on HyperLogLog */
'PFADD' => 'Predis\Command\HyperLogLogAdd',
'PFCOUNT' => 'Predis\Command\HyperLogLogCount',
'PFMERGE' => 'Predis\Command\HyperLogLogMerge',
/* remote server control commands */
'COMMAND' => 'Predis\Command\ServerCommand',
/* ---------------- Redis 3.2 ---------------- */
/* commands operating on hashes */
'HSTRLEN' => 'Predis\Command\HashStringLength',
'BITFIELD' => 'Predis\Command\StringBitField',
/* commands performing geospatial operations */
'GEOADD' => 'Predis\Command\GeospatialGeoAdd',
'GEOHASH' => 'Predis\Command\GeospatialGeoHash',
'GEOPOS' => 'Predis\Command\GeospatialGeoPos',
'GEODIST' => 'Predis\Command\GeospatialGeoDist',
'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius',
'GEORADIUSBYMEMBER' => 'Predis\Command\GeospatialGeoRadiusByMember',
);
}
}
+1 -2
View File
@@ -36,8 +36,7 @@ class RequestSerializer implements RequestSerializerInterface
$buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";
for ($i = 0, $reqlen--; $i < $reqlen; ++$i) {
$argument = $arguments[$i];
foreach ($arguments as $argument) {
$arglen = strlen($argument);
$buffer .= "\${$arglen}\r\n{$argument}\r\n";
}
+9 -9
View File
@@ -18,17 +18,17 @@ namespace Predis\PubSub;
*/
abstract class AbstractConsumer implements \Iterator
{
const SUBSCRIBE = 'subscribe';
const UNSUBSCRIBE = 'unsubscribe';
const PSUBSCRIBE = 'psubscribe';
const SUBSCRIBE = 'subscribe';
const UNSUBSCRIBE = 'unsubscribe';
const PSUBSCRIBE = 'psubscribe';
const PUNSUBSCRIBE = 'punsubscribe';
const MESSAGE = 'message';
const PMESSAGE = 'pmessage';
const PONG = 'pong';
const MESSAGE = 'message';
const PMESSAGE = 'pmessage';
const PONG = 'pong';
const STATUS_VALID = 1; // 0b0001
const STATUS_SUBSCRIBED = 2; // 0b0010
const STATUS_PSUBSCRIBED = 4; // 0b0100
const STATUS_VALID = 1; // 0b0001
const STATUS_SUBSCRIBED = 2; // 0b0010
const STATUS_PSUBSCRIBED = 4; // 0b0100
private $position = null;
private $statusFlags = self::STATUS_VALID;
+1 -1
View File
@@ -91,7 +91,7 @@ class DispatcherLoop
* Binds a callback to a channel.
*
* @param string $channel Channel name.
* @param Callable $callback A callback.
* @param callable $callback A callback.
*/
public function attachCallback($channel, $callback)
{
+68 -1
View File
@@ -100,8 +100,69 @@ class ReplicationStrategy
protected function isSortReadOnly(CommandInterface $command)
{
$arguments = $command->getArguments();
$argc = count($arguments);
return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE';
if ($argc > 1) {
for ($i = 1; $i < $argc; ++$i) {
$argument = strtoupper($arguments[$i]);
if ($argument === 'STORE') {
return false;
}
}
}
return true;
}
/**
* Checks if BITFIELD performs a read-only operation by looking for certain
* SET and INCRYBY modifiers in the arguments array of the command.
*
* @param CommandInterface $command Command instance.
*
* @return bool
*/
protected function isBitfieldReadOnly(CommandInterface $command)
{
$arguments = $command->getArguments();
$argc = count($arguments);
if ($argc >= 2) {
for ($i = 1; $i < $argc; ++$i) {
$argument = strtoupper($arguments[$i]);
if ($argument === 'SET' || $argument === 'INCRBY') {
return false;
}
}
}
return true;
}
/**
* Checks if a GEORADIUS command is a readable operation by parsing the
* arguments array of the specified commad instance.
*
* @param CommandInterface $command Command instance.
*
* @return bool
*/
protected function isGeoradiusReadOnly(CommandInterface $command)
{
$arguments = $command->getArguments();
$argc = count($arguments);
$startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
if ($argc > $startIndex) {
for ($i = $startIndex; $i < $argc; ++$i) {
$argument = strtoupper($arguments[$i]);
if ($argument === 'STORE' || $argument === 'STOREDIST') {
return false;
}
}
}
return true;
}
/**
@@ -232,6 +293,12 @@ class ReplicationStrategy
'TIME' => true,
'PFCOUNT' => true,
'SORT' => array($this, 'isSortReadOnly'),
'BITFIELD' => array($this, 'isBitfieldReadOnly'),
'GEOHASH' => true,
'GEOPOS' => true,
'GEODIST' => true,
'GEORADIUS' => array($this, 'isGeoradiusReadOnly'),
'GEORADIUSBYMEMBER' => array($this, 'isGeoradiusReadOnly'),
);
}
}
+3 -3
View File
@@ -20,9 +20,9 @@ class MultiExecState
{
const INITIALIZED = 1; // 0b00001
const INSIDEBLOCK = 2; // 0b00010
const DISCARDED = 4; // 0b00100
const CAS = 8; // 0b01000
const WATCH = 16; // 0b10000
const DISCARDED = 4; // 0b00100
const CAS = 8; // 0b01000
const WATCH = 16; // 0b10000
private $flags;
+31 -14
View File
@@ -96,18 +96,6 @@ abstract class PredisConnectionTestCase extends PredisTestCase
$this->assertEquals($parameters, $unserialized->getParameters());
}
/**
* @group disconnected
* @group slow
* @expectedException \Predis\Connection\ConnectionException
*/
public function testThrowExceptionWhenUnableToConnect()
{
$parameters = array('host' => '169.254.10.10', 'timeout' => 0.5);
$connection = $this->createConnectionWithParams($parameters, false);
$connection->executeCommand($this->getCurrentProfile()->createCommand('ping'));
}
// ******************************************************************** //
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //
@@ -221,6 +209,19 @@ abstract class PredisConnectionTestCase extends PredisTestCase
$this->assertEquals('PONG', $connection->executeCommand($cmdPing));
}
/**
* @group connected
*/
public function testExecutesCommandWithHolesInArguments()
{
$profile = $this->getCurrentProfile();
$cmdDel = $profile->createCommand('mget', array(0 => 'key:0', 2 => 'key:2'));
$connection = $this->createConnection();
$this->assertSame(array(null, null), $connection->executeCommand($cmdDel));
}
/**
* @group connected
*/
@@ -415,9 +416,17 @@ abstract class PredisConnectionTestCase extends PredisTestCase
*/
public function testThrowsExceptionOnConnectionTimeout()
{
// TODO: float timeouts for connect() under HHVM 3.6.6 are broken and,
// unfortunately, this is the version still being used by Travis CI.
if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.6.6', '<=')) {
$timeout = 1;
} else {
$timeout = 0.1;
}
$connection = $this->createConnectionWithParams(array(
'host' => '169.254.10.10',
'timeout' => 0.1,
'timeout' => $timeout,
), false);
$connection->connect();
@@ -431,9 +440,17 @@ abstract class PredisConnectionTestCase extends PredisTestCase
*/
public function testThrowsExceptionOnConnectionTimeoutIPv6()
{
// TODO: float timeouts for connect() under HHVM 3.6.6 are broken and,
// unfortunately, this is the version still being used by Travis CI.
if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.6.6', '<=')) {
$timeout = 1;
} else {
$timeout = 0.1;
}
$connection = $this->createConnectionWithParams(array(
'host' => '0:0:0:0:0:ffff:a9fe:a0a',
'timeout' => 0.1,
'timeout' => $timeout,
), false);
$connection->connect();
+3 -1
View File
@@ -10,6 +10,7 @@
*/
use Predis\Command\CommandInterface;
use SebastianBergmann\Exporter\Exporter;
/**
* Constraint that verifies a redis command.
@@ -72,6 +73,7 @@ class RedisCommandConstraint extends \PHPUnit_Framework_Constraint
*/
public function toString()
{
$exporter = new Exporter();
$string = 'is a Redis command';
if ($this->commandID) {
@@ -80,7 +82,7 @@ class RedisCommandConstraint extends \PHPUnit_Framework_Constraint
if ($this->arguments) {
$string .= " and the following arguments:\n\n";
$string .= \PHPUnit_Util_Type::export($this->arguments);
$string .= $exporter->export($this->arguments);
}
return $string;
+63 -2
View File
@@ -107,6 +107,24 @@ class PredisStrategyTest extends PredisTestCase
}
}
/**
* @group disconnected
*/
public function testKeysForSortCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('{key}:1', 'value1', '{key}:2', 'value2');
$commandID = 'SORT';
$command = $profile->createCommand($commandID, array('{key}:1'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
$command = $profile->createCommand($commandID, array('{key}:1', array('STORE' => '{key}:2')));
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
/**
* @group disconnected
*/
@@ -152,6 +170,40 @@ class PredisStrategyTest extends PredisTestCase
}
}
/**
* @group disconnected
*/
public function testKeysForGeoradiusCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$commandID = 'GEORADIUS';
$command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
$command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
/**
* @group disconnected
*/
public function testKeysForGeoradiusByMemberCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$commandID = 'GEORADIUSBYMEMBER';
$command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
$command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
/**
* @group disconnected
*/
@@ -255,7 +307,7 @@ class PredisStrategyTest extends PredisTestCase
{
$commands = array(
/* commands operating on the key space */
'EXISTS' => 'keys-first',
'EXISTS' => 'keys-all',
'DEL' => 'keys-all',
'TYPE' => 'keys-first',
'EXPIRE' => 'keys-first',
@@ -265,7 +317,7 @@ class PredisStrategyTest extends PredisTestCase
'PEXPIREAT' => 'keys-first',
'TTL' => 'keys-first',
'PTTL' => 'keys-first',
'SORT' => 'keys-first', // TODO
'SORT' => 'variable',
'DUMP' => 'keys-first',
'RESTORE' => 'keys-first',
@@ -292,6 +344,7 @@ class PredisStrategyTest extends PredisTestCase
'SUBSTR' => 'keys-first',
'BITOP' => 'keys-bitop',
'BITCOUNT' => 'keys-first',
'BITFIELD' => 'keys-first',
/* commands operating on lists */
'LINSERT' => 'keys-first',
@@ -376,6 +429,14 @@ class PredisStrategyTest extends PredisTestCase
/* scripting */
'EVAL' => 'keys-script',
'EVALSHA' => 'keys-script',
/* commands performing geospatial operations */
'GEOADD' => 'keys-first',
'GEOHASH' => 'keys-first',
'GEOPOS' => 'keys-first',
'GEODIST' => 'keys-first',
'GEORADIUS' => 'keys-georadius',
'GEORADIUSBYMEMBER' => 'keys-georadius',
);
if (isset($type)) {
+62 -1
View File
@@ -135,6 +135,24 @@ class RedisStrategyTest extends PredisTestCase
}
}
/**
* @group disconnected
*/
public function testKeysForSortCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$arguments = array('{key}:1', 'value1', '{key}:2', 'value2');
$commandID = 'SORT';
$command = $profile->createCommand($commandID, array('{key}:1'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
$command = $profile->createCommand($commandID, array('{key}:1', array('STORE' => '{key}:2')));
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
/**
* @group disconnected
*/
@@ -165,6 +183,40 @@ class RedisStrategyTest extends PredisTestCase
}
}
/**
* @group disconnected
*/
public function testKeysForGeoradiusCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$commandID = 'GEORADIUS';
$command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
$command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
/**
* @group disconnected
*/
public function testKeysForGeoradiusByMemberCommand()
{
$strategy = $this->getClusterStrategy();
$profile = Profile\Factory::getDevelopment();
$commandID = 'GEORADIUSBYMEMBER';
$command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
$command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
$this->assertNotNull($strategy->getSlot($command), $commandID);
}
/**
* @group disconnected
*/
@@ -265,7 +317,7 @@ class RedisStrategyTest extends PredisTestCase
{
$commands = array(
/* commands operating on the key space */
'EXISTS' => 'keys-first',
'EXISTS' => 'keys-all',
'DEL' => 'keys-all',
'TYPE' => 'keys-first',
'EXPIRE' => 'keys-first',
@@ -302,6 +354,7 @@ class RedisStrategyTest extends PredisTestCase
'SUBSTR' => 'keys-first',
'BITOP' => 'keys-bitop',
'BITCOUNT' => 'keys-first',
'BITFIELD' => 'keys-first',
/* commands operating on lists */
'LINSERT' => 'keys-first',
@@ -386,6 +439,14 @@ class RedisStrategyTest extends PredisTestCase
/* scripting */
'EVAL' => 'keys-script',
'EVALSHA' => 'keys-script',
/* commands performing geospatial operations */
'GEOADD' => 'keys-first',
'GEOHASH' => 'keys-first',
'GEOPOS' => 'keys-first',
'GEODIST' => 'keys-first',
'GEORADIUS' => 'keys-georadius',
'GEORADIUSBYMEMBER' => 'keys-georadius',
);
if (isset($type)) {
@@ -56,6 +56,51 @@ class HashKeyTest extends PredisTestCase
$this->assertFalse($iterator->valid());
}
/**
* @link https://github.com/nrk/predis/pull/330
* @link https://github.com/nrk/predis/issues/331
* @group disconnected
*/
public function testIterationWithIntegerFields()
{
$client = $this->getMock('Predis\Client', array('getProfile', 'hscan'));
$client->expects($this->any())
->method('getProfile')
->will($this->returnValue(Profile\Factory::get('2.8')));
$client->expects($this->once())
->method('hscan')
->with('key:hash', 0, array())
->will($this->returnValue(array(0, array(
1 => 'a', 2 => 'b', 3 => 100, 'foo' => 'bar',
))));
$iterator = new HashKey($client, 'key:hash');
$iterator->rewind();
$this->assertTrue($iterator->valid());
$this->assertSame('a', $iterator->current());
$this->assertSame(1, $iterator->key());
$iterator->next();
$this->assertTrue($iterator->valid());
$this->assertSame('b', $iterator->current());
$this->assertSame(2, $iterator->key());
$iterator->next();
$this->assertTrue($iterator->valid());
$this->assertSame(100, $iterator->current());
$this->assertSame(3, $iterator->key());
$iterator->next();
$this->assertTrue($iterator->valid());
$this->assertSame('bar', $iterator->current());
$this->assertSame('foo', $iterator->key());
$iterator->next();
$this->assertFalse($iterator->valid());
}
/**
* @group disconnected
*/
@@ -0,0 +1,106 @@
<?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\Command;
/**
* @group commands
* @group realm-geospatial
*/
class GeospatialGeoAddTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\GeospatialGeoAdd';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'GEOADD';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$expected = array('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithMembersAsSingleArray()
{
$arguments = array('Sicily', array(
array('13.361389', '38.115556', 'Palermo'),
array('15.087269', '37.502669', 'Catania'),
));
$expected = array('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse()
{
$raw = 1;
$expected = 1;
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandFillsSortedSet()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo');
$this->assertSame(array('Palermo' => '3479099956230698'), $redis->zrange('Sicily', 0, -1, 'WITHSCORES'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$redis = $this->getClient();
$redis->lpush('Sicily', 'Palermo');
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo');
}
}
@@ -0,0 +1,88 @@
<?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\Command;
/**
* @group commands
* @group realm-geospatial
*/
class GeospatialGeoDistTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\GeospatialGeoDist';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'GEODIST';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array('key', 'member:1', 'member:2', 'km');
$expected = array('key', 'member:1', 'member:2', 'km');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse()
{
$raw = array('103.31822459492736');
$expected = array('103.31822459492736');
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoDistance()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$this->assertSame('166.2742', $redis->geodist('Sicily', 'Palermo', 'Catania', 'km'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$redis = $this->getClient();
$redis->lpush('Sicily', 'Palermo');
$redis->geodist('Sicily', 'Palermo', 'Catania');
}
}
@@ -0,0 +1,102 @@
<?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\Command;
/**
* @group commands
* @group realm-geospatial
*/
class GeospatialGeoHashTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\GeospatialGeoHash';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'GEOHASH';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array('key', 'member:1', 'member:2');
$expected = array('key', 'member:1', 'member:2');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithMembersAsSingleArray()
{
$arguments = array('key', array('member:1', 'member:2'));
$expected = array('key', 'member:1', 'member:2');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse()
{
$raw = array('sqc8b49rny0', 'sqdtr74hyu0');
$expected = array('sqc8b49rny0', 'sqdtr74hyu0');
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoHashes()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$this->assertSame(array('sqc8b49rny0', 'sqdtr74hyu0'), $redis->geohash('Sicily', 'Palermo', 'Catania'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$redis = $this->getClient();
$redis->lpush('Sicily', 'Palermo');
$redis->geohash('Sicily', 'Palermo');
}
}
@@ -0,0 +1,112 @@
<?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\Command;
/**
* @group commands
* @group realm-geospatial
*/
class GeospatialGeoPosTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\GeospatialGeoPos';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'GEOPOS';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array('key', 'member:1', 'member:2');
$expected = array('key', 'member:1', 'member:2');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithMembersAsSingleArray()
{
$arguments = array('key', array('member:1', 'member:2'));
$expected = array('key', 'member:1', 'member:2');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse()
{
$raw = array(
array('13.361389338970184', '38.115556395496299'),
array('15.087267458438873', '37.50266842333162'),
);
$expected = array(
array('13.361389338970184', '38.115556395496299'),
array('15.087267458438873', '37.50266842333162'),
);
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoPositions()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$this->assertEquals(array(
array('13.361389338970184', '38.115556395496299'),
array('15.087267458438873', '37.50266842333162'),
), $redis->geopos('Sicily', 'Palermo', 'Catania'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$redis = $this->getClient();
$redis->lpush('Sicily', 'Palermo');
$redis->geopos('Sicily', 'Palermo');
}
}
@@ -0,0 +1,168 @@
<?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\Command;
/**
* @group commands
* @group realm-geospatial
*/
class GeospatialGeoRadiusByMemberTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\GeospatialGeoRadiusByMember';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'GEORADIUSBYMEMBER';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array(
'Sicily', 'Agrigento', 100, 'km',
'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist',
);
$expected = array(
'Sicily', 'Agrigento', 100, 'km',
'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist',
);
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithComplexOptions()
{
$arguments = array(
'Sicily', 'Agrigento', 100, 'km', array(
'store' => 'key:store',
'storedist' => 'key:storedist',
'withdist' => true,
'withcoord' => true,
'withhash' => true,
'count' => 1,
'sort' => 'asc',
),
);
$expected = array(
'Sicily', 'Agrigento', 100, 'km',
'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist',
);
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithSpecificOptionsSetToFalse()
{
$arguments = array(
'Sicily', 'Agrigento', 100, 'km', array(
'store' => 'key:store',
'storedist' => 'key:storedist',
'withdist' => false,
'withcoord' => false,
'withhash' => false,
'count' => 1,
'sort' => 'asc',
),
);
$expected = array('Sicily', 'Agrigento', 100, 'km', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponseWithNoOptions()
{
$raw = array(
array('Agrigento', 'Palermo'),
);
$expected = array(
array('Agrigento', 'Palermo'),
);
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoRadiusInfoWithNoOptions()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania', '13.583333', '37.316667', 'Agrigento');
$this->assertEquals(array('Agrigento', 'Palermo'), $redis->georadiusbymember('Sicily', 'Agrigento', 100, 'km'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoRadiusInfoWithOptions()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania', '13.583333', '37.316667', 'Agrigento');
$this->assertEquals(array(
array('Agrigento', '0.0000', array('13.5833314061164856', '37.31666804993816555')),
array('Palermo', '90.9778', array('13.361389338970184', '38.115556395496299')),
), $redis->georadiusbymember('Sicily', 'Agrigento', 100, 'km', 'WITHDIST', 'WITHCOORD'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$redis = $this->getClient();
$redis->lpush('Sicily', 'Palermo');
$redis->georadiusbymember('Sicily', 'Agrigento', 200, 'km');
}
}
@@ -0,0 +1,170 @@
<?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\Command;
/**
* @group commands
* @group realm-geospatial
*/
class GeospatialGeoRadiusTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\GeospatialGeoRadius';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'GEORADIUS';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array(
'Sicily', 15, 37, 200, 'km',
'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist',
);
$expected = array(
'Sicily', 15, 37, 200, 'km',
'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist',
);
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithComplexOptions()
{
$arguments = array(
'Sicily', 15, 37, 200, 'km', array(
'store' => 'key:store',
'storedist' => 'key:storedist',
'withdist' => true,
'withcoord' => true,
'withhash' => true,
'count' => 1,
'sort' => 'asc',
),
);
$expected = array(
'Sicily', 15, 37, 200, 'km',
'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist',
);
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterArgumentsWithSpecificOptionsSetToFalse()
{
$arguments = array(
'Sicily', 15, 37, 200, 'km', array(
'store' => 'key:store',
'storedist' => 'key:storedist',
'withdist' => false,
'withcoord' => false,
'withhash' => false,
'count' => 1,
'sort' => 'asc',
),
);
$expected = array('Sicily', 15, 37, 200, 'km', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponseWithNoOptions()
{
$raw = array(
array('Palermo', '190.4424'),
array('Catania', '56.4413'),
);
$expected = array(
array('Palermo', '190.4424'),
array('Catania', '56.4413'),
);
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoRadiusInfoWithNoOptions()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$this->assertEquals(array('Palermo', 'Catania'), $redis->georadius('Sicily', 15, 37, 200, 'km'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testCommandReturnsGeoRadiusInfoWithOptions()
{
$redis = $this->getClient();
$redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
$this->assertEquals(array(
array('Palermo', '190.4424', array('13.361389338970184', '38.115556395496299')),
array('Catania', '56.4413', array('15.087267458438873', '37.50266842333162')),
), $redis->georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$redis = $this->getClient();
$redis->lpush('Sicily', 'Palermo');
$redis->georadius('Sicily', 15, 37, 200, 'km');
}
}
@@ -88,7 +88,7 @@ class HashIncrementByFloatTest extends PredisCommandTestCase
/**
* @group connected
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage ERR hash value is not a valid float
* @expectedExceptionMessageRegExp /ERR hash value is not a( valid)? float/
*/
public function testThrowsExceptionOnStringField()
{
@@ -61,6 +61,7 @@ class HashStringLengthTest extends PredisCommandTestCase
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testReturnsStringLengthOfSpecifiedField()
{
@@ -81,6 +82,7 @@ class HashStringLengthTest extends PredisCommandTestCase
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
@@ -864,6 +864,51 @@ class KeyPrefixProcessorTest extends PredisTestCase
array('127.0.0.1', '6379', 'key', '0', '10', 'COPY', 'REPLACE'),
array('127.0.0.1', '6379', 'prefix:key', '0', '10', 'COPY', 'REPLACE'),
),
array('EXISTS',
array('key1', 'key2', 'key3'),
array('prefix:key1', 'prefix:key2', 'prefix:key3'),
),
/* ---------------- Redis 3.2 ---------------- */
array('HSTRLEN',
array('key', 'field'),
array('prefix:key', 'field'),
),
array('BITFIELD',
array('key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'),
array('prefix:key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'),
),
array('GEOADD',
array('key', '13.361389', '38.115556', 'member:1', '15.087269', '37.502669', 'member:2'),
array('prefix:key', '13.361389', '38.115556', 'member:1', '15.087269', '37.502669', 'member:2'),
),
array('GEOHASH',
array('key', 'member:1', 'member:2'),
array('prefix:key', 'member:1', 'member:2'),
),
array('GEOPOS',
array('key', 'member:1', 'member:2'),
array('prefix:key', 'member:1', 'member:2'),
),
array('GEODIST',
array('key', 'member:1', 'member:2', 'km'),
array('prefix:key', 'member:1', 'member:2', 'km'),
),
array('GEORADIUS',
array('key', '15', '37', '200', 'km'),
array('prefix:key', '15', '37', '200', 'km'),
),
array('GEORADIUS',
array('key', '15', '37', '200', 'km', 'WITHDIST', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'),
array('prefix:key', '15', '37', '200', 'km', 'WITHDIST', 'STORE', 'prefix:key:store', 'STOREDIST', 'prefix:key:storedist'),
),
array('GEORADIUSBYMEMBER',
array('key', 'member', '100', 'km'),
array('prefix:key', 'member', '100', 'km'),
),
array('GEORADIUSBYMEMBER',
array('key', 'member', '100', 'km', 'WITHDIST', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'),
array('prefix:key', 'member', '100', 'km', 'WITHDIST', 'STORE', 'prefix:key:store', 'STOREDIST', 'prefix:key:storedist'),
),
);
}
}
@@ -145,7 +145,7 @@ class PubSubSubscribeByPatternTest extends PredisCommandTestCase
/**
* @group connected
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context
* @expectedExceptionMessageRegExp /ERR only .* allowed in this context/
*/
public function testCannotSendOtherCommandsAfterPsubscribe()
{
+1 -1
View File
@@ -145,7 +145,7 @@ class PubSubSubscribeTest extends PredisCommandTestCase
/**
* @group connected
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context
* @expectedExceptionMessageRegExp /ERR only .* allowed in this context/
*/
public function testCannotSendOtherCommandsAfterSubscribe()
{
+3 -3
View File
@@ -114,9 +114,9 @@ addr=127.0.0.1:45395 fd=8 idle=0 flags=N db=0 sub=0 psub=0
BUFFER;
$parsed = array(
array('addr' => '127.0.0.1:45393','fd' => '6','idle' => '0','flags' => 'N','db' => '0','sub' => '0','psub' => '0'),
array('addr' => '127.0.0.1:45394','fd' => '7','idle' => '0','flags' => 'N','db' => '0','sub' => '0','psub' => '0'),
array('addr' => '127.0.0.1:45395','fd' => '8','idle' => '0','flags' => 'N','db' => '0','sub' => '0','psub' => '0'),
array('addr' => '127.0.0.1:45393', 'fd' => '6', 'idle' => '0', 'flags' => 'N', 'db' => '0', 'sub' => '0', 'psub' => '0'),
array('addr' => '127.0.0.1:45394', 'fd' => '7', 'idle' => '0', 'flags' => 'N', 'db' => '0', 'sub' => '0', 'psub' => '0'),
array('addr' => '127.0.0.1:45395', 'fd' => '8', 'idle' => '0', 'flags' => 'N', 'db' => '0', 'sub' => '0', 'psub' => '0'),
);
$this->assertSame($parsed, $command->parseResponse($raw));
+2 -2
View File
@@ -107,8 +107,8 @@ class ServerCommandTest extends PredisCommandTestCase
// Predis\Response\Status instead of plain strings. This class responds
// to __toString() so the string conversion is implicit, but assertSame
// checks for strict equality while assertEquals is loose.
$expected = array(array('command', 0, array('readonly', 'loading', 'stale'), 0, 0, 0));
$this->assertCount(1, $response = $redis->command('INFO', 'COMMAND'));
$expected = array(array('get', 2, array('readonly', 'fast'), 1, 1, 1));
$this->assertCount(1, $response = $redis->command('INFO', 'GET'));
$this->assertEquals($expected, $response);
}
+1 -1
View File
@@ -52,7 +52,7 @@ class ServerConfigTest extends PredisCommandTestCase
*/
public function testParseResponseOfConfigGet()
{
$raw = array('slowlog-log-slower-than','10000','slowlog-max-len','64','loglevel','verbose');
$raw = array('slowlog-log-slower-than', '10000', 'slowlog-max-len', '64', 'loglevel', 'verbose');
$expected = array(
'slowlog-log-slower-than' => '10000',
'slowlog-max-len' => '64',
+1 -1
View File
@@ -267,7 +267,7 @@ BUFFER;
'used_cpu_user_children' => '0.00',
'cluster_enabled' => '0',
'db0' => array('keys' => '2', 'expires' => '0'),
'db5' => array('keys' => '1','expires' => '0'),
'db5' => array('keys' => '1', 'expires' => '0'),
);
$this->assertSame($expected, $this->getCommand()->parseResponse($raw));
+18 -2
View File
@@ -38,8 +38,8 @@ class SetPopTest extends PredisCommandTestCase
*/
public function testFilterArguments()
{
$arguments = array('key');
$expected = array('key');
$arguments = array('key', 2);
$expected = array('key', 2);
$command = $this->getCommand();
$command->setArguments($arguments);
@@ -70,6 +70,22 @@ class SetPopTest extends PredisCommandTestCase
$this->assertNull($redis->spop('letters'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testPopsMoreRandomMembersFromSet()
{
$redis = $this->getClient();
$redis->sadd('letters', 'a', 'b', 'c');
$this->assertSameValues(array('a', 'b', 'c'), $redis->spop('letters', 3));
$this->assertEmpty($redis->spop('letters', 3));
$this->assertNull($redis->spop('letters'));
}
/**
* @group connected
* @expectedException \Predis\Response\ServerException
+155
View File
@@ -0,0 +1,155 @@
<?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\Command;
/**
* @group commands
* @group realm-string
*/
class StringBitFieldTest extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand()
{
return 'Predis\Command\StringBitField';
}
/**
* {@inheritdoc}
*/
protected function getExpectedId()
{
return 'BITFIELD';
}
/**
* @group disconnected
*/
public function testFilterArguments()
{
$arguments = array('key');
$expected = array('key');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testFilterMultipleArguments()
{
$arguments = array('key', 'incrby', 'u2', '100', '1', 'OVERFLOW', 'SAT', 'incrby', 'u2', '102', '1', 'GET', 'u2', '100');
$expected = array('key', 'incrby', 'u2', '100', '1', 'OVERFLOW', 'SAT', 'incrby', 'u2', '102', '1', 'GET', 'u2', '100');
$command = $this->getCommand();
$command->setArguments($arguments);
$this->assertSame($expected, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse()
{
$raw = array(1);
$expected = array(1);
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group disconnected
*/
public function testParseResponseComplex()
{
$raw = array(1, 0, 3);
$expected = array(1, 0, 3);
$command = $this->getCommand();
$this->assertSame($expected, $command->parseResponse($raw));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testBitfieldWithGetModifier()
{
$redis = $this->getClient();
$redis->setbit('string', 0, 1);
$redis->setbit('string', 8, 1);
$this->assertSame(array(128), $redis->bitfield('string', 'GET', 'u8', 0));
$this->assertSame(array(128, 1, 128), $redis->bitfield('string', 'GET', 'u8', 0, 'GET', 'u8', 1, 'GET', 'u8', 8));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testBitfieldWithSetModifier()
{
$redis = $this->getClient();
$redis->setbit('string', 0, 1);
$redis->setbit('string', 8, 1);
$this->assertSame(array(128), $redis->bitfield('string', 'SET', 'u8', 0, 1));
$this->assertSame(array(1, 128), $redis->bitfield('string', 'SET', 'u8', 0, 128, 'SET', 'u8', 8, 1));
$this->assertSame(array(1, 128), $redis->bitfield('string', 'SET', 'u8', 8, 128, 'GET', 'u8', 8));
$this->assertSame("\x80\x80", $redis->get('string'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
*/
public function testBitfieldWithIncrbyModifier()
{
$redis = $this->getClient();
$redis->setbit('string', 0, 1);
$redis->setbit('string', 8, 1);
$this->assertSame(array(138), $redis->bitfield('string', 'INCRBY', 'u8', 0, 10));
$this->assertSame(array(143, 128), $redis->bitfield('string', 'INCRBY', 'u8', 0, 5, 'INCRBY', 'u8', 0, -15));
$this->assertSame("\x80\x80", $redis->get('string'));
}
/**
* @group connected
* @requiresRedisVersion >= 3.2.0
* @expectedException \Predis\Response\ServerException
* @expectedExceptionMessage Operation against a key holding the wrong kind of value
*/
public function testThrowsExceptionOnWrongType()
{
$this->markTestSkipped('Currently skipped due issues in Redis (see antirez/redis#3259).');
$redis = $this->getClient();
$redis->lpush('metavars', 'foo');
$redis->bitfield('metavars', 'SET', 'u4', '0', '1');
}
}
@@ -333,7 +333,7 @@ class RedisClusterTest extends PredisTestCase
/**
* @group disconnected
*/
public function testCanAssignConnectionsToCustomSlotsFromParameters()
public function testCanAssignConnectionsToRangeOfSlotsFromParameters()
{
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6379?slots=0-5460');
$connection2 = $this->getMockConnection('tcp://127.0.0.1:6380?slots=5461-10921');
@@ -344,15 +344,50 @@ class RedisClusterTest extends PredisTestCase
$cluster->add($connection2);
$cluster->add($connection3);
$cluster->buildSlotsMap();
$expectedMap = array_merge(
array_fill(0, 5461, '127.0.0.1:6379'),
array_fill(5460, 5461, '127.0.0.1:6380'),
array_fill(10921, 5462, '127.0.0.1:6381')
);
$actualMap = $cluster->getSlotsMap();
ksort($actualMap);
$this->assertSame($expectedMap, $actualMap);
}
/**
* @group disconnected
*/
public function testCanAssignConnectionsToSingleSlotOrRangesOfSlotsFromParameters()
{
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6379?slots=0-5460,5500-5600,11000');
$connection2 = $this->getMockConnection('tcp://127.0.0.1:6380?slots=5461-5499,5600-10921');
$connection3 = $this->getMockConnection('tcp://127.0.0.1:6381?slots=10922-10999,11001-16383');
$cluster = new RedisCluster(new Connection\Factory());
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$cluster->buildSlotsMap();
$this->assertSame($expectedMap, $cluster->getSlotsMap());
$expectedMap = array_merge(
array_fill(0, 5461, '127.0.0.1:6379'),
array_fill(5460, 39, '127.0.0.1:6380'),
array_fill(5499, 101, '127.0.0.1:6379'),
array_fill(5599, 5321, '127.0.0.1:6380'),
array_fill(10921, 78, '127.0.0.1:6381'),
array_fill(11000, 1, '127.0.0.1:6379'),
array_fill(11000, 5383, '127.0.0.1:6381')
);
$actualMap = $cluster->getSlotsMap();
ksort($actualMap);
$this->assertSame($expectedMap, $actualMap);
}
/**
@@ -451,6 +486,281 @@ class RedisClusterTest extends PredisTestCase
$cluster->readResponse($command);
}
/**
* @group disconnected
*/
public function testRetriesExecutingCommandOnConnectionFailureOnlyAfterFetchingNewSlotsMap()
{
$slotsmap = array(
array(0, 5460, array('127.0.0.1', 9381), array()),
array(5461, 10921, array('127.0.0.1', 6382), array()),
array(10922, 16383, array('127.0.0.1', 6383), array()),
);
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6381?slots=0-5460');
$connection1->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'GET', array('node:1001')
))
->will($this->throwException(
new Connection\ConnectionException($connection1, 'Unknown connection error [127.0.0.1:6381]')
));
$connection2 = $this->getMockConnection('tcp://127.0.0.1:6382?slots=5461-10921');
$connection2->expects($this->any())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->returnValue($slotsmap));
$connection3 = $this->getMockConnection('tcp://127.0.0.1:6383?slots=10922-16383');
$connection3->expects($this->any())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->returnValue($slotsmap));
$connection4 = $this->getMockConnection('tcp://127.0.0.1:9381');
$connection4->expects($this->at(0))
->method('executeCommand')
->with($this->isRedisCommand(
'GET', array('node:1001')
))
->will($this->returnValue('value:1001'));
$connection4->expects($this->at(1))
->method('executeCommand')
->with($this->isRedisCommand(
'GET', array('node:5001')
))
->will($this->returnValue('value:5001'));
$factory = $this->getMock('Predis\Connection\FactoryInterface');
$factory->expects($this->once())
->method('create')
->with(array(
'host' => '127.0.0.1',
'port' => '9381',
))
->will($this->returnValue($connection4));
$cluster = new RedisCluster($factory);
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$this->assertSame('value:1001', $cluster->executeCommand(
Command\RawCommand::create('get', 'node:1001')
));
$this->assertSame('value:5001', $cluster->executeCommand(
Command\RawCommand::create('get', 'node:5001')
));
}
/**
* @group disconnected
*/
public function testRetriesExecutingCommandOnConnectionFailureButDoNotAskSlotsMapWhenDisabled()
{
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6381?slots=0-5500');
$connection1->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'GET', array('node:1001')
))
->will($this->throwException(
new Connection\ConnectionException($connection1, 'Unknown connection error [127.0.0.1:6381]')
));
$connection2 = $this->getMockConnection('tcp://127.0.0.1:6382?slots=5501-11000');
$connection2->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'GET', array('node:1001')
))
->will($this->returnValue(
new Response\Error('MOVED 1970 127.0.0.1:9381')
));
$connection3 = $this->getMockConnection('tcp://127.0.0.1:6383?slots=11101-16383');
$connection3->expects($this->never())
->method('executeCommand');
$connection4 = $this->getMockConnection('tcp://127.0.0.1:9381');
$connection4->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'GET', array('node:1001')
))
->will($this->returnValue('value:1001'));
$factory = $this->getMock('Predis\Connection\FactoryInterface');
$factory->expects($this->once())
->method('create')
->with(array(
'host' => '127.0.0.1',
'port' => '9381',
))
->will($this->returnValue($connection4));
// TODO: I'm not sure about mocking a protected method, but it'll do for now
$cluster = $this->getMock('Predis\Connection\Aggregate\RedisCluster', array('getRandomConnection'), array($factory));
$cluster->expects($this->never())
->method('getRandomConnection');
$cluster->useClusterSlots(false);
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$this->assertSame('value:1001', $cluster->executeCommand(
Command\RawCommand::create('get', 'node:1001')
));
}
/**
* @group disconnected
* @expectedException \Predis\ClientException
* @expectedExceptionMessage No connections available in the pool
*/
public function testThrowsClientExceptionWhenExecutingCommandWithEmptyPool()
{
$factory = $this->getMock('Predis\Connection\FactoryInterface');
$factory->expects($this->never())->method('create');
$cluster = new RedisCluster($factory);
$cluster->executeCommand(Command\RawCommand::create('get', 'node:1001'));
}
/**
* @group disconnected
*/
public function testAskSlotsMapReturnEmptyArrayOnEmptyConnectionsPool()
{
$factory = $this->getMock('Predis\Connection\FactoryInterface');
$factory->expects($this->never())->method('create');
$cluster = new RedisCluster($factory);
$this->assertEmpty($cluster->askSlotsMap());
}
/**
* @group disconnected
*/
public function testAskSlotsMapRetriesOnDifferentNodeOnConnectionFailure()
{
$slotsmap = array(
array(0, 5460, array('127.0.0.1', 9381), array()),
array(5461, 10921, array('127.0.0.1', 6382), array()),
array(10922, 16383, array('127.0.0.1', 6383), array()),
);
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6381?slots=0-5460');
$connection1->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->throwException(
new Connection\ConnectionException($connection1, 'Unknown connection error [127.0.0.1:6381]')
));
$connection2 = $this->getMockConnection('tcp://127.0.0.1:6382?slots=5461-10921');
$connection2->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->throwException(
new Connection\ConnectionException($connection2, 'Unknown connection error [127.0.0.1:6383]')
));
$connection3 = $this->getMockConnection('tcp://127.0.0.1:6383?slots=10922-16383');
$connection3->expects($this->once())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->returnValue($slotsmap));
$factory = $this->getMock('Predis\Connection\FactoryInterface');
$factory->expects($this->never())->method('create');
// TODO: I'm not sure about mocking a protected method, but it'll do for now
$cluster = $this->getMock('Predis\Connection\Aggregate\RedisCluster', array('getRandomConnection'), array($factory));
$cluster->expects($this->exactly(3))
->method('getRandomConnection')
->will($this->onConsecutiveCalls($connection1, $connection2, $connection3));
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$this->assertCount(16384, $cluster->askSlotsMap());
}
/**
* @group disconnected
* @expectedException \Predis\Connection\ConnectionException
* @expectedExceptionMessage Unknown connection error [127.0.0.1:6382]
*/
public function testAskSlotsMapHonorsRetryLimitOnMultipleConnectionFailures()
{
$slotsmap = array(
array(0, 5460, array('127.0.0.1', 9381), array()),
array(5461, 10921, array('127.0.0.1', 6382), array()),
array(10922, 16383, array('127.0.0.1', 6383), array()),
);
$connection1 = $this->getMockConnection('tcp://127.0.0.1:6381?slots=0-5460');
$connection1->expects($this->any())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->throwException(
new Connection\ConnectionException($connection1, 'Unknown connection error [127.0.0.1:6381]')
));
$connection2 = $this->getMockConnection('tcp://127.0.0.1:6382?slots=5461-10921');
$connection2->expects($this->any())
->method('executeCommand')
->with($this->isRedisCommand(
'CLUSTER', array('SLOTS')
))
->will($this->throwException(
new Connection\ConnectionException($connection2, 'Unknown connection error [127.0.0.1:6382]')
));
$connection3 = $this->getMockConnection('tcp://127.0.0.1:6383?slots=10922-16383');
$connection3->expects($this->never())
->method('executeCommand');
$factory = $this->getMock('Predis\Connection\FactoryInterface');
$factory->expects($this->never())->method('create');
// TODO: I'm not sure about mocking a protected method, but it'll do for now
$cluster = $this->getMock('Predis\Connection\Aggregate\RedisCluster', array('getRandomConnection'), array($factory));
$cluster->expects($this->exactly(2))
->method('getRandomConnection')
->will($this->onConsecutiveCalls($connection1, $connection2));
$cluster->add($connection1);
$cluster->add($connection2);
$cluster->add($connection3);
$cluster->setRetryLimit(1);
$cluster->askSlotsMap();
}
/**
* @group disconnected
*/
@@ -673,11 +983,11 @@ class RedisClusterTest extends PredisTestCase
{
$response = array(
array(12288, 13311, array('10.1.0.51', 6387), array('10.1.0.52', 6387)),
array(3072 , 4095, array('10.1.0.52', 6392), array('10.1.0.51', 6392)),
array(6144 , 7167, array('', 6384), array('10.1.0.52', 6384)),
array(3072, 4095, array('10.1.0.52', 6392), array('10.1.0.51', 6392)),
array(6144, 7167, array('', 6384), array('10.1.0.52', 6384)),
array(14336, 15359, array('10.1.0.51', 6388), array('10.1.0.52', 6388)),
array(15360, 16383, array('10.1.0.52', 6398), array('10.1.0.51', 6398)),
array(1024 , 2047, array('10.1.0.52', 6391), array('10.1.0.51', 6391)),
array(1024, 2047, array('10.1.0.52', 6391), array('10.1.0.51', 6391)),
array(11264, 12287, array('10.1.0.52', 6396), array('10.1.0.51', 6396)),
array(5120, 6143, array('10.1.0.52', 6393), array('10.1.0.51', 6393)),
array(0, 1023, array('10.1.0.51', 6381), array('10.1.0.52', 6381)),
@@ -716,7 +1026,7 @@ class RedisClusterTest extends PredisTestCase
$cmdGET = Command\RawCommand::create('GET', 'node:1001');
$rspMOVED = new Response\Error('MOVED 1970 127.0.0.1:6380');
$rspSlotsArray = array(
array(0 , 8191, array('127.0.0.1', 6379)),
array(0, 8191, array('127.0.0.1', 6379)),
array(8192, 16383, array('127.0.0.1', 6380)),
);
+1 -1
View File
@@ -126,7 +126,7 @@ class FactoryTest extends PredisTestCase
public function testCreateConnectionWithoutInitializationCommands()
{
$profile = $this->getMock('Predis\Profile\ProfileInterface');
$profile->expects($this->never())->method('create');
$profile->expects($this->never())->method('createCommand');
$factory = new Factory($profile);
$parameters = new Parameters();
@@ -190,6 +190,13 @@ class RedisUnstableTest extends PredisProfileTestCase
149 => 'PFMERGE',
150 => 'COMMAND',
151 => 'HSTRLEN',
152 => 'BITFIELD',
153 => 'GEOADD',
154 => 'GEOHASH',
155 => 'GEOPOS',
156 => 'GEODIST',
157 => 'GEORADIUS',
158 => 'GEORADIUSBYMEMBER',
);
}
}
@@ -0,0 +1,202 @@
<?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\Profile;
/**
*
*/
class RedisVersion320Test extends PredisProfileTestCase
{
/**
* {@inheritdoc}
*/
public function getProfile($version = null)
{
return new RedisVersion320();
}
/**
* {@inheritdoc}
*/
public function getExpectedVersion()
{
return '3.2';
}
/**
* {@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 => 'DUMP',
13 => 'RESTORE',
14 => 'SET',
15 => 'SETNX',
16 => 'MSET',
17 => 'MSETNX',
18 => 'GET',
19 => 'MGET',
20 => 'GETSET',
21 => 'INCR',
22 => 'INCRBY',
23 => 'DECR',
24 => 'DECRBY',
25 => 'RPUSH',
26 => 'LPUSH',
27 => 'LLEN',
28 => 'LRANGE',
29 => 'LTRIM',
30 => 'LINDEX',
31 => 'LSET',
32 => 'LREM',
33 => 'LPOP',
34 => 'RPOP',
35 => 'RPOPLPUSH',
36 => 'SADD',
37 => 'SREM',
38 => 'SPOP',
39 => 'SMOVE',
40 => 'SCARD',
41 => 'SISMEMBER',
42 => 'SINTER',
43 => 'SINTERSTORE',
44 => 'SUNION',
45 => 'SUNIONSTORE',
46 => 'SDIFF',
47 => 'SDIFFSTORE',
48 => 'SMEMBERS',
49 => 'SRANDMEMBER',
50 => 'ZADD',
51 => 'ZINCRBY',
52 => 'ZREM',
53 => 'ZRANGE',
54 => 'ZREVRANGE',
55 => 'ZRANGEBYSCORE',
56 => 'ZCARD',
57 => 'ZSCORE',
58 => 'ZREMRANGEBYSCORE',
59 => 'PING',
60 => 'AUTH',
61 => 'SELECT',
62 => 'ECHO',
63 => 'QUIT',
64 => 'INFO',
65 => 'SLAVEOF',
66 => 'MONITOR',
67 => 'DBSIZE',
68 => 'FLUSHDB',
69 => 'FLUSHALL',
70 => 'SAVE',
71 => 'BGSAVE',
72 => 'LASTSAVE',
73 => 'SHUTDOWN',
74 => 'BGREWRITEAOF',
75 => 'SETEX',
76 => 'APPEND',
77 => 'SUBSTR',
78 => 'BLPOP',
79 => 'BRPOP',
80 => 'ZUNIONSTORE',
81 => 'ZINTERSTORE',
82 => 'ZCOUNT',
83 => 'ZRANK',
84 => 'ZREVRANK',
85 => 'ZREMRANGEBYRANK',
86 => 'HSET',
87 => 'HSETNX',
88 => 'HMSET',
89 => 'HINCRBY',
90 => 'HGET',
91 => 'HMGET',
92 => 'HDEL',
93 => 'HEXISTS',
94 => 'HLEN',
95 => 'HKEYS',
96 => 'HVALS',
97 => 'HGETALL',
98 => 'MULTI',
99 => 'EXEC',
100 => 'DISCARD',
101 => 'SUBSCRIBE',
102 => 'UNSUBSCRIBE',
103 => 'PSUBSCRIBE',
104 => 'PUNSUBSCRIBE',
105 => 'PUBLISH',
106 => 'CONFIG',
107 => 'PERSIST',
108 => 'STRLEN',
109 => 'SETRANGE',
110 => 'GETRANGE',
111 => 'SETBIT',
112 => 'GETBIT',
113 => 'RPUSHX',
114 => 'LPUSHX',
115 => 'LINSERT',
116 => 'BRPOPLPUSH',
117 => 'ZREVRANGEBYSCORE',
118 => 'WATCH',
119 => 'UNWATCH',
120 => 'OBJECT',
121 => 'SLOWLOG',
122 => 'CLIENT',
123 => 'PTTL',
124 => 'PEXPIRE',
125 => 'PEXPIREAT',
126 => 'MIGRATE',
127 => 'PSETEX',
128 => 'INCRBYFLOAT',
129 => 'BITOP',
130 => 'BITCOUNT',
131 => 'HINCRBYFLOAT',
132 => 'EVAL',
133 => 'EVALSHA',
134 => 'SCRIPT',
135 => 'TIME',
136 => 'SENTINEL',
137 => 'SCAN',
138 => 'BITPOS',
139 => 'SSCAN',
140 => 'ZSCAN',
141 => 'ZLEXCOUNT',
142 => 'ZRANGEBYLEX',
143 => 'ZREMRANGEBYLEX',
144 => 'ZREVRANGEBYLEX',
145 => 'HSCAN',
146 => 'PUBSUB',
147 => 'PFADD',
148 => 'PFCOUNT',
149 => 'PFMERGE',
150 => 'COMMAND',
151 => 'HSTRLEN',
152 => 'BITFIELD',
153 => 'GEOADD',
154 => 'GEOHASH',
155 => 'GEOPOS',
156 => 'GEODIST',
157 => 'GEORADIUS',
158 => 'GEORADIUSBYMEMBER',
);
}
}
@@ -61,4 +61,26 @@ class RequestSerializerTest extends PredisTestCase
$this->assertSame("*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", $result);
}
/**
* @group disconnected
*/
public function testSerializerDoesNotBreakOnArgumentsWithHoles()
{
$serializer = new RequestSerializer();
$command = $this->getMock('Predis\Command\CommandInterface');
$command->expects($this->once())
->method('getId')
->will($this->returnValue('DEL'));
$command->expects($this->once())
->method('getArguments')
->will($this->returnValue(array(0 => 'key:1', 2 => 'key:2')));
$result = $serializer->serialize($command);
$this->assertSame("*3\r\n$3\r\nDEL\r\n$5\r\nkey:1\r\n$5\r\nkey:2\r\n", $result);
}
}
@@ -94,6 +94,105 @@ class ReplicationStrategyTest extends PredisTestCase
);
}
/**
* @group disconnected
*/
public function testBitFieldCommand()
{
$profile = Profile\Factory::getDevelopment();
$strategy = new ReplicationStrategy();
$command = $profile->createCommand('BITFIELD', array('key'));
$this->assertTrue(
$strategy->isReadOperation($command),
'BITFIELD with no modifiers is expected to be a read operation.'
);
$command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0'));
$this->assertTrue(
$strategy->isReadOperation($command),
'BITFIELD with GET only is expected to be a read operation.'
);
$command = $profile->createCommand('BITFIELD', array('key', 'SET', 'u4', '0', 1));
$this->assertFalse(
$strategy->isReadOperation($command),
'BITFIELD with SET is expected to be a write operation.'
);
$command = $profile->createCommand('BITFIELD', array('key', 'INCRBY', 'u4', '0', 1));
$this->assertFalse(
$strategy->isReadOperation($command),
'BITFIELD with INCRBY is expected to be a write operation.'
);
$command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0', 'INCRBY', 'u4', '0', 1));
$this->assertFalse(
$strategy->isReadOperation($command),
'BITFIELD with GET and INCRBY is expected to be a write operation.'
);
$command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0', 'SET', 'u4', '0', 1));
$this->assertFalse(
$strategy->isReadOperation($command),
'BITFIELD with GET and SET is expected to be a write operation.'
);
}
/**
* @group disconnected
*/
public function testGeoradiusCommand()
{
$profile = Profile\Factory::getDevelopment();
$strategy = new ReplicationStrategy();
$command = $profile->createCommand('GEORADIUS', array('key:geo', 15, 37, 200, 'km'));
$this->assertTrue(
$strategy->isReadOperation($command),
'GEORADIUS is expected to be a read operation.'
);
$command = $profile->createCommand('GEORADIUS', array('key:geo', 15, 37, 200, 'km', 'store', 'key:store'));
$this->assertFalse(
$strategy->isReadOperation($command),
'GEORADIUS with STORE is expected to be a write operation.'
);
$command = $profile->createCommand('GEORADIUS', array('key:geo', 15, 37, 200, 'km', 'storedist', 'key:storedist'));
$this->assertFalse(
$strategy->isReadOperation($command),
'GEORADIUS with STOREDIST is expected to be a write operation.'
);
}
/**
* @group disconnected
*/
public function testGeoradiusByMemberCommand()
{
$profile = Profile\Factory::getDevelopment();
$strategy = new ReplicationStrategy();
$command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km'));
$this->assertTrue(
$strategy->isReadOperation($command),
'GEORADIUSBYMEMBER is expected to be a read operation.'
);
$command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km', 'store', 'key:store'));
$this->assertFalse(
$strategy->isReadOperation($command),
'GEORADIUSBYMEMBER with STORE is expected to be a write operation.'
);
$command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km', 'storedist', 'key:storedist'));
$this->assertFalse(
$strategy->isReadOperation($command),
'GEORADIUSBYMEMBER with STOREDIST is expected to be a write operation.'
);
}
/**
* @group disconnected
* @expectedException \Predis\NotSupportedException
@@ -316,6 +415,7 @@ class ReplicationStrategyTest extends PredisTestCase
'SETRANGE' => 'write',
'STRLEN' => 'read',
'SUBSTR' => 'read',
'BITFIELD' => 'variable',
/* commands operating on lists */
'LINSERT' => 'write',
@@ -392,6 +492,14 @@ class ReplicationStrategyTest extends PredisTestCase
/* scripting */
'EVAL' => 'write',
'EVALSHA' => 'write',
/* commands performing geospatial operations */
'GEOADD' => 'write',
'GEOHASH' => 'read',
'GEOPOS' => 'read',
'GEODIST' => 'read',
'GEORADIUS' => 'variable',
'GEORADIUSBYMEMBER' => 'variable',
);
if (isset($type)) {
+1 -1
View File
@@ -47,7 +47,7 @@ Integration tests for commands that are not defined in the specified server prof
of the `REDIS_SERVER_VERSION` constant in `phpunit.xml`) are marked as __skipped__ automatically.
By default, the test suite is configured to execute integration tests using the server profile for
Redis v2.8. You can run the suite against a Redis instance built from the `unstable` branch with the
Redis 3.2. You can run the suite against a Redis instance built from the `unstable` branch with the
development profile by changing the `REDIS_SERVER_VERSION` to `dev` in the `phpunit.xml` file.
If you do not have a Redis instance up and running or available for testing, you can completely