Compare commits

...

59 Commits

Author SHA1 Message Date
Daniele Alessandri ec42796cb2 Bump version, update CHANGELOG, fasten seat belts and get ready for this new year release. 2011-01-01 18:49:46 +01:00
Daniele Alessandri c75bdd9509 Update README. 2011-01-01 17:14:08 +01:00
Daniele Alessandri 12c3e611a0 Update CHANGELOG. 2011-01-01 14:40:28 +01:00
Daniele Alessandri 0c9b822095 Code style changes only. 2011-01-01 13:49:55 +01:00
Daniele Alessandri 030d9740bd Cannot execute with a transaction block after using fluent interface style with Predis\MultiExecBlock. 2011-01-01 13:49:39 +01:00
Daniele Alessandri 0bbbe064b5 Improve fluent-interface style for transactions with CAS. 2011-01-01 13:49:39 +01:00
Daniele Alessandri 87439ae631 Cannot use the 'retry' option for transactions without providing a function block. 2011-01-01 13:49:39 +01:00
Daniele Alessandri d8c227e074 Add an example for MULTI/EXEC transactions using CAS. 2011-01-01 13:49:38 +01:00
Daniele Alessandri 7997eab57c Do not fail with empty transactions when using CAS. 2011-01-01 13:49:38 +01:00
Daniele Alessandri 2bc61ea0bc Update README. 2011-01-01 13:49:38 +01:00
Daniele Alessandri d667bcb6bb Fix and optimize WATCH with CAS. 2011-01-01 13:49:38 +01:00
Daniele Alessandri e3dd311dd3 Add support for optional automatic retries upon aborted transactions. 2011-01-01 13:49:08 +01:00
Daniele Alessandri 34f4d5584b Support DISCARD with CAS. 2011-01-01 13:49:08 +01:00
Daniele Alessandri 39619d0c54 Add support for check-and-set (CAS) for transactions with Predis\MultiExecBlock. 2011-01-01 13:49:08 +01:00
Daniele Alessandri 07a998ebdf Test suite: tiny fix in a test for LINSERT (again, this time for real). 2011-01-01 13:47:23 +01:00
Daniele Alessandri b73b9682c0 Test suite: tiny fix in a test for LINSERT. 2010-12-21 18:37:02 +01:00
Daniele Alessandri 355d6b6cf4 Update CHANGELOG. 2010-12-18 17:16:06 +01:00
Daniele Alessandri fe2316a655 Test suite: temporary fix for testSetExpire(). 2010-12-18 17:14:17 +01:00
Daniele Alessandri 7b2cd4abd0 Remove unused class. 2010-12-18 17:11:13 +01:00
Daniele Alessandri 35fd6ca509 New command: BRPOPLPUSH (Redis v2.2-dev). 2010-12-18 17:09:28 +01:00
Daniele Alessandri dea03a6aa9 Test suite: move up testStrlen(). 2010-12-18 16:53:31 +01:00
Daniele Alessandri fb5f878e21 New command: GETBIT (Redis v2.2-dev). 2010-12-18 16:53:26 +01:00
Daniele Alessandri e3ee595768 New command: SETBIT (Redis v2.2-dev). 2010-12-18 15:53:01 +01:00
Daniele Alessandri cc16311950 New command: SETRANGE (Redis v2.2-dev). 2010-12-18 13:11:59 +01:00
Daniele Alessandri f32cd19800 New command: GETRANGE (Redis v2.2-dev - replaces SUBSTR). 2010-12-18 12:36:04 +01:00
Daniele Alessandri 7db5e650ef Bump version and... release! 2010-11-28 20:23:39 +01:00
Daniele Alessandri 975bd98933 Update CHANGELOG. 2010-11-28 19:22:26 +01:00
Daniele Alessandri 7d5cccaf7c Overridden methods for UNWATCH and DISCARD in Predis\MultiExecBlock must return $this to not break method chaining. 2010-11-28 19:19:48 +01:00
Daniele Alessandri 08b1761e10 Fix a bunch of broken tests. 2010-11-28 17:21:46 +01:00
Daniele Alessandri 24f49abc14 Update CHANGELOG. 2010-11-28 16:54:02 +01:00
Daniele Alessandri b530f9f613 New command: PERSIST (Redis v2.2-dev). 2010-11-28 16:49:20 +01:00
Daniele Alessandri d677c72ae3 New command: ZREVRANGEBYSCORE (Redis v2.2-dev). 2010-11-28 16:39:07 +01:00
Daniele Alessandri c3cf2acb51 Update CHANGELOG. 2010-09-25 17:54:52 +02:00
Daniele Alessandri 8bfae69bf0 Make WATCH accept an array of keys that must be watched. 2010-09-25 17:52:23 +02:00
Daniele Alessandri 92d6ad62ce Test suite: add test for WATCH in Predis\MultiExecBlock. 2010-09-25 17:08:30 +02:00
Daniele Alessandri fa4d654d38 Update CHANGELOG. 2010-09-25 17:04:29 +02:00
Daniele Alessandri 9ac4571f91 Fix improper handling of a few corner cases for Predis\MultiExecBlock and send DISCARD to the server on uncaught exceptions inside a callable block. 2010-09-25 16:55:31 +02:00
Daniele Alessandri fea5e1c602 Test suite: add missing tests for Predis\MultiExecBlock. 2010-09-25 16:53:21 +02:00
Daniele Alessandri ec51b14fbb Fix an infinite recursion loop when calling Predis\Client::multiExec() (ISSUE #14). 2010-09-25 16:52:59 +02:00
Daniele Alessandri dc56526256 New commands: RPUSHX, LPUSHX (Redis v2.2-dev). 2010-09-25 16:38:16 +02:00
Daniele Alessandri 195c6eae99 New command: LINSERT (Redis v2.2-dev). 2010-09-25 16:34:18 +02:00
Daniele Alessandri 9ae47c865e New command: STRLEN (Redis v2.2-dev). 2010-09-25 16:31:35 +02:00
Daniele Alessandri d3ba25436c Slight micro-optimization. 2010-09-25 16:26:55 +02:00
Daniele Alessandri 580cf80436 Get the connection out of a cluster only once inside Predis\Client::getClientFor(). 2010-09-18 18:56:39 +02:00
Daniele Alessandri d6bea4390e Perform (p)unsubscribtion only if (p)subscribed to a channel when the destructor for Predis\PubSubContext is invoked. 2010-09-17 22:24:59 +02:00
Daniele Alessandri fd7f4a7b48 Make arguments handling more solid for the SORT command. 2010-09-17 22:11:37 +02:00
Daniele Alessandri 92f7e8bf9b Remove useless code duplication. 2010-09-17 21:56:31 +02:00
Daniele Alessandri 006b44e4bf Fix checks on the value for the STORE argument (ISSUE #13). 2010-09-17 11:36:47 +02:00
Daniele Alessandri bd961a767b Update CHANGELOG. It's release time! 2010-07-11 17:12:28 +02:00
Daniele Alessandri b3599a8e6b Update CHANGELOG. 2010-07-11 17:11:58 +02:00
Daniele Alessandri 0b46de424e Test suite: add a test for the LIMIT modifier of ZRANGEBYSCORE configured via associative array. 2010-07-11 17:02:33 +02:00
Daniele Alessandri 73bdfc2d72 Fix handling of the WEIGHT modifier for ZUNIONSTORE and ZINTERSTORE with more than two weights specified. 2010-07-11 16:35:03 +02:00
Daniele Alessandri 3077e76708 Update CHANGELOG. 2010-07-11 16:24:06 +02:00
Daniele Alessandri 208fdf6daf Update CHANGELOG. 2010-07-11 15:32:14 +02:00
Daniele Alessandri 7d7995ff8f Update CHANGELOG. 2010-07-07 20:07:15 +02:00
Daniele Alessandri 418197af75 Add tests for the new options handling for the ZRANGE commands family. 2010-07-07 19:59:16 +02:00
Daniele Alessandri 90cb9d437d Add support for the LIMIT modifier in ZRANGEBYSCORE. 2010-07-07 19:58:52 +02:00
Daniele Alessandri 4668bdca83 ZRANGE, ZREVRANGE and ZRANGEBYSCORE accepts an array instance for optional modifiers. 2010-07-07 19:58:41 +02:00
Daniele Alessandri 93a3c184ed Bump version number. 2010-07-02 22:31:39 +02:00
8 changed files with 933 additions and 157 deletions
+56 -12
View File
@@ -1,13 +1,54 @@
v0.6.1 (2010-xx-xx) v0.6.3 (2011-01-01)
* New commands available in the Redis v2.2 profile (dev):
- Strings: SETRANGE, GETRANGE, SETBIT, GETBIT
- Lists : BRPOPLPUSH
* The abstraction for MULTI/EXEC transactions has been dramatically improved
by providing support for check-and-set (CAS) operations when using Redis >=
2.2. Aborted transactions can also be optionally replayed in automatic up
to a user-defined number of times, after which a Predis\AbortedMultiExec
exception is thrown.
v0.6.2 (2010-11-28)
* Minor internal improvements and clean ups.
* New commands available in the Redis v2.2 profile (dev):
- Strings: STRLEN
- Lists : LINSERT, RPUSHX, LPUSHX
- ZSets : ZREVRANGEBYSCORE
- Misc. : PERSIST
* WATCH also accepts a single array parameter with the keys that should be
monitored during a transaction.
* Improved the behaviour of Predis\MultiExecBlock in certain corner cases.
* Improved parameters checking for the SORT command.
* FIX: the STORE parameter for the SORT command didn't work correctly when
using '0' as the target key (ISSUE #13).
* FIX: the methods for UNWATCH and DISCARD do not break anymore method
chaining with Predis\MultiExecBlock.
v0.6.1 (2010-07-11)
* Minor internal improvements and clean ups.
* New commands available in the Redis v2.2 profile (dev): * New commands available in the Redis v2.2 profile (dev):
- Misc. : WATCH, UNWATCH - Misc. : WATCH, UNWATCH
* Minor internal improvements and clean ups. * Optional modifiers for ZRANGE, ZREVRANGE and ZRANGEBYSCORE queries are
supported using an associative array passed as the last argument of their
respective methods.
* The constructor of Predis\Client::__construct now accepts also instances * The LIMIT modifier for ZRANGEBYSCORE can be specified using either:
of Predis\ConnectionParameters. - an indexed array: array($offset, $count)
- an associative array: array('offset' => $offset, 'count' => $count)
* Predis\MultiExecBlock and Predis\PubSubContext will throw an exception * The method Predis\Client::__construct() now accepts also instances of
Predis\ConnectionParameters.
* Predis\MultiExecBlock and Predis\PubSubContext now throw an exception
when trying to create their instances using a profile that does not when trying to create their instances using a profile that does not
support the required Redis commands or when the client is connected to support the required Redis commands or when the client is connected to
a cluster of connections. a cluster of connections.
@@ -17,14 +58,17 @@ v0.6.1 (2010-xx-xx)
- support for WATCH and UNWATCH when using the current development - support for WATCH and UNWATCH when using the current development
profile (Redis v2.2) and aborted transactions. profile (Redis v2.2) and aborted transactions.
* New method signature for Predis\Client::multiExec(). Now it is able to * New signature for Predis\Client::multiExec() which is now able to accept
accept an array of options for the underlying Predis\MultiExecBlock, but an array of options for the underlying instance of Predis\MultiExecBlock.
it is still backwards compatible with previous releases of Predis. Backwards compatibility with previous releases of Predis is ensured.
* New method signature for Predis\Client::pipeline(). Now it is able to * New signature for Predis\Client::pipeline() which is now able to accept
accept an array of options for the underlying Predis\CommandPipeline, an array of options for the underlying instance of Predis\CommandPipeline.
but it is still backwards compatible with previous releases of Predis. Backwards compatibility with previous releases of Predis is ensured.
Predis\Client::pipelineSafe() is to be considered obsolete. The method Predis\Client::pipelineSafe() is to be considered deprecated.
* FIX: The WEIGHT modifier for ZUNIONSTORE and ZINTERSTORE was handled
incorrectly with more than two weights specified.
v0.6.0 (2010-05-24) v0.6.0 (2010-05-24)
* Switched to the new multi-bulk request protocol for all of the commands * Switched to the new multi-bulk request protocol for all of the commands
+3 -2
View File
@@ -20,6 +20,7 @@ to be implemented soon in Predis.
- Full support for Redis 2.0. Different versions of Redis are supported via server profiles. - Full support for Redis 2.0. Different versions of Redis are supported via server profiles.
- Client-side sharding (support for consistent hashing and custom distribution strategies). - Client-side sharding (support for consistent hashing and custom distribution strategies).
- Command pipelining on single and multiple connections (transparent). - Command pipelining on single and multiple connections (transparent).
- Abstraction for Redis transactions (>= 2.0) with support for CAS operations (>= 2.2).
- Lazy connections (connections to Redis instances are only established just in time). - Lazy connections (connections to Redis instances are only established just in time).
- Flexible system to define and register your own set of commands to a client instance. - Flexible system to define and register your own set of commands to a client instance.
@@ -60,10 +61,10 @@ Furthermore, a pipeline can be initialized on a cluster of redis instances in th
same exact way they are created on single connection. Sharding is still transparent same exact way they are created on single connection. Sharding is still transparent
to the user: to the user:
$redis = Predis\Client::create( $redis = new Predis\Client(array(
array('host' => '10.0.0.1', 'port' => 6379), array('host' => '10.0.0.1', 'port' => 6379),
array('host' => '10.0.0.2', 'port' => 6379) array('host' => '10.0.0.2', 'port' => 6379)
); ));
$replies = $redis->pipeline(function($pipe) { $replies = $redis->pipeline(function($pipe) {
for ($i = 0; $i < 1000; $i++) { for ($i = 0; $i < 1000; $i++) {
+1 -1
View File
@@ -1 +1 @@
0.6.0 0.6.3
+41
View File
@@ -0,0 +1,41 @@
<?php
require_once 'SharedConfigurations.php';
/*
This is an implementation of an atomic client-side ZPOP using the support for
check-and-set (CAS) operations with MULTI/EXEC transactions, as described in
"WATCH explained" from http://redis.io/topics/transactions
First, populate your database with a tiny sample data set:
./redis-cli
SELECT 15
ZADD zset 1 a
ZADD zset 2 b
ZADD zset 3 c
*/
function zpop($client, $zsetKey) {
$element = null;
$options = array(
'cas' => true, // Initialize with support for CAS operations
'watch' => $zsetKey, // Key that needs to be WATCHed to detect changes
'retry' => 3, // Number of retries on aborted transactions, after
// which the client bails out with an exception.
);
$txReply = $client->multiExec($options, function($tx)
use ($zsetKey, &$element) {
@list($element) = $tx->zrange($zsetKey, 0, 0);
if (isset($element)) {
$tx->multi(); // With CAS, MULTI *must* be explicitly invoked.
$tx->zrem($zsetKey, $element);
}
});
return $element;
}
$redis = new Predis\Client($single_server, 'dev');
$zpopped = zpop($redis, 'zset');
echo isset($zpopped) ? "ZPOPed $zpopped" : "Nothing to ZPOP!", "\n";
?>
+264 -135
View File
@@ -170,7 +170,7 @@ class Client {
$newClient = new Client(); $newClient = new Client();
$newClient->setupClient($this->_options); $newClient->setupClient($this->_options);
$newClient->setConnection($this->getConnection($connectionAlias)); $newClient->setConnection($connection);
return $newClient; return $newClient;
} }
@@ -230,21 +230,24 @@ class Client {
return $this->_connection->rawCommand($rawCommandData, $closesConnection); return $this->_connection->rawCommand($rawCommandData, $closesConnection);
} }
public function pipeline(/* arguments */) { private function sharedInitializer($argv, $initializer) {
$argv = func_get_args(); $argc = count($argv);
$argc = func_num_args();
if ($argc === 0) { if ($argc === 0) {
return $this->initPipeline(); return $this->$initializer();
} }
else if ($argc === 1) { else if ($argc === 1) {
list($arg0) = $argv; list($arg0) = $argv;
return is_array($arg0) ? $this->initPipeline($arg0) : $this->initPipeline(null, $arg0); return is_array($arg0) ? $this->$initializer($arg0) : $this->$initializer(null, $arg0);
} }
else if ($argc === 2) { else if ($argc === 2) {
list($arg0, $arg1) = $argv; list($arg0, $arg1) = $argv;
return $this->initPipeline($arg0, $arg1); return $this->$initializer($arg0, $arg1);
} }
return $this->$initializer($this, $arguments);
}
public function pipeline(/* arguments */) {
return $this->sharedInitializer(func_get_args(), 'initPipeline');
} }
public function pipelineSafe($pipelineBlock = null) { public function pipelineSafe($pipelineBlock = null) {
@@ -276,20 +279,7 @@ class Client {
} }
public function multiExec(/* arguments */) { public function multiExec(/* arguments */) {
$argv = func_get_args(); return $this->sharedInitializer(func_get_args(), 'initMultiExec');
$argc = func_num_args();
if ($argc === 0) {
return $this->initMultiExec();
}
else if ($argc === 1) {
list($arg0) = $argv;
return is_array($arg0) ? $this->initMultiExec($arg0) : $this->initMultiExec(null, $arg0);
}
else if ($argc === 2) {
list($arg0, $arg1) = $argv;
return $this->initMultiExec($arg0, $arg1);
}
} }
private function initMultiExec(Array $options = null, $transBlock = null) { private function initMultiExec(Array $options = null, $transBlock = null) {
@@ -470,12 +460,12 @@ abstract class Command {
public function setArguments(/* arguments */) { public function setArguments(/* arguments */) {
$this->_arguments = $this->filterArguments(func_get_args()); $this->_arguments = $this->filterArguments(func_get_args());
$this->_hash = null; unset($this->_hash);
} }
public function setArgumentsArray(Array $arguments) { public function setArgumentsArray(Array $arguments) {
$this->_arguments = $this->filterArguments($arguments); $this->_arguments = $this->filterArguments($arguments);
$this->_hash = null; unset($this->_hash);
} }
public function getArguments() { public function getArguments() {
@@ -815,18 +805,15 @@ class CommandPipeline {
} }
class MultiExecBlock { class MultiExecBlock {
private $_initialized, $_discarded, $_insideBlock; private $_initialized, $_discarded, $_insideBlock, $_checkAndSet;
private $_redisClient, $_options, $_commands; private $_redisClient, $_options, $_commands;
private $_supportsWatch; private $_supportsWatch;
public function __construct(Client $redisClient, Array $options = null) { public function __construct(Client $redisClient, Array $options = null) {
$this->checkCapabilities($redisClient); $this->checkCapabilities($redisClient);
$this->_initialized = false; $this->_options = $options ?: array();
$this->_discarded = false;
$this->_insideBlock = false;
$this->_redisClient = $redisClient; $this->_redisClient = $redisClient;
$this->_options = $options ?: array(); $this->reset();
$this->_commands = array();
} }
private function checkCapabilities(Client $redisClient) { private function checkCapabilities(Client $redisClient) {
@@ -852,136 +839,184 @@ class MultiExecBlock {
} }
} }
private function initialize() { private function reset() {
if ($this->_initialized === false) { $this->_initialized = false;
if (isset($this->_options['watch'])) { $this->_discarded = false;
$this->watch($this->_options['watch']); $this->_checkAndSet = false;
} $this->_insideBlock = false;
$this->_redisClient->multi(); $this->_commands = array();
$this->_initialized = true;
$this->_discarded = false;
}
} }
private function setInsideBlock($value) { private function initialize() {
$this->_insideBlock = $value; if ($this->_initialized === true) {
return;
}
$options = &$this->_options;
$this->_checkAndSet = isset($options['cas']) && $options['cas'];
if (isset($options['watch'])) {
$this->watch($options['watch']);
}
if (!$this->_checkAndSet || ($this->_discarded && $this->_checkAndSet)) {
$this->_redisClient->multi();
if ($this->_discarded) {
$this->_checkAndSet = false;
}
}
$this->_initialized = true;
$this->_discarded = false;
} }
public function __call($method, $arguments) { public function __call($method, $arguments) {
$this->initialize(); $this->initialize();
$command = $this->_redisClient->createCommand($method, $arguments); $client = $this->_redisClient;
$response = $this->_redisClient->executeCommand($command); if ($this->_checkAndSet) {
if (isset($response->queued)) { return call_user_func_array(array($client, $method), $arguments);
$this->_commands[] = $command;
return $this;
} }
else { $command = $client->createCommand($method, $arguments);
$this->malformedServerResponse('The server did not respond with a QUEUED status reply'); $response = $client->executeCommand($command);
if (!isset($response->queued)) {
$this->malformedServerResponse(
'The server did not respond with a QUEUED status reply'
);
} }
$this->_commands[] = $command;
return $this;
} }
public function watch($keys) { public function watch($keys) {
$this->isWatchSupported(); $this->isWatchSupported();
if ($this->_initialized === true) { if ($this->_initialized && !$this->_checkAndSet) {
throw new \Predis\ClientException('WATCH inside MULTI is not allowed'); throw new ClientException('WATCH inside MULTI is not allowed');
} }
return $this->_redisClient->watch($keys);
$reply = null;
if (is_array($keys)) {
$reply = array();
foreach ($keys as $key) {
$reply = $this->_redisClient->watch($keys);
}
}
else {
$reply = $this->_redisClient->watch($keys);
}
return $reply;
} }
public function multi() { public function multi() {
if ($this->_initialized && $this->_checkAndSet) {
$this->_checkAndSet = false;
$this->_redisClient->multi();
return $this;
}
$this->initialize(); $this->initialize();
return $this;
} }
public function unwatch() { public function unwatch() {
$this->isWatchSupported(); $this->isWatchSupported();
$this->_redisClient->unwatch(); $this->_redisClient->unwatch();
return $this;
} }
public function discard() { public function discard() {
$this->_redisClient->discard(); $this->_redisClient->discard();
$this->_commands = array(); $this->reset();
$this->_initialized = false; $this->_discarded = true;
$this->_discarded = true; return $this;
} }
public function exec() { public function exec() {
return $this->execute(); return $this->execute();
} }
public function execute($block = null) { private function checkBeforeExecution($block) {
if ($this->_insideBlock === true) { if ($this->_insideBlock === true) {
throw new \Predis\ClientException( throw new \Predis\ClientException(
"Cannot invoke 'execute' or 'exec' inside an active client transaction block" "Cannot invoke 'execute' or 'exec' inside an active client transaction block"
); );
} }
if ($block) {
if ($block && !is_callable($block)) { if (!is_callable($block)) {
throw new \InvalidArgumentException('Argument passed must be a callable object'); throw new \InvalidArgumentException(
'Argument passed must be a callable object'
);
}
if (count($this->_commands) > 0) {
throw new ClientException(
'Cannot execute a transaction block after using fluent interface'
);
}
} }
if (isset($this->_options['retry']) && !isset($block)) {
$this->discard();
throw new \InvalidArgumentException(
'Automatic retries can be used only when a transaction block is provided'
);
}
}
$blockException = null; public function execute($block = null) {
$returnValues = array(); $this->checkBeforeExecution($block);
try { $reply = null;
$returnValues = array();
$attemptsLeft = isset($this->_options['retry']) ? (int)$this->_options['retry'] : 0;
do {
$blockException = null;
if ($block !== null) { if ($block !== null) {
$this->setInsideBlock(true); $this->_insideBlock = true;
$block($this); try {
$this->setInsideBlock(false); $block($this);
}
catch (CommunicationException $exception) {
$blockException = $exception;
}
catch (ServerException $exception) {
$blockException = $exception;
}
catch (\Exception $exception) {
$blockException = $exception;
if ($this->_initialized === true) {
$this->discard();
}
}
$this->_insideBlock = false;
if ($blockException !== null) {
throw $blockException;
}
} }
if ($this->_discarded === true) { if ($this->_initialized === false || count($this->_commands) == 0) {
return; return;
} }
$reply = $this->_redisClient->exec(); $reply = $this->_redisClient->exec();
if ($reply === null) { if ($reply === null) {
throw new AbortedMultiExec('The current transaction has been aborted by the server'); if ($attemptsLeft === 0) {
throw new AbortedMultiExec(
'The current transaction has been aborted by the server'
);
}
$this->reset();
continue;
} }
break;
} while ($attemptsLeft-- > 0);
$execReply = $reply instanceof \Iterator ? iterator_to_array($reply) : $reply; $execReply = $reply instanceof \Iterator ? iterator_to_array($reply) : $reply;
$commands = &$this->_commands; $sizeofReplies = count($execReply);
$sizeofReplies = count($execReply);
if ($sizeofReplies !== count($commands)) { $commands = &$this->_commands;
$this->malformedServerResponse('Unexpected number of responses for a MultiExecBlock'); if ($sizeofReplies !== count($commands)) {
} $this->malformedServerResponse(
'Unexpected number of responses for a MultiExecBlock'
for ($i = 0; $i < $sizeofReplies; $i++) { );
$returnValues[] = $commands[$i]->parseResponse($execReply[$i] instanceof \Iterator
? iterator_to_array($execReply[$i])
: $execReply[$i]
);
unset($commands[$i]);
}
} }
catch (\Exception $exception) { for ($i = 0; $i < $sizeofReplies; $i++) {
$this->setInsideBlock(false); $returnValues[] = $commands[$i]->parseResponse($execReply[$i] instanceof \Iterator
$blockException = $exception; ? iterator_to_array($execReply[$i])
} : $execReply[$i]
);
if ($blockException !== null) { unset($commands[$i]);
throw $blockException;
} }
return $returnValues; return $returnValues;
} }
private function malformedServerResponse($message) { private function malformedServerResponse($message) {
// NOTE: a MULTI/EXEC block cannot be initialized on a clustered // Since a MULTI/EXEC block cannot be initialized over a clustered
// connection, which means that Predis\Client::getConnection // connection, we can safely assume that Predis\Client::getConnection()
// will always return an instance of Predis\Connection. // will always return an instance of Predis\Connection.
Shared\Utils::onCommunicationException(new MalformedServerResponse( Utils::onCommunicationException(new MalformedServerResponse(
$this->_redisClient->getConnection(), $message $this->_redisClient->getConnection(), $message
)); ));
} }
@@ -1008,10 +1043,7 @@ class PubSubContext implements \Iterator {
} }
public function __destruct() { public function __destruct() {
if ($this->valid()) { $this->closeContext();
$this->_redisClient->unsubscribe();
$this->_redisClient->punsubscribe();
}
} }
private function checkCapabilities(Client $redisClient) { private function checkCapabilities(Client $redisClient) {
@@ -1824,6 +1856,25 @@ class RedisServer_vNext extends RedisServer_v2_0 {
/* transactions */ /* transactions */
'watch' => '\Predis\Commands\Watch', 'watch' => '\Predis\Commands\Watch',
'unwatch' => '\Predis\Commands\Unwatch', 'unwatch' => '\Predis\Commands\Unwatch',
/* commands operating on string values */
'strlen' => '\Predis\Commands\Strlen',
'setrange' => '\Predis\Commands\SetRange',
'getrange' => '\Predis\Commands\Substr',
'setbit' => '\Predis\Commands\SetBit',
'getbit' => '\Predis\Commands\GetBit',
/* commands operating on the key space */
'persist' => '\Predis\Commands\Persist',
/* commands operating on lists */
'rpushx' => '\Predis\Commands\ListPushTailX',
'lpushx' => '\Predis\Commands\ListPushHeadX',
'linsert' => '\Predis\Commands\ListInsert',
'brpoplpush' => '\Predis\Commands\ListPopLastPushHeadBlocking',
/* commands operating on sorted sets */
'zrevrangebyscore' => '\Predis\Commands\ZSetReverseRangeByScore',
)); ));
} }
} }
@@ -2347,10 +2398,26 @@ class Append extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'APPEND'; } public function getCommandId() { return 'APPEND'; }
} }
class SetRange extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'SETRANGE'; }
}
class Substr extends \Predis\MultiBulkCommand { class Substr extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'SUBSTR'; } public function getCommandId() { return 'SUBSTR'; }
} }
class SetBit extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'SETBIT'; }
}
class GetBit extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'GETBIT'; }
}
class Strlen extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'STRLEN'; }
}
/* commands operating on the key space */ /* commands operating on the key space */
class Keys extends \Predis\MultiBulkCommand { class Keys extends \Predis\MultiBulkCommand {
public function canBeHashed() { return false; } public function canBeHashed() { return false; }
@@ -2391,6 +2458,11 @@ class ExpireAt extends \Predis\MultiBulkCommand {
public function parseResponse($data) { return (bool) $data; } public function parseResponse($data) { return (bool) $data; }
} }
class Persist extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'PERSIST'; }
public function parseResponse($data) { return (bool) $data; }
}
class DatabaseSize extends \Predis\MultiBulkCommand { class DatabaseSize extends \Predis\MultiBulkCommand {
public function canBeHashed() { return false; } public function canBeHashed() { return false; }
public function getCommandId() { return 'DBSIZE'; } public function getCommandId() { return 'DBSIZE'; }
@@ -2405,10 +2477,18 @@ class ListPushTail extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'RPUSH'; } public function getCommandId() { return 'RPUSH'; }
} }
class ListPushTailX extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'RPUSHX'; }
}
class ListPushHead extends \Predis\MultiBulkCommand { class ListPushHead extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'LPUSH'; } public function getCommandId() { return 'LPUSH'; }
} }
class ListPushHeadX extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'LPUSHX'; }
}
class ListLength extends \Predis\MultiBulkCommand { class ListLength extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'LLEN'; } public function getCommandId() { return 'LLEN'; }
} }
@@ -2437,8 +2517,8 @@ class ListPopLastPushHead extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'RPOPLPUSH'; } public function getCommandId() { return 'RPOPLPUSH'; }
} }
class ListPopLastPushHeadBulk extends \Predis\MultiBulkCommand { class ListPopLastPushHeadBlocking extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'RPOPLPUSH'; } public function getCommandId() { return 'BRPOPLPUSH'; }
} }
class ListPopFirst extends \Predis\MultiBulkCommand { class ListPopFirst extends \Predis\MultiBulkCommand {
@@ -2457,6 +2537,10 @@ class ListPopLastBlocking extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'BRPOP'; } public function getCommandId() { return 'BRPOP'; }
} }
class ListInsert extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'LINSERT'; }
}
/* commands operating on sets */ /* commands operating on sets */
class SetAdd extends \Predis\MultiBulkCommand { class SetAdd extends \Predis\MultiBulkCommand {
public function getCommandId() { return 'SADD'; } public function getCommandId() { return 'SADD'; }
@@ -2550,8 +2634,9 @@ class ZSetUnionStore extends \Predis\MultiBulkCommand {
$finalizedOpts = array(); $finalizedOpts = array();
if (isset($opts['WEIGHTS']) && is_array($opts['WEIGHTS'])) { if (isset($opts['WEIGHTS']) && is_array($opts['WEIGHTS'])) {
$finalizedOpts[] = 'WEIGHTS'; $finalizedOpts[] = 'WEIGHTS';
$finalizedOpts[] = $opts['WEIGHTS'][0]; foreach ($opts['WEIGHTS'] as $weight) {
$finalizedOpts[] = $opts['WEIGHTS'][1]; $finalizedOpts[] = $weight;
}
} }
if (isset($opts['AGGREGATE'])) { if (isset($opts['AGGREGATE'])) {
$finalizedOpts[] = 'AGGREGATE'; $finalizedOpts[] = 'AGGREGATE';
@@ -2566,20 +2651,42 @@ class ZSetIntersectionStore extends \Predis\Commands\ZSetUnionStore {
} }
class ZSetRange extends \Predis\MultiBulkCommand { class ZSetRange extends \Predis\MultiBulkCommand {
private $_withScores = false;
public function getCommandId() { return 'ZRANGE'; } public function getCommandId() { return 'ZRANGE'; }
public function parseResponse($data) { public function filterArguments(Array $arguments) {
$arguments = $this->getArguments();
if (count($arguments) === 4) { if (count($arguments) === 4) {
if (strtolower($arguments[3]) === 'withscores') { $lastType = gettype($arguments[3]);
if ($data instanceof \Iterator) { if ($lastType === 'string' && strtolower($arguments[3]) === 'withscores') {
return new \Predis\Shared\MultiBulkResponseKVIterator($data); // used for compatibility with older versions
} $arguments[3] = array('WITHSCORES' => true);
$result = array(); $lastType = 'array';
for ($i = 0; $i < count($data); $i++) {
$result[] = array($data[$i], $data[++$i]);
}
return $result;
} }
if ($lastType === 'array') {
$options = $this->prepareOptions(array_pop($arguments));
return array_merge($arguments, $options);
}
}
return $arguments;
}
protected function prepareOptions($options) {
$opts = array_change_key_case($options, CASE_UPPER);
$finalizedOpts = array();
if (isset($opts['WITHSCORES'])) {
$finalizedOpts[] = 'WITHSCORES';
$this->_withScores = true;
}
return $finalizedOpts;
}
public function parseResponse($data) {
if ($this->_withScores) {
if ($data instanceof \Iterator) {
return new \Predis\Shared\MultiBulkResponseKVIterator($data);
}
$result = array();
for ($i = 0; $i < count($data); $i++) {
$result[] = array($data[$i], $data[++$i]);
}
return $result;
} }
return $data; return $data;
} }
@@ -2591,6 +2698,21 @@ class ZSetReverseRange extends \Predis\Commands\ZSetRange {
class ZSetRangeByScore extends \Predis\Commands\ZSetRange { class ZSetRangeByScore extends \Predis\Commands\ZSetRange {
public function getCommandId() { return 'ZRANGEBYSCORE'; } public function getCommandId() { return 'ZRANGEBYSCORE'; }
protected function prepareOptions($options) {
$opts = array_change_key_case($options, CASE_UPPER);
$finalizedOpts = array();
if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
$limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);
$finalizedOpts[] = 'LIMIT';
$finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
$finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
}
return array_merge($finalizedOpts, parent::prepareOptions($options));
}
}
class ZSetReverseRangeByScore extends \Predis\Commands\ZSetRangeByScore {
public function getCommandId() { return 'ZREVRANGEBYSCORE'; }
} }
class ZSetCount extends \Predis\MultiBulkCommand { class ZSetCount extends \Predis\MultiBulkCommand {
@@ -2737,16 +2859,15 @@ class Sort extends \Predis\MultiBulkCommand {
return $arguments; return $arguments;
} }
// TODO: add more parameters checks
$query = array($arguments[0]); $query = array($arguments[0]);
$sortParams = $arguments[1]; $sortParams = array_change_key_case($arguments[1], CASE_UPPER);
if (isset($sortParams['by'])) { if (isset($sortParams['BY'])) {
$query[] = 'BY'; $query[] = 'BY';
$query[] = $sortParams['by']; $query[] = $sortParams['BY'];
} }
if (isset($sortParams['get'])) { if (isset($sortParams['GET'])) {
$getargs = $sortParams['get']; $getargs = $sortParams['GET'];
if (is_array($getargs)) { if (is_array($getargs)) {
foreach ($getargs as $getarg) { foreach ($getargs as $getarg) {
$query[] = 'GET'; $query[] = 'GET';
@@ -2758,20 +2879,22 @@ class Sort extends \Predis\MultiBulkCommand {
$query[] = $getargs; $query[] = $getargs;
} }
} }
if (isset($sortParams['limit']) && is_array($sortParams['limit'])) { if (isset($sortParams['LIMIT']) && is_array($sortParams['LIMIT'])
&& count($sortParams['LIMIT']) == 2) {
$query[] = 'LIMIT'; $query[] = 'LIMIT';
$query[] = $sortParams['limit'][0]; $query[] = $sortParams['LIMIT'][0];
$query[] = $sortParams['limit'][1]; $query[] = $sortParams['LIMIT'][1];
} }
if (isset($sortParams['sort'])) { if (isset($sortParams['SORT'])) {
$query[] = strtoupper($sortParams['sort']); $query[] = strtoupper($sortParams['SORT']);
} }
if (isset($sortParams['alpha']) && $sortParams['alpha'] == true) { if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) {
$query[] = 'ALPHA'; $query[] = 'ALPHA';
} }
if (isset($sortParams['store']) && $sortParams['store'] == true) { if (isset($sortParams['STORE'])) {
$query[] = 'STORE'; $query[] = 'STORE';
$query[] = $sortParams['store']; $query[] = $sortParams['STORE'];
} }
return $query; return $query;
@@ -2797,6 +2920,12 @@ class Discard extends \Predis\MultiBulkCommand {
class Watch extends \Predis\MultiBulkCommand { class Watch extends \Predis\MultiBulkCommand {
public function canBeHashed() { return false; } public function canBeHashed() { return false; }
public function getCommandId() { return 'WATCH'; } public function getCommandId() { return 'WATCH'; }
public function filterArguments(Array $arguments) {
if (isset($arguments[0]) && is_array($arguments[0])) {
return $arguments[0];
}
return $arguments;
}
public function parseResponse($data) { return (bool) $data; } public function parseResponse($data) { return (bool) $data; }
} }
+283 -5
View File
@@ -261,16 +261,19 @@ class PredisClientFeaturesTestSuite extends PHPUnit_Framework_TestCase {
function testConnection_WriteCommandAndCloseConnection() { function testConnection_WriteCommandAndCloseConnection() {
$cmd = \Predis\RedisServerProfile::getDefault()->createCommand('quit'); $cmd = \Predis\RedisServerProfile::getDefault()->createCommand('quit');
$connection = new \Predis\Connection(RC::getConnectionParameters()); $connection = new \Predis\Connection(new \Predis\ConnectionParameters(
$connection->connect(); RC::getConnectionArguments() + array('read_write_timeout' => 0.5)
));
$connection->connect();
$this->assertTrue($connection->isConnected()); $this->assertTrue($connection->isConnected());
$connection->writeCommand($cmd); $connection->writeCommand($cmd);
$connection->disconnect();
$exceptionMessage = 'Error while reading line from the server'; $exceptionMessage = 'Error while reading line from the server';
RC::testForCommunicationException($this, $exceptionMessage, function() use($connection, $cmd) { RC::testForCommunicationException($this, $exceptionMessage, function() use($connection, $cmd) {
$connection->readResponse($cmd); $connection->readResponse($cmd);
}); });
//$this->assertFalse($connection->isConnected());
} }
function testConnection_GetSocketOpensConnection() { function testConnection_GetSocketOpensConnection() {
@@ -354,8 +357,8 @@ class PredisClientFeaturesTestSuite extends PHPUnit_Framework_TestCase {
function testResponseReader_OptionExceptionOnError() { function testResponseReader_OptionExceptionOnError() {
$connection = new \Predis\Connection(RC::getConnectionParameters()); $connection = new \Predis\Connection(RC::getConnectionParameters());
$responseReader = $connection->getResponseReader(); $responseReader = $connection->getResponseReader();
$connection->rawCommand("SET key 5\r\nvalue\r\n"); $connection->rawCommand("*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n");
$rawCmdUnexpected = "LPUSH key 5\r\nvalue\r\n"; $rawCmdUnexpected = "*3\r\n$5\r\nLPUSH\r\n$3\r\nkey\r\n$5\r\nvalue\r\n";
$responseReader->setHandler( $responseReader->setHandler(
\Predis\Protocol::PREFIX_ERROR, \Predis\Protocol::PREFIX_ERROR,
@@ -471,5 +474,280 @@ class PredisClientFeaturesTestSuite extends PHPUnit_Framework_TestCase {
$this->assertEquals('bar', $replies[3][0]); $this->assertEquals('bar', $replies[3][0]);
$this->assertEquals('piyo', $replies[3][1]); $this->assertEquals('piyo', $replies[3][1]);
} }
/* Client + MultiExecBlock */
function testMultiExecBlock_Simple() {
$client = RC::getConnection();
$client->flushdb();
$multi = $client->multiExec();
$this->assertType('\Predis\MultiExecBlock', $multi);
$this->assertType('\Predis\MultiExecBlock', $multi->set('foo', 'bar'));
$this->assertType('\Predis\MultiExecBlock', $multi->set('hoge', 'piyo'));
$this->assertType('\Predis\MultiExecBlock', $multi->mset(array(
'foofoo' => 'barbar', 'hogehoge' => 'piyopiyo'
)));
$this->assertType('\Predis\MultiExecBlock', $multi->mget(array(
'foo', 'hoge', 'foofoo', 'hogehoge'
)));
$replies = $multi->execute();
$this->assertType('array', $replies);
$this->assertEquals(4, count($replies));
$this->assertEquals(4, count($replies[3]));
$this->assertEquals('barbar', $replies[3][2]);
}
function testMultiExecBlock_FluentInterface() {
$client = RC::getConnection();
$client->flushdb();
$replies = $client->multiExec()->ping()->set('foo', 'bar')->get('foo')->execute();
$this->assertType('array', $replies);
$this->assertEquals('bar', $replies[2]);
}
function testMultiExecBlock_CallableAnonymousBlock() {
$client = RC::getConnection();
$client->flushdb();
$replies = $client->multiExec(function($multi) {
$multi->ping();
$multi->set('foo', 'bar');
$multi->get('foo');
});
$this->assertType('array', $replies);
$this->assertEquals('bar', $replies[2]);
}
/**
* @expectedException Predis\ClientException
*/
function testMultiExecBlock_CannotMixFluentInterfaceAndAnonymousBlock() {
$emptyBlock = function($tx) { };
$tx = RC::getConnection()->multiExec()->get('foo')->execute($emptyBlock);
}
function testMultiExecBlock_EmptyCallableBlock() {
$client = RC::getConnection();
$client->flushdb();
$replies = $client->multiExec(function($multi) { });
$this->assertEquals(0, count($replies));
$options = array('cas' => true);
$replies = $client->multiExec($options, function($multi) { });
$this->assertEquals(0, count($replies));
$options = array('cas' => true);
$replies = $client->multiExec($options, function($multi) {
$multi->multi();
});
$this->assertEquals(0, count($replies));
}
function testMultiExecBlock_ClientExceptionInCallableBlock() {
$client = RC::getConnection();
$client->flushdb();
RC::testForClientException($this, 'TEST', function() use($client) {
$client->multiExec(function($multi) {
$multi->ping();
$multi->set('foo', 'bar');
throw new \Predis\ClientException("TEST");
});
});
$this->assertFalse($client->exists('foo'));
}
function testMultiExecBlock_ServerExceptionInCallableBlock() {
$client = RC::getConnection();
$client->flushdb();
$client->getResponseReader()->setHandler('-', new \Predis\ResponseErrorSilentHandler());
$replies = $client->multiExec(function($multi) {
$multi->set('foo', 'bar');
$multi->lpush('foo', 'piyo'); // LIST operation on STRING type returns an ERROR
$multi->set('hoge', 'piyo');
});
$this->assertType('array', $replies);
$this->assertType('\Predis\ResponseError', $replies[1]);
$this->assertTrue($client->exists('foo'));
$this->assertTrue($client->exists('hoge'));
}
function testMultiExecBlock_Discard() {
$client = RC::getConnection();
$client->flushdb();
$replies = $client->multiExec(function($multi) {
$multi->set('foo', 'bar');
$multi->discard();
$multi->set('hoge', 'piyo');
});
$this->assertEquals(1, count($replies));
$this->assertFalse($client->exists('foo'));
$this->assertTrue($client->exists('hoge'));
}
function testMultiExecBlock_DiscardEmpty() {
$client = RC::getConnection();
$client->flushdb();
$replies = $client->multiExec(function($multi) {
$multi->discard();
});
$this->assertEquals(0, count($replies));
}
function testMultiExecBlock_Watch() {
$client1 = RC::getConnection();
$client2 = RC::getConnection(true);
$client1->flushdb();
RC::testForAbortedMultiExecException($this, function()
use($client1, $client2) {
$client1->multiExec(array('watch' => 'sentinel'), function($multi)
use ($client2) {
$multi->set('sentinel', 'client1');
$multi->get('sentinel');
$client2->set('sentinel', 'client2');
});
});
$this->assertEquals('client2', $client1->get('sentinel'));
}
function testMultiExecBlock_CheckAndSet() {
$client = RC::getConnection();
$client->flushdb();
$client->set('foo', 'bar');
$options = array('watch' => 'foo', 'cas' => true);
$replies = $client->multiExec($options, function($tx) {
$tx->watch('foobar');
$foo = $tx->get('foo');
$tx->multi();
$tx->set('foobar', $foo);
$tx->mget('foo', 'foobar');
});
$this->assertType('array', $replies);
$this->assertEquals(array(true, array('bar', 'bar')), $replies);
$tx = $client->multiExec($options);
$tx->watch('foobar');
$foo = $tx->get('foo');
$replies = $tx->multi()
->set('foobar', $foo)
->mget('foo', 'foobar')
->execute();
$this->assertType('array', $replies);
$this->assertEquals(array(true, array('bar', 'bar')), $replies);
}
function testMultiExecBlock_RetryOnServerAbort() {
$client1 = RC::getConnection();
$client2 = RC::getConnection(true);
$client1->flushdb();
$retry = 3;
$attempts = 0;
RC::testForAbortedMultiExecException($this, function()
use($client1, $client2, $retry, &$attempts) {
$options = array('watch' => 'sentinel', 'retry' => $retry);
$client1->multiExec($options, function($tx)
use ($client2, &$attempts) {
$attempts++;
$tx->set('sentinel', 'client1');
$tx->get('sentinel');
$client2->set('sentinel', 'client2');
});
});
$this->assertEquals('client2', $client1->get('sentinel'));
$this->assertEquals($retry + 1, $attempts);
$retry = 3;
$attempts = 0;
RC::testForAbortedMultiExecException($this, function()
use($client1, $client2, $retry, &$attempts) {
$options = array(
'watch' => 'sentinel',
'cas' => true,
'retry' => $retry
);
$client1->multiExec($options, function($tx)
use ($client2, &$attempts) {
$attempts++;
$tx->incr('attempts');
$tx->multi();
$tx->set('sentinel', 'client1');
$tx->get('sentinel');
$client2->set('sentinel', 'client2');
});
});
$this->assertEquals('client2', $client1->get('sentinel'));
$this->assertEquals($retry + 1, $attempts);
$this->assertEquals($attempts, $client1->get('attempts'));
}
/**
* @expectedException InvalidArgumentException
*/
function testMultiExecBlock_RetryNotAvailableWithoutBlock() {
$options = array('watch' => 'foo', 'retry' => 1);
$tx = RC::getConnection()->multiExec($options);
$tx->multi()->get('foo')->exec();
}
function testMultiExecBlock_CheckAndSet_Discard() {
$client = RC::getConnection();
$client->flushdb();
$client->set('foo', 'bar');
$options = array('watch' => 'foo', 'cas' => true);
$replies = $client->multiExec($options, function($tx) {
$tx->watch('foobar');
$foo = $tx->get('foo');
$tx->multi();
$tx->set('foobar', $foo);
$tx->discard();
$tx->mget('foo', 'foobar');
});
$this->assertType('array', $replies);
$this->assertEquals(array(array('bar', null)), $replies);
$hijack = true;
$client->set('foo', 'bar');
$client2 = RC::getConnection(true);
$options = array('watch' => 'foo', 'cas' => true, 'retry' => 1);
$replies = $client->multiExec($options, function($tx)
use ($client2, &$hijack) {
$foo = $tx->get('foo');
$tx->multi();
$tx->set('foobar', $foo);
$tx->discard();
if ($hijack) {
$hijack = false;
$client2->set('foo', 'hijacked!');
}
$tx->mget('foo', 'foobar');
});
$this->assertType('array', $replies);
$this->assertEquals(array(array('hijacked!', null)), $replies);
}
} }
?> ?>
+18 -1
View File
@@ -20,11 +20,14 @@ class RC {
const EXCEPTION_WRONG_TYPE = 'Operation against a key holding the wrong kind of value'; const EXCEPTION_WRONG_TYPE = 'Operation against a key holding the wrong kind of value';
const EXCEPTION_NO_SUCH_KEY = 'no such key'; const EXCEPTION_NO_SUCH_KEY = 'no such key';
const EXCEPTION_OUT_OF_RANGE = 'index out of range'; const EXCEPTION_OUT_OF_RANGE = 'index out of range';
const EXCEPTION_OFFSET_RANGE = 'offset is out of range';
const EXCEPTION_INVALID_DB_IDX = 'invalid DB index'; const EXCEPTION_INVALID_DB_IDX = 'invalid DB index';
const EXCEPTION_VALUE_NOT_INT = 'value is not an integer'; const EXCEPTION_VALUE_NOT_INT = 'value is not an integer';
const EXCEPTION_EXEC_NO_MULTI = 'EXEC without MULTI'; const EXCEPTION_EXEC_NO_MULTI = 'EXEC without MULTI';
const EXCEPTION_SETEX_TTL = 'invalid expire time in SETEX'; const EXCEPTION_SETEX_TTL = 'invalid expire time in SETEX';
const EXCEPTION_HASH_VALNOTINT = 'hash value is not an integer'; const EXCEPTION_HASH_VALNOTINT = 'hash value is not an integer';
const EXCEPTION_BIT_VALUE = 'bit is not an integer or out of range';
const EXCEPTION_BIT_OFFSET = 'bit offset is not an integer or out of range';
private static $_connection; private static $_connection;
@@ -44,7 +47,10 @@ class RC {
return $connection; return $connection;
} }
public static function getConnection() { public static function getConnection($new = false) {
if ($new == true) {
return self::createConnection();
}
if (self::$_connection === null || !self::$_connection->isConnected()) { if (self::$_connection === null || !self::$_connection->isConnected()) {
self::$_connection = self::createConnection(); self::$_connection = self::createConnection();
} }
@@ -149,6 +155,17 @@ class RC {
} }
} }
public static function testForAbortedMultiExecException($testcaseInstance, $wrapFunction) {
$thrownException = null;
try {
$wrapFunction($testcaseInstance);
}
catch (Predis\AbortedMultiExec $exception) {
$thrownException = $exception;
}
$testcaseInstance->assertType('Predis\AbortedMultiExec', $thrownException);
}
public static function pushTailAndReturn(Predis\Client $client, $keyName, Array $values, $wipeOut = 0) { public static function pushTailAndReturn(Predis\Client $client, $keyName, Array $values, $wipeOut = 0) {
if ($wipeOut == true) { if ($wipeOut == true) {
$client->del($keyName); $client->del($keyName);
+267 -1
View File
@@ -223,6 +223,28 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
}); });
} }
function testSetRange() {
$this->assertEquals(6, $this->redis->setrange('var', 0, 'foobar'));
$this->assertEquals('foobar', $this->redis->get('var'));
$this->assertEquals(6, $this->redis->setrange('var', 3, 'foo'));
$this->assertEquals('foofoo', $this->redis->get('var'));
$this->assertEquals(16, $this->redis->setrange('var', 10, 'barbar'));
$this->assertEquals("foofoo\x00\x00\x00\x00barbar", $this->redis->get('var'));
$this->assertEquals(4, $this->redis->setrange('binary', 0, pack('l', -2147483648)));
list($unpacked) = array_values(unpack('l', $this->redis->get('binary')));
$this->assertEquals(-2147483648, $unpacked);
RC::testForServerException($this, RC::EXCEPTION_OFFSET_RANGE, function($test) {
$test->redis->setrange('var', -1, 'bogus');
});
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->rpush('metavars', 'foo');
$test->redis->setrange('metavars', 0, 'hoge');
});
}
function testSubstr() { function testSubstr() {
$this->redis->set('var', 'foobar'); $this->redis->set('var', 'foobar');
$this->assertEquals('foo', $this->redis->substr('var', 0, 2)); $this->assertEquals('foo', $this->redis->substr('var', 0, 2));
@@ -240,6 +262,74 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
}); });
} }
function testStrlen() {
$this->redis->set('var', 'foobar');
$this->assertEquals(6, $this->redis->strlen('var'));
$this->assertEquals(9, $this->redis->append('var', '___'));
$this->assertEquals(9, $this->redis->strlen('var'));
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->rpush('metavars', 'foo');
$test->redis->strlen('metavars');
});
}
function testSetBit() {
$this->assertEquals(0, $this->redis->setbit('binary', 31, 1));
$this->assertEquals(0, $this->redis->setbit('binary', 0, 1));
$this->assertEquals(4, $this->redis->strlen('binary'));
$this->assertEquals("\x80\x00\00\x01", $this->redis->get('binary'));
$this->assertEquals(1, $this->redis->setbit('binary', 0, 0));
$this->assertEquals(0, $this->redis->setbit('binary', 0, 0));
$this->assertEquals("\x00\x00\00\x01", $this->redis->get('binary'));
RC::testForServerException($this, RC::EXCEPTION_BIT_OFFSET, function($test) {
$test->redis->setbit('binary', -1, 1);
});
RC::testForServerException($this, RC::EXCEPTION_BIT_OFFSET, function($test) {
$test->redis->setbit('binary', 'invalid', 1);
});
RC::testForServerException($this, RC::EXCEPTION_BIT_VALUE, function($test) {
$test->redis->setbit('binary', 15, 255);
});
RC::testForServerException($this, RC::EXCEPTION_BIT_VALUE, function($test) {
$test->redis->setbit('binary', 15, 'invalid');
});
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->rpush('metavars', 'foo');
$test->redis->setbit('metavars', 0, 1);
});
}
function testGetBit() {
$this->redis->set('binary', "\x80\x00\00\x01");
$this->assertEquals(1, $this->redis->getbit('binary', 0));
$this->assertEquals(0, $this->redis->getbit('binary', 15));
$this->assertEquals(1, $this->redis->getbit('binary', 31));
$this->assertEquals(0, $this->redis->getbit('binary', 63));
RC::testForServerException($this, RC::EXCEPTION_BIT_OFFSET, function($test) {
$test->redis->getbit('binary', -1);
});
RC::testForServerException($this, RC::EXCEPTION_BIT_OFFSET, function($test) {
$test->redis->getbit('binary', 'invalid');
});
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->rpush('metavars', 'foo');
$test->redis->getbit('metavars', 0);
});
}
/* commands operating on the key space */ /* commands operating on the key space */
function testKeys() { function testKeys() {
@@ -324,6 +414,18 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
$this->assertEquals(-1, $this->redis->ttl('foo')); $this->assertEquals(-1, $this->redis->ttl('foo'));
} }
function testPersist() {
$this->redis->set('foo', 'bar');
$this->assertTrue($this->redis->expire('foo', 1));
$this->assertEquals(1, $this->redis->ttl('foo'));
$this->assertTrue($this->redis->persist('foo'));
$this->assertEquals(-1, $this->redis->ttl('foo'));
$this->assertFalse($this->redis->persist('foo'));
$this->assertFalse($this->redis->persist('foobar'));
}
function testSetExpire() { function testSetExpire() {
$this->assertTrue($this->redis->setex('foo', 10, 'bar')); $this->assertTrue($this->redis->setex('foo', 10, 'bar'));
$this->assertTrue($this->redis->exists('foo')); $this->assertTrue($this->redis->exists('foo'));
@@ -333,7 +435,8 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
sleep(2); sleep(2);
$this->assertFalse($this->redis->exists('hoge')); $this->assertFalse($this->redis->exists('hoge'));
RC::testForServerException($this, RC::EXCEPTION_VALUE_NOT_INT, function($test) { // TODO: do not check the error message RC::EXCEPTION_VALUE_NOT_INT for now
RC::testForServerException($this, null, function($test) {
$test->redis->setex('hoge', 2.5, 'piyo'); $test->redis->setex('hoge', 2.5, 'piyo');
}); });
RC::testForServerException($this, RC::EXCEPTION_SETEX_TTL, function($test) { RC::testForServerException($this, RC::EXCEPTION_SETEX_TTL, function($test) {
@@ -367,6 +470,20 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
}); });
} }
function testPushTailX() {
$this->assertEquals(0, $this->redis->rpushx('numbers', 1));
$this->assertEquals(1, $this->redis->rpush('numbers', 2));
$this->assertEquals(2, $this->redis->rpushx('numbers', 3));
$this->assertEquals(2, $this->redis->llen('numbers'));
$this->assertEquals(array(2, 3), $this->redis->lrange('numbers', 0, -1));
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar');
$test->redis->rpushx('foo', 'bar');
});
}
function testPushHead() { function testPushHead() {
// NOTE: List push operations return the list length since Redis commit 520b5a3 // NOTE: List push operations return the list length since Redis commit 520b5a3
$this->assertEquals(1, $this->redis->lpush('metavars', 'foo')); $this->assertEquals(1, $this->redis->lpush('metavars', 'foo'));
@@ -380,6 +497,20 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
}); });
} }
function testPushHeadX() {
$this->assertEquals(0, $this->redis->lpushx('numbers', 1));
$this->assertEquals(1, $this->redis->lpush('numbers', 2));
$this->assertEquals(2, $this->redis->lpushx('numbers', 3));
$this->assertEquals(2, $this->redis->llen('numbers'));
$this->assertEquals(array(3, 2), $this->redis->lrange('numbers', 0, -1));
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar');
$test->redis->lpushx('foo', 'bar');
});
}
function testListLength() { function testListLength() {
$this->assertEquals(1, $this->redis->rpush('metavars', 'foo')); $this->assertEquals(1, $this->redis->rpush('metavars', 'foo'));
$this->assertEquals(2, $this->redis->rpush('metavars', 'hoge')); $this->assertEquals(2, $this->redis->rpush('metavars', 'hoge'));
@@ -680,6 +811,47 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
$this->assertEquals((float)(time() - $start), 2, '', 1); $this->assertEquals((float)(time() - $start), 2, '', 1);
} }
function testListBlockingPopLastPushHead() {
// TODO: this test does not cover all the aspects of BLPOP/BRPOP as it
// does not run with a concurrent client pushing items on lists.
$numbers = RC::pushTailAndReturn($this->redis, 'numbers', array(1, 2, 3));
$src_count = count($numbers);
$dst_count = 0;
while ($item = $this->redis->brpoplpush('numbers', 'temporary', 1)) {
$this->assertEquals(--$src_count, $this->redis->llen('numbers'));
$this->assertEquals(++$dst_count, $this->redis->llen('temporary'));
$this->assertEquals(array_pop($numbers), $this->redis->lindex('temporary', 0));
}
$start = time();
$this->assertNull($this->redis->brpoplpush('numbers', 'temporary', 2));
$this->assertEquals(2, (float)(time() - $start), '', 1);
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->del('numbers');
$test->redis->del('temporary');
$test->redis->set('numbers', 'foobar');
$test->redis->brpoplpush('numbers', 'temporary', 1);
});
}
function testListInsert() {
$numbers = RC::pushTailAndReturn($this->redis, 'numbers', RC::getArrayOfNumbers());
$this->assertEquals(11, $this->redis->linsert('numbers', 'before', 0, -2));
$this->assertEquals(12, $this->redis->linsert('numbers', 'after', -2, -1));
$this->assertEquals(array(-2, -1, 0, 1), $this->redis->lrange('numbers', 0, 3));
$this->assertEquals(-1, $this->redis->linsert('numbers', 'after', 100, 200));
$this->assertEquals(-1, $this->redis->linsert('numbers', 'before', 100, 50));
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar');
$test->redis->linsert('foo', 'before', 0, 0);
});
}
/* commands operating on sets */ /* commands operating on sets */
@@ -1095,6 +1267,11 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
$this->redis->zrange('zset', 0, 2, 'withscores') $this->redis->zrange('zset', 0, 2, 'withscores')
); );
$this->assertEquals(
array(array('a', -10), array('b', 0), array('c', 10)),
$this->redis->zrange('zset', 0, 2, array('withscores' => true))
);
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) { RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar'); $test->redis->set('foo', 'bar');
$test->redis->zrange('foo', 0, -1); $test->redis->zrange('foo', 0, -1);
@@ -1149,6 +1326,11 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
$this->redis->zrevrange('zset', 0, 2, 'withscores') $this->redis->zrevrange('zset', 0, 2, 'withscores')
); );
$this->assertEquals(
array(array('f', 30), array('e', 20), array('d', 20)),
$this->redis->zrevrange('zset', 0, 2, array('withscores' => true))
);
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) { RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar'); $test->redis->set('foo', 'bar');
$test->redis->zrevrange('foo', 0, -1); $test->redis->zrevrange('foo', 0, -1);
@@ -1183,12 +1365,96 @@ class RedisCommandTestSuite extends PHPUnit_Framework_TestCase {
$this->redis->zrangebyscore('zset', 10, 20, 'withscores') $this->redis->zrangebyscore('zset', 10, 20, 'withscores')
); );
$this->assertEquals(
array(array('c', 10), array('d', 20), array('e', 20)),
$this->redis->zrangebyscore('zset', 10, 20, array('withscores' => true))
);
$this->assertEquals(
array('d', 'e'),
$this->redis->zrangebyscore('zset', 10, 20, array('limit' => array(1, 2)))
);
$this->assertEquals(
array('d', 'e'),
$this->redis->zrangebyscore('zset', 10, 20, array(
'limit' => array('offset' => 1, 'count' => 2)
))
);
$this->assertEquals(
array(array('d', 20), array('e', 20)),
$this->redis->zrangebyscore('zset', 10, 20, array(
'limit' => array(1, 2),
'withscores' => true,
))
);
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) { RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar'); $test->redis->set('foo', 'bar');
$test->redis->zrangebyscore('foo', 0, 0); $test->redis->zrangebyscore('foo', 0, 0);
}); });
} }
function testZsetReverseRangeByScore() {
$zset = RC::zsetAddAndReturn($this->redis, 'zset', RC::getZSetArray());
$this->assertEquals(
array('a'),
$this->redis->zrevrangebyscore('zset', -10, -10)
);
$this->assertEquals(
array('b', 'a'),
$this->redis->zrevrangebyscore('zset', 0, -10)
);
$this->assertEquals(
array('e', 'd'),
$this->redis->zrevrangebyscore('zset', 20, 20)
);
$this->assertEquals(
array('f', 'e', 'd', 'c', 'b'),
$this->redis->zrevrangebyscore('zset', 30, 0)
);
$this->assertEquals(
array(array('e', 20), array('d', 20), array('c', 10)),
$this->redis->zrevrangebyscore('zset', 20, 10, 'withscores')
);
$this->assertEquals(
array(array('e', 20), array('d', 20), array('c', 10)),
$this->redis->zrevrangebyscore('zset', 20, 10, array('withscores' => true))
);
$this->assertEquals(
array('d', 'c'),
$this->redis->zrevrangebyscore('zset', 20, 10, array('limit' => array(1, 2)))
);
$this->assertEquals(
array('d', 'c'),
$this->redis->zrevrangebyscore('zset', 20, 10, array(
'limit' => array('offset' => 1, 'count' => 2)
))
);
$this->assertEquals(
array(array('d', 20), array('c', 10)),
$this->redis->zrevrangebyscore('zset', 20, 10, array(
'limit' => array(1, 2),
'withscores' => true,
))
);
RC::testForServerException($this, RC::EXCEPTION_WRONG_TYPE, function($test) {
$test->redis->set('foo', 'bar');
$test->redis->zrevrangebyscore('foo', 0, 0);
});
}
function testZsetUnionStore() { function testZsetUnionStore() {
$zsetA = RC::zsetAddAndReturn($this->redis, 'zseta', array('a' => 1, 'b' => 2, 'c' => 3)); $zsetA = RC::zsetAddAndReturn($this->redis, 'zseta', array('a' => 1, 'b' => 2, 'c' => 3));
$zsetB = RC::zsetAddAndReturn($this->redis, 'zsetb', array('b' => 1, 'c' => 2, 'd' => 3)); $zsetB = RC::zsetAddAndReturn($this->redis, 'zsetb', array('b' => 1, 'c' => 2, 'd' => 3));