From 9c12c40a66cbd76f5efa053b1718fa936320400b Mon Sep 17 00:00:00 2001 From: vostok4 Date: Wed, 9 Apr 2014 11:14:45 +0200 Subject: Merge nicolasff:b9a16b5ad5 in, fixing for Win32 Now we should be up to master with upstream for an easier merge. --- CREDITS | 1 + README.markdown | 177 ++++++++++++++- common.h | 28 +++ library.c | 54 ++++- library.h | 2 + package.xml | 92 +++++++- php_redis.h | 14 +- redis.c | 609 ++++++++++++++++++++++++++++++++++++++++++++++++---- redis_array.c | 72 ++++--- redis_array.h | 1 + redis_array_impl.c | 40 +++- redis_array_impl.h | 2 +- rpm/php-redis.spec | 2 +- tests/TestRedis.php | 344 +++++++++++++++++++++++++++++ 14 files changed, 1352 insertions(+), 86 deletions(-) diff --git a/CREDITS b/CREDITS index c501060a..53010079 100644 --- a/CREDITS +++ b/CREDITS @@ -2,3 +2,4 @@ Redis client extension for PHP Alfonso Jimenez (yo@alfonsojimenez.com) Nasreddine Bouafif (n.bouafif@owlient.eu) Nicolas Favre-Felix (n.favre-felix@owlient.eu) +Michael Grunder (michael.grunder@gmail.com) \ No newline at end of file diff --git a/README.markdown b/README.markdown index 7d5d2dc8..91d09f19 100644 --- a/README.markdown +++ b/README.markdown @@ -3,7 +3,7 @@ The phpredis extension provides an API for communicating with the [Redis](http://redis.io/) key-value store. It is released under the [PHP License, version 3.01](http://www.php.net/license/3_01.txt). This code has been developed and maintained by Owlient from November 2009 to March 2011. -You can send comments, patches, questions [here on github](https://github.com/nicolasff/phpredis/issues) or to n.favrefelix@gmail.com ([@yowgi](http://twitter.com/yowgi)). +You can send comments, patches, questions [here on github](https://github.com/nicolasff/phpredis/issues), to n.favrefelix@gmail.com ([@yowgi](http://twitter.com/yowgi)), or to michael.grunder@gmail.com ([@grumi78](http://twitter.com/grumi78)). # Table of contents @@ -69,6 +69,10 @@ Taken from [Compiling phpredis on Zend Server CE/OSX ](http://www.tumblr.com/tag See also: [Install Redis & PHP Extension PHPRedis with Macports](http://www.lecloud.net/post/3378834922/install-redis-php-extension-phpredis-with-macports). +You can install install it using Homebrew: + +- [Get homebrew-php](https://github.com/josegonzalez/homebrew-php) +- `brew install php55-redis` (or php53-redis, php54-redis) ## PHP Session handler @@ -268,6 +272,15 @@ $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); // use built-in $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); // use igBinary serialize/unserialize $redis->setOption(Redis::OPT_PREFIX, 'myAppName:'); // use custom prefix on all keys + +/* Options for the SCAN family of commands, indicating whether to abstract + empty results from the user. If set to SCAN_NORETRY (the default), phpredis + will just issue one SCAN command at a time, sometimes returning an empty + array of results. If set to SCAN_RETRY, phpredis will retry the scan command + until keys come back OR Redis returns an iterator of zero +*/ +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NORETRY); +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); ~~~ @@ -607,6 +620,7 @@ $redis->slowlog('len'); * [expire, setTimeout, pexpire](#expire-settimeout-pexpire) - Set a key's time to live in seconds * [expireAt, pexpireAt](#expireat-pexpireat) - Set the expiration for a key as a UNIX timestamp * [keys, getKeys](#keys-getkeys) - Find all keys matching the given pattern +* [scan](#scan) - Scan for keys in the keyspace (Redis >= 2.8.0) * [migrate](#migrate) - Atomically transfer a key from a Redis instance to another one * [move](#move) - Move a key to another database * [object](#object) - Inspect the internals of Redis objects @@ -658,10 +672,10 @@ $redis->set('key', 'value'); $redis->set('key','value', 10); // Will set the key, if it doesn't exist, with a ttl of 10 seconds -$redis->set('key', 'value', Array('nx', 'ex'=>10); +$redis->set('key', 'value', Array('nx', 'ex'=>10)); // Will set a key, if it does exist, with a ttl of 1000 miliseconds -$redis->set('key', 'value', Array('xx', 'px'=>1000); +$redis->set('key', 'value', Array('xx', 'px'=>1000)); ~~~ @@ -780,7 +794,7 @@ $redis->incrByFloat('key1', 1.5); /* key1 didn't exist, so it will now be 1.5 */ $redis->incrByFloat('key1', 1.5); /* 3 */ $redis->incrByFloat('key1', -1.5); /* 1.5 */ -$redis->incrByFloat('key1', 2.5); /* 3.5 */ +$redis->incrByFloat('key1', 2.5); /* 4 */ ~~~ ### decr, decrBy @@ -953,7 +967,29 @@ $allKeys = $redis->keys('*'); // all keys will match this. $keyWithUserPrefix = $redis->keys('user*'); ~~~ +### scan +----- +_**Description**_: Scan the keyspace for keys + +##### *Parameters* +*LONG (reference)*: Iterator, initialized to NULL +*STRING, Optional*: Pattern to match +*LONG, Optional*: Count of keys per iteration (only a suggestion to Redis) +##### *Return value* +*Array, boolean*: This function will return an array of keys or FALSE if there are no more keys + +##### *Example* +~~~ +$it = NULL; /* Initialize our iterator to NULL */ +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); /* retry when we get no keys back */ +while($arr_keys = $redis->scan($it)) { + foreach($arr_keys as $str_key) { + echo "Here is a key: $str_key\n"; + } + echo "No more keys to scan!\n"; +} +~~~ ### object ----- @@ -1261,9 +1297,13 @@ _**Description**_: Migrates a key to a different Redis instance. *key* string. The key to migrate. *destination-db* integer. The target DB. *timeout* integer. The maximum amount of time given to this transfer. +*copy* boolean, optional. Should we send the COPY flag to redis +*replace* boolean, optional. Should we send the REPLACE flag to redis ##### *Examples* ~~~ $redis->migrate('backup', 6379, 'foo', 0, 3600); +$redis->migrate('backup', 6379, 'foo', 0, 3600, true, true); /* copy and replace */ +$redis->migrate('backup', 6379, 'foo', 0, 3600, false, true); /* just REPLACE flag */ ~~~ @@ -1283,6 +1323,7 @@ $redis->migrate('backup', 6379, 'foo', 0, 3600); * [hSet](#hset) - Set the string value of a hash field * [hSetNx](#hsetnx) - Set the value of a hash field, only if the field does not exist * [hVals](#hvals) - Get all the values in a hash +* [hScan](#hscan) - Scan a hash key for members ### hSet ----- @@ -1542,7 +1583,28 @@ $redis->hSet('h', 'field2', 'value2'); $redis->hmGet('h', array('field1', 'field2')); /* returns array('field1' => 'value1', 'field2' => 'value2') */ ~~~ +### hScan +----- +_**Description**_: Scan a HASH value for members, with an optional pattern and count +##### *Parameters* +*key*: String +*iterator*: Long (reference) +*pattern*: Optional pattern to match against +*count*: How many keys to return in a go (only a sugestion to Redis) +##### *Return value* +*Array* An array of members that match our pattern +##### *Examples* +~~~ +$it = NULL; +/* Don't ever return an empty array until we're done iterating */ +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); +while($arr_keys = $redis->hscan('hash', $it)) { + foreach($arr_keys as $str_field => $str_value) { + echo "$str_field => $str_value\n"; /* Print the hash member and value */ + } +} +~~~ ## Lists @@ -1981,6 +2043,7 @@ $redis->lSize('key1');/* 2 */ * [sRem, sRemove](#srem-sremove) - Remove one or more members from a set * [sUnion](#sunion) - Add multiple sets * [sUnionStore](#sunionstore) - Add multiple sets and store the resulting set in a key +* [sScan](#sscan) - Scan a set for members ### sAdd ----- @@ -2380,6 +2443,41 @@ array(4) { } ~~~ +### sScan +----- +_**Description**_: Scan a set for members + +##### *Parameters* +*Key*: The set to search +*iterator*: LONG (reference) to the iterator as we go +*pattern*: String, optional pattern to match against +*count*: How many members to return at a time (Redis might return a different amount) + +##### *Return value* +*Array, boolean*: PHPRedis will return an array of keys or FALSE when we're done iterating + +##### *Example* +~~~ +$it = NULL; +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); /* don't return empty results until we're done */ +while($arr_mems = $redis->sscan('set', $it, "*pattern*")) { + foreach($arr_mems as $str_mem) { + echo "Member: $str_mem\n"; + } +} + +$it = NULL; +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NORETRY); /* return after each iteration, even if empty */ +while(($arr_mems = $redis->sscan('set', $it, "*pattern*"))!==FALSE) { + if(count($arr_mems) > 0) { + foreach($arr_mems as $str_mem) { + echo "Member found: $str_mem\n"; + } + } else { + echo "No members in this iteration, iterator value: $it\n"; + } +} +~~~ ## Sorted sets @@ -2397,6 +2495,7 @@ array(4) { * [zRevRange](#zrevrange) - Return a range of members in a sorted set, by index, with scores ordered from high to low * [zScore](#zscore) - Get the score associated with the given member in a sorted set * [zUnion](#zunion) - Add multiple sorted sets and store the resulting sorted set in a new key +* [zScan](#zscan) - Scan a sorted set for members ### zAdd ----- @@ -2736,11 +2835,36 @@ $redis->zUnion('ko2', array('k1', 'k2'), array(1, 1)); /* 4, 'ko2' => array('val $redis->zUnion('ko3', array('k1', 'k2'), array(5, 1)); /* 4, 'ko3' => array('val0', 'val2', 'val3', 'val1') */ ~~~ +### zScan +----- +_**Description**_: Scan a sorted set for members, with optional pattern and count + +##### *Parameters* +*key*: String, the set to scan +*iterator*: Long (reference), initialized to NULL +*pattern*: String (optional), the pattern to match +*count*: How many keys to return per iteration (Redis might return a different number) + +##### *Return value* +*Array, boolean* PHPRedis will return matching keys from Redis, or FALSE when iteration is complete + +##### *Example* +~~~ +$it = NULL; +$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); +while($arr_matches = $redis->zscan('zset', $it, '*pattern*')) { + foreach($arr_matches as $str_mem => $f_score) { + echo "Key: $str_mem, Score: $f_score\n"; + } +} +~~~ + ## Pub/sub * [psubscribe](#psubscribe) - Subscribe to channels by pattern * [publish](#publish) - Post a message to a channel * [subscribe](#subscribe) - Subscribe to channels +* [pubsub](#pubsub) - Introspection into the pub/sub subsystem ### psubscribe ----- @@ -2801,6 +2925,26 @@ function f($redis, $chan, $msg) { $redis->subscribe(array('chan-1', 'chan-2', 'chan-3'), 'f'); // subscribe to 3 chans ~~~ +### pubsub +----- +_**Description**_: A command allowing you to get information on the Redis pub/sub system. + +##### *Parameters* +*keyword*: String, which can be: "channels", "numsub", or "numpat" +*argument*: Optional, variant. For the "channels" subcommand, you can pass a string pattern. For "numsub" an array of channel names. + +##### *Return value* +*CHANNELS*: Returns an array where the members are the matching channels. +*NUMSUB*: Returns a key/value array where the keys are channel names and values are their counts. +*NUMPAT*: Integer return containing the number active pattern subscriptions + +##### *Example* +~~~ +$redis->pubsub("channels"); /*All channels */ +$redis->pubsub("channels", "*pattern*"); /* Just channels matching your pattern */ +$redis->pubsub("numsub", Array("chan1", "chan2")); /*Get subscriber counts for 'chan1' and 'chan2'*/ +$redsi->pubsub("numpat"); /* Get the number of pattern subscribers */ +``` ## Transactions @@ -2867,6 +3011,7 @@ $ret = FALSE if x has been modified between the call to WATCH and the call to EX * [clearLastError](#) - Clear the last error message * [_prefix](#) - A utility method to prefix the value with the prefix setting for phpredis * [_unserialize](#) - A utility method to unserialize data with whatever serializer is set up +* [_serialize](#) - A utility method to serialize data with whatever serializer is set up ### eval ----- @@ -3016,6 +3161,28 @@ $redis->setOption(Redis::OPT_PREFIX, 'my-prefix:'); $redis->_prefix('my-value'); // Will return 'my-prefix:my-value' ~~~ +### _serialize +----- +_**Description**_: A utility method to serialize values manually. + +This method allows you to serialize a value with whatever serializer is configured, manually. +This can be useful for serialization/unserialization of data going in and out of EVAL commands +as phpredis can't automatically do this itself. Note that if no serializer is set, phpredis +will change Array values to 'Array', and Objects to 'Object'. + +##### *Parameters* +*value*: Mixed. The value to be serialized + +##### *Examples* +~~~ +$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); +$redis->_serialize("foo"); // returns "foo" +$redis->_serialize(Array()); // Returns "Array" +$redis->_serialize(new stdClass()); // Returns "Object" + +$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); +$redis->_serialize("foo"); // Returns 's:3:"foo";' + ### _unserialize ----- _**Description**_: A utility method to unserialize data with whatever serializer is set up. @@ -3080,7 +3247,7 @@ None ### GetTimeout ----- -_**Description**_: Get the (write) timeout in use for phpreids +_**Description**_: Get the (write) timeout in use for phpredis ##### *Parameters* None diff --git a/common.h b/common.h index 63b1d9ee..a0623fbd 100644 --- a/common.h +++ b/common.h @@ -37,22 +37,48 @@ typedef enum _REDIS_REPLY_TYPE { TYPE_MULTIBULK = '*' } REDIS_REPLY_TYPE; +/* SCAN variants */ +typedef enum _REDIS_SCAN_TYPE { + TYPE_SCAN, + TYPE_SSCAN, + TYPE_HSCAN, + TYPE_ZSCAN +} REDIS_SCAN_TYPE; + +/* PUBSUB subcommands */ +typedef enum _PUBSUB_TYPE { + PUBSUB_CHANNELS, + PUBSUB_NUMSUB, + PUBSUB_NUMPAT +} PUBSUB_TYPE; + /* options */ #define REDIS_OPT_SERIALIZER 1 #define REDIS_OPT_PREFIX 2 #define REDIS_OPT_READ_TIMEOUT 3 +#define REDIS_OPT_SCAN 4 /* serializers */ #define REDIS_SERIALIZER_NONE 0 #define REDIS_SERIALIZER_PHP 1 #define REDIS_SERIALIZER_IGBINARY 2 +/* SCAN options */ + +#define REDIS_SCAN_NORETRY 0 +#define REDIS_SCAN_RETRY 1 + +/* GETBIT/SETBIT offset range limits */ +#define BITOP_MIN_OFFSET 0 +#define BITOP_MAX_OFFSET 4294967295 + #define IF_MULTI() if(redis_sock->mode == MULTI) #define IF_MULTI_OR_ATOMIC() if(redis_sock->mode == MULTI || redis_sock->mode == ATOMIC)\ #define IF_MULTI_OR_PIPELINE() if(redis_sock->mode == MULTI || redis_sock->mode == PIPELINE) #define IF_PIPELINE() if(redis_sock->mode == PIPELINE) #define IF_NOT_MULTI() if(redis_sock->mode != MULTI) +#define IF_NOT_ATOMIC() if(redis_sock->mode != ATOMIC) #define IF_ATOMIC() if(redis_sock->mode == ATOMIC) #define ELSE_IF_MULTI() else if(redis_sock->mode == MULTI) { \ if(redis_response_enqueued(redis_sock TSRMLS_CC) == 1) {\ @@ -197,6 +223,8 @@ typedef struct { char *err; int err_len; zend_bool lazy_connect; + + int scan; } RedisSock; /* }}} */ diff --git a/library.c b/library.c index 4fd523de..c368d3a3 100644 --- a/library.c +++ b/library.c @@ -106,6 +106,54 @@ PHP_REDIS_API int redis_check_eof(RedisSock *redis_sock TSRMLS_DC) return 0; } + +PHP_REDIS_API int +redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + REDIS_SCAN_TYPE type, long *iter) +{ + REDIS_REPLY_TYPE reply_type; + int reply_info; + char *p_iter; + + /* Our response should have two multibulk replies */ + if(redis_read_reply_type(redis_sock, &reply_type, &reply_info TSRMLS_CC)<0 + || reply_type != TYPE_MULTIBULK || reply_info != 2) + { + return -1; + } + + /* The BULK response iterator */ + if(redis_read_reply_type(redis_sock, &reply_type, &reply_info TSRMLS_CC)<0 + || reply_type != TYPE_BULK) + { + return -1; + } + + /* Attempt to read the iterator */ + if(!(p_iter = redis_sock_read_bulk_reply(redis_sock, reply_info TSRMLS_CC))) { + return -1; + } + + /* Push the iterator out to the caller */ + *iter = atol(p_iter); + efree(p_iter); + + /* Read our actual keys/members/etc differently depending on what kind of + scan command this is. They all come back in slightly different ways */ + switch(type) { + case TYPE_SCAN: + return redis_sock_read_multibulk_reply_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); + case TYPE_SSCAN: + return redis_sock_read_multibulk_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); + case TYPE_ZSCAN: + return redis_sock_read_multibulk_reply_zipped(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); + case TYPE_HSCAN: + return redis_sock_read_multibulk_reply_zipped_strings(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); + default: + return -1; + } +} + PHP_REDIS_API zval *redis_sock_read_multibulk_reply_zval(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock) { char inbuf[1024]; int numElems; @@ -1074,6 +1122,8 @@ PHP_REDIS_API RedisSock* redis_sock_create(char *host, int host_len, unsigned sh redis_sock->err = NULL; redis_sock->err_len = 0; + redis_sock->scan = REDIS_SCAN_NORETRY; + return redis_sock; } @@ -1498,8 +1548,10 @@ redis_serialize(RedisSock *redis_sock, zval *z, char **val, int *val_len TSRMLS_ #endif smart_str sstr = {0}; zval *z_copy; +#ifdef HAVE_REDIS_IGBINARY size_t sz; uint8_t *val8; +#endif switch(redis_sock->serializer) { case REDIS_SERIALIZER_NONE: @@ -1834,7 +1886,7 @@ redis_read_variant_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zv default: /* Protocol error */ zend_throw_exception_ex(redis_exception_ce, 0 TSRMLS_CC, "protocol error, got '%c' as reply-type byte\n", reply_type); - break; + return FAILURE; } IF_MULTI_OR_PIPELINE() { diff --git a/library.h b/library.h index b749cf63..4813b126 100644 --- a/library.h +++ b/library.h @@ -35,6 +35,8 @@ PHP_REDIS_API int redis_sock_read_multibulk_reply_loop(INTERNAL_FUNCTION_PARAMET PHP_REDIS_API int redis_sock_read_multibulk_reply_zipped(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); PHP_REDIS_API int redis_sock_read_multibulk_reply_zipped_strings(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); PHP_REDIS_API int redis_sock_read_multibulk_reply_assoc(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); +PHP_REDIS_API int redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, REDIS_SCAN_TYPE type, long *iter); + PHP_REDIS_API int redis_sock_write(RedisSock *redis_sock, char *cmd, size_t sz TSRMLS_DC); PHP_REDIS_API void redis_stream_close(RedisSock *redis_sock TSRMLS_DC); PHP_REDIS_API int redis_check_eof(RedisSock *redis_sock TSRMLS_DC); diff --git a/package.xml b/package.xml index dd97ad77..f3b82d4f 100644 --- a/package.xml +++ b/package.xml @@ -21,10 +21,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> michael.grunder@gmail.com yes - 2013-09-01 + 2014-03-15 - 2.2.4 - 2.2.4 + 2.2.5 + 2.2.5 stable @@ -32,7 +32,25 @@ http://pear.php.net/dtd/package-2.0.xsd"> PHP - First public release + phpredis 2.2.5 + + This is a minor release with several bug fixes as well as additions to support + new commands that have been introduced to Redis since our last release. + + A special thanks to everyone who helps the project by commenting on issues and + submitting pull requests! :) + + [NEW] Support for the BITPOS command + [NEW] Connection timeout option for RedisArray (@MikeToString) + [NEW] A _serialize method, to complement our existing _unserialize method + [NEW] Support for the PUBSUB command + [NEW] Support for SCAN, SSCAN, HSCAN, and ZSCAN + [NEW] Support for the WAIT command + + [FIX] Handle the COPY and REPLACE arguments for the MIGRATE command + + [DOC] Fix syntax error in documentation for the SET command (@mithunsatheesh) + [DOC] Homebrew documentation instructions (@mathias) @@ -53,6 +71,13 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + @@ -70,12 +95,69 @@ http://pear.php.net/dtd/package-2.0.xsd"> redis + + stablestable + 2.2.52.2.5 + 2014-03-15 + + phpredis 2.2.5 + + This is a minor release with several bug fixes as well as additions to support + new commands that have been introduced to Redis since our last release. + + A special thanks to everyone who helps the project by commenting on issues and + submitting pull requests! :) + + [NEW] Support for the BITPOS command + [NEW] Connection timeout option for RedisArray (@MikeToString) + [NEW] A _serialize method, to complement our existing _unserialize method + [NEW] Support for the PUBSUB command + [NEW] Support for SCAN, SSCAN, HSCAN, and ZSCAN + [NEW] Support for the WAIT command + + [FIX] Handle the COPY and REPLACE arguments for the MIGRATE command + + [DOC] Fix syntax error in documentation for the SET command (@mithunsatheesh) + [DOC] Homebrew documentation instructions (@mathias) + + + stablestable 2.2.42.2.4 2013-09-01 - See GitHub for release notes + ** + ** Features / Improvements + ** + + * Randomized reconnect delay for RedisArray @mobli + This feature adds an optional parameter when constructing a RedisArray object + such that a random delay will be introduced if reconnections are made, + mitigating any 'thundering herd' type problems. + + * Lazy connections to RedisArray servers @mobli + By default, RedisArray will attempt to connect to each server you pass in + the ring on construction. This feature lets you specify that you would + rather have RedisArray only attempt a connection when it needs to get data + from a particular node (throughput/performance improvement). + + * Allow LONG and STRING keys in MGET/MSET + * Extended SET options for Redis >= 2.6.12 + * Persistent connections and UNIX SOCKET support for RedisArray + * Allow aggregates for ZUNION/ZINTER without weights @mheijkoop + * Support for SLOWLOG command + * Reworked MGET algorithm to run in linear time regardless of key count. + * Reworked ZINTERSTORE/ZUNIONSTORE algorithm to run in linear time + + ** + ** Bug fixes + ** + + * C99 Compliance (or rather lack thereof) fix @mobli + * Added ZEND_ACC_CTOR and ZEND_ACC_DTOR @euskadi31 + * Stop throwing and clearing an exception on connect failure @matmoi + * Fix a false positive unit test failure having to do with TTL returns diff --git a/php_redis.h b/php_redis.h index 905c7b3b..ccc2e724 100644 --- a/php_redis.h +++ b/php_redis.h @@ -14,6 +14,7 @@ +----------------------------------------------------------------------+ | Original author: Alfonso Jimenez | | Maintainer: Nicolas Favre-Felix | + | Maintainer: Michael Grunder | | Maintainer: Nasreddine Bouafif | +----------------------------------------------------------------------+ */ @@ -128,6 +129,7 @@ PHP_METHOD(Redis, slaveof); PHP_METHOD(Redis, object); PHP_METHOD(Redis, bitop); PHP_METHOD(Redis, bitcount); +PHP_METHOD(Redis, bitpos); PHP_METHOD(Redis, eval); PHP_METHOD(Redis, evalsha); @@ -141,6 +143,7 @@ PHP_METHOD(Redis, time); PHP_METHOD(Redis, getLastError); PHP_METHOD(Redis, clearLastError); PHP_METHOD(Redis, _prefix); +PHP_METHOD(Redis, _serialize); PHP_METHOD(Redis, _unserialize); PHP_METHOD(Redis, mset); @@ -181,9 +184,18 @@ PHP_METHOD(Redis, setOption); PHP_METHOD(Redis, config); PHP_METHOD(Redis, slowlog); +PHP_METHOD(Redis, wait); +PHP_METHOD(Redis, pubsub); PHP_METHOD(Redis, client); +/* SCAN and friends */ +PHP_METHOD(Redis, scan); +PHP_METHOD(Redis, hscan); +PHP_METHOD(Redis, sscan); +PHP_METHOD(Redis, zscan); + +/* Reflection */ PHP_METHOD(Redis, getHost); PHP_METHOD(Redis, getPort); PHP_METHOD(Redis, getDBNum); @@ -257,7 +269,7 @@ extern zend_module_entry redis_module_entry; #define phpext_redis_ptr redis_module_ptr -#define PHP_REDIS_VERSION "2.2.4" +#define PHP_REDIS_VERSION "2.2.5" #endif diff --git a/redis.c b/redis.c index f869d001..b565069d 100644 --- a/redis.c +++ b/redis.c @@ -69,6 +69,25 @@ PHP_INI_BEGIN() PHP_INI_ENTRY("redis.arrays.autorehash", "", PHP_INI_ALL, NULL) PHP_INI_END() +/** + * Argument info for the SCAN proper + */ +ZEND_BEGIN_ARG_INFO_EX(arginfo_scan, 0, 0, 1) + ZEND_ARG_INFO(1, i_iterator) + ZEND_ARG_INFO(0, str_pattern) + ZEND_ARG_INFO(0, i_count) +ZEND_END_ARG_INFO(); + +/** + * Argument info for key scanning + */ +ZEND_BEGIN_ARG_INFO_EX(arginfo_kscan, 0, 0, 2) + ZEND_ARG_INFO(0, str_key) + ZEND_ARG_INFO(1, i_iterator) + ZEND_ARG_INFO(0, str_pattern) + ZEND_ARG_INFO(0, i_count) +ZEND_END_ARG_INFO(); + #ifdef ZTS ZEND_DECLARE_MODULE_GLOBALS(redis) #endif @@ -160,6 +179,7 @@ static zend_function_entry redis_functions[] = { PHP_ME(Redis, object, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, bitop, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, bitcount, NULL, ZEND_ACC_PUBLIC) + PHP_ME(Redis, bitpos, NULL, ZEND_ACC_PUBLIC) /* 1.1 */ PHP_ME(Redis, mset, NULL, ZEND_ACC_PUBLIC) @@ -228,10 +248,17 @@ static zend_function_entry redis_functions[] = { PHP_ME(Redis, clearLastError, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, _prefix, NULL, ZEND_ACC_PUBLIC) + PHP_ME(Redis, _serialize, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, _unserialize, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, client, NULL, ZEND_ACC_PUBLIC) + /* SCAN and friends */ + PHP_ME(Redis, scan, arginfo_scan, ZEND_ACC_PUBLIC) + PHP_ME(Redis, hscan, arginfo_kscan, ZEND_ACC_PUBLIC) + PHP_ME(Redis, zscan, arginfo_kscan, ZEND_ACC_PUBLIC) + PHP_ME(Redis, sscan, arginfo_kscan, ZEND_ACC_PUBLIC) + /* options */ PHP_ME(Redis, getOption, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, setOption, NULL, ZEND_ACC_PUBLIC) @@ -252,6 +279,9 @@ static zend_function_entry redis_functions[] = { PHP_ME(Redis, getAuth, NULL, ZEND_ACC_PUBLIC) PHP_ME(Redis, isConnected, NULL, ZEND_ACC_PUBLIC) + PHP_ME(Redis, wait, NULL, ZEND_ACC_PUBLIC) + PHP_ME(Redis, pubsub, NULL, ZEND_ACC_PUBLIC) + /* aliases */ PHP_MALIAS(Redis, open, connect, NULL, ZEND_ACC_PUBLIC) PHP_MALIAS(Redis, popen, pconnect, NULL, ZEND_ACC_PUBLIC) @@ -488,6 +518,11 @@ PHP_MINIT_FUNCTION(redis) /* serializer */ add_constant_long(redis_ce, "SERIALIZER_NONE", REDIS_SERIALIZER_NONE); add_constant_long(redis_ce, "SERIALIZER_PHP", REDIS_SERIALIZER_PHP); + + /* scan options*/ + add_constant_long(redis_ce, "OPT_SCAN", REDIS_OPT_SCAN); + add_constant_long(redis_ce, "SCAN_RETRY", REDIS_SCAN_RETRY); + add_constant_long(redis_ce, "SCAN_NORETRY", REDIS_SCAN_NORETRY); #ifdef HAVE_REDIS_IGBINARY add_constant_long(redis_ce, "SERIALIZER_IGBINARY", REDIS_SERIALIZER_IGBINARY); #endif @@ -790,6 +825,59 @@ PHP_METHOD(Redis, bitcount) } /* }}} */ +/* {{{ proto integer Redis::bitpos(string key, int bit, [int start], [int end]) */ +PHP_METHOD(Redis, bitpos) +{ + zval *object; + RedisSock *redis_sock; + char *key, *cmd; + int key_len, cmd_len, argc, key_free=0; + long bit, start, end; + + argc = ZEND_NUM_ARGS(); + + if(zend_parse_method_parameters(argc TSRMLS_CC, getThis(), "Osl|ll", + &object, redis_ce, &key, &key_len, &bit, + &start, &end)==FAILURE) + { + RETURN_FALSE; + } + + if(redis_sock_get(object, &redis_sock TSRMLS_CC, 0) < 0) { + RETURN_FALSE; + } + + // We can prevalidate the first argument + if(bit != 0 && bit != 1) { + RETURN_FALSE; + } + + // Prefix our key + key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); + + // Various command semantics + if(argc == 2) { + cmd_len = redis_cmd_format_static(&cmd, "BITPOS", "sd", key, key_len, + bit); + } else if(argc == 3) { + cmd_len = redis_cmd_format_static(&cmd, "BITPOS", "sdd", key, key_len, + bit, start); + } else { + cmd_len = redis_cmd_format_static(&cmd, "BITPOS", "sddd", key, key_len, + bit, start, end); + } + + // Free our key if it was prefixed + if(key_free) efree(key); + + REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); + IF_ATOMIC() { + redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); + } + REDIS_PROCESS_RESPONSE(redis_long_response); +} +/* }}} */ + /* {{{ proto boolean Redis::close() */ PHP_METHOD(Redis, close) @@ -839,7 +927,9 @@ PHP_METHOD(Redis, set) { /* Our optional argument can either be a long (to support legacy SETEX */ /* redirection), or an array with Redis >= 2.6.12 set options */ - if(z_opts && Z_TYPE_P(z_opts) != IS_LONG && Z_TYPE_P(z_opts) != IS_ARRAY) { + if(z_opts && Z_TYPE_P(z_opts) != IS_LONG && Z_TYPE_P(z_opts) != IS_ARRAY + && Z_TYPE_P(z_opts) != IS_NULL) + { RETURN_FALSE; } @@ -903,7 +993,7 @@ PHP_METHOD(Redis, set) { /* Free our key or value if we prefixed/serialized */ if(key_free) efree(key); - if(val_free) efree(val); + if(val_free) STR_FREE(val); /* Kick off the command */ REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -936,7 +1026,7 @@ PHP_REDIS_API void redis_generic_setex(INTERNAL_FUNCTION_PARAMETERS, char *keywo val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, keyword, "sls", key, key_len, expire, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -985,7 +1075,7 @@ PHP_METHOD(Redis, setnx) val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "SETNX", "ss", key, key_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -1023,7 +1113,7 @@ PHP_METHOD(Redis, getSet) val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "GETSET", "ss", key, key_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -1331,12 +1421,10 @@ PHP_METHOD(Redis, incrByFloat) { RETURN_FALSE; } - // Prefix our key, free it if we have + // Prefix key, format command, free old key if necissary key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); - if(key_free) efree(key); - - // Format our INCRBYFLOAT command cmd_len = redis_cmd_format_static(&cmd, "INCRBYFLOAT", "sf", key, key_len, val); + if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); IF_ATOMIC() { @@ -1742,6 +1830,11 @@ PHP_METHOD(Redis, getBit) RETURN_FALSE; } + // GETBIT and SETBIT only work for 0 - 2^32-1 + if(offset < BITOP_MIN_OFFSET || offset > BITOP_MAX_OFFSET) { + RETURN_FALSE; + } + key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "GETBIT", "sd", key, key_len, (int)offset); if(key_free) efree(key); @@ -1771,6 +1864,11 @@ PHP_METHOD(Redis, setBit) RETURN_FALSE; } + // GETBIT and SETBIT only work for 0 - 2^32-1 + if(offset < BITOP_MIN_OFFSET || offset > BITOP_MAX_OFFSET) { + RETURN_FALSE; + } + key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "SETBIT", "sdd", key, key_len, (int)offset, (int)val); if(key_free) efree(key); @@ -1833,7 +1931,7 @@ generic_push_function(INTERNAL_FUNCTION_PARAMETERS, char *keyword, int keyword_l val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, keyword, "ss", key, key_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -1909,9 +2007,9 @@ PHP_METHOD(Redis, lInsert) val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); pivot_free = redis_serialize(redis_sock, z_pivot, &pivot, &pivot_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "LINSERT", "ssss", key, key_len, position, position_len, pivot, pivot_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); - if(pivot_free) efree(pivot); + if(pivot_free) STR_FREE(pivot); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); IF_ATOMIC() { @@ -2083,7 +2181,7 @@ PHP_METHOD(Redis, lRemove) val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "LREM", "sds", key, key_len, count, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -2287,7 +2385,7 @@ PHP_METHOD(Redis, sMove) src_free = redis_key_prefix(redis_sock, &src, &src_len TSRMLS_CC); dst_free = redis_key_prefix(redis_sock, &dst, &dst_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "SMOVE", "sss", src, src_len, dst, dst_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(src_free) efree(src); if(dst_free) efree(dst); @@ -2349,14 +2447,23 @@ PHP_METHOD(Redis, sRandMember) // Process our command REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); - // Process our reply + // Either bulk or multi-bulk depending on argument count + if(ZEND_NUM_ARGS() == 2) { IF_ATOMIC() { - // This will be bulk or multi-bulk depending if we passed the optional [COUNT] argument - if(redis_read_variant_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL) < 0) { + if(redis_sock_read_multibulk_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, + redis_sock, NULL, NULL) < 0) + { RETURN_FALSE; } } - REDIS_PROCESS_RESPONSE(redis_read_variant_reply); + REDIS_PROCESS_RESPONSE(redis_sock_read_multibulk_reply); + } else { + IF_ATOMIC() { + redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, + NULL, NULL); + } + REDIS_PROCESS_RESPONSE(redis_string_response); + } } /* }}} */ @@ -2384,7 +2491,7 @@ PHP_METHOD(Redis, sContains) val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "SISMEMBER", "ss", key, key_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -2612,7 +2719,7 @@ PHP_REDIS_API int generic_multiple_args_cmd(INTERNAL_FUNCTION_PARAMETERS, char * /* cleanup prefixed keys. */ for(i = 0; i < real_argc + (has_timeout?-1:0); ++i) { if(keys_to_free[i]) - efree(keys[i]); + STR_FREE(keys[i]); } if(single_array && has_timeout) { /* cleanup string created to contain timeout value */ efree(keys[real_argc-1]); @@ -3258,7 +3365,7 @@ PHP_METHOD(Redis, lSet) { val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "LSET", "sds", key, key_len, index, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -3678,7 +3785,7 @@ generic_mset(INTERNAL_FUNCTION_PARAMETERS, char *kw, void (*fun)(INTERNAL_FUNCTI memcpy(p, _NL, 2); p += 2; } - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); } } @@ -3861,7 +3968,7 @@ PHP_METHOD(Redis, zAdd) { smart_str_appendl(&buf, val, val_len); smart_str_appendl(&buf, _NL, sizeof(_NL) - 1); - if(val_free) efree(val); + if(val_free) STR_FREE(val); } /* end string */ @@ -4258,7 +4365,7 @@ PHP_METHOD(Redis, zScore) val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, "ZSCORE", "ss", key, key_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -4291,7 +4398,7 @@ PHP_REDIS_API void generic_rank_method(INTERNAL_FUNCTION_PARAMETERS, char *keywo val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, keyword, "ss", key, key_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -4341,7 +4448,7 @@ PHP_REDIS_API void generic_incrby_method(INTERNAL_FUNCTION_PARAMETERS, char *key val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, keyword, "sfs", key, key_len, add, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -4556,7 +4663,7 @@ generic_hset(INTERNAL_FUNCTION_PARAMETERS, char *kw, void (*fun)(INTERNAL_FUNCTI val_free = redis_serialize(redis_sock, z_value, &val, &val_len TSRMLS_CC); key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); cmd_len = redis_cmd_format_static(&cmd, kw, "sss", key, key_len, member, member_len, val, val_len); - if(val_free) efree(val); + if(val_free) STR_FREE(val); if(key_free) efree(key); REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); @@ -5076,7 +5183,7 @@ PHP_METHOD(Redis, hMset) redis_cmd_append_sstr(&set_cmds, hkey, hkey_len - 1); redis_cmd_append_sstr(&set_cmds, hval, hval_len); - if(hval_free) efree(hval); + if(hval_free) STR_FREE(hval); } // Now construct the entire command @@ -5804,22 +5911,19 @@ PHP_METHOD(Redis, getOption) { } switch(option) { - case REDIS_OPT_SERIALIZER: RETURN_LONG(redis_sock->serializer); - case REDIS_OPT_PREFIX: if(redis_sock->prefix) { RETURN_STRINGL(redis_sock->prefix, redis_sock->prefix_len, 1); } RETURN_NULL(); - case REDIS_OPT_READ_TIMEOUT: RETURN_DOUBLE(redis_sock->read_timeout); - + case REDIS_OPT_SCAN: + RETURN_LONG(redis_sock->scan); default: RETURN_FALSE; - } } /* }}} */ @@ -5857,7 +5961,6 @@ PHP_METHOD(Redis, setOption) { RETURN_FALSE; } break; - case REDIS_OPT_PREFIX: if(redis_sock->prefix) { efree(redis_sock->prefix); @@ -5871,17 +5974,22 @@ PHP_METHOD(Redis, setOption) { memcpy(redis_sock->prefix, val_str, val_len); } RETURN_TRUE; - case REDIS_OPT_READ_TIMEOUT: redis_sock->read_timeout = atof(val_str); if(redis_sock->stream) { read_tv.tv_sec = (time_t)redis_sock->read_timeout; read_tv.tv_usec = (int)((redis_sock->read_timeout - read_tv.tv_sec) * 1000000); - php_stream_set_option(redis_sock->stream, PHP_STREAM_OPTION_READ_TIMEOUT, - 0, &read_tv); + php_stream_set_option(redis_sock->stream, PHP_STREAM_OPTION_READ_TIMEOUT,0, &read_tv); } RETURN_TRUE; - + case REDIS_OPT_SCAN: + val_long = atol(val_str); + if(val_long == REDIS_SCAN_NORETRY || val_long == REDIS_SCAN_RETRY) { + redis_sock->scan = val_long; + RETURN_TRUE; + } + RETURN_FALSE; + break; default: RETURN_FALSE; } @@ -5992,6 +6100,207 @@ PHP_METHOD(Redis, slowlog) { REDIS_PROCESS_RESPONSE(redis_read_variant_reply); } +/* {{{ proto Redis::wait(int num_slaves, int ms) }}} + */ +PHP_METHOD(Redis, wait) { + zval *object; + RedisSock *redis_sock; + long num_slaves, timeout; + char *cmd; + int cmd_len; + + // Make sure arguments are valid + if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oll", + &object, redis_ce, &num_slaves, &timeout) + ==FAILURE) + { + RETURN_FALSE; + } + + // Don't even send this to Redis if our args are negative + if(num_slaves < 0 || timeout < 0) { + RETURN_FALSE; + } + + // Grab our socket + if(redis_sock_get(object, &redis_sock TSRMLS_CC, 0)<0) { + RETURN_FALSE; + } + + // Construct the command + cmd_len = redis_cmd_format_static(&cmd, "WAIT", "ll", num_slaves, timeout); + + // Kick it off + REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); + IF_ATOMIC() { + redis_long_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL); + } + REDIS_PROCESS_RESPONSE(redis_long_response); +} + +/* + * Construct a PUBSUB command + */ +PHP_REDIS_API int +redis_build_pubsub_cmd(RedisSock *redis_sock, char **ret, PUBSUB_TYPE type, + zval *arg TSRMLS_DC) +{ + HashTable *ht_chan; + HashPosition ptr; + zval **z_ele; + char *key; + int cmd_len, key_len, key_free; + smart_str cmd = {0}; + + if(type == PUBSUB_CHANNELS) { + if(arg) { + // Get string argument and length. + key = Z_STRVAL_P(arg); + key_len = Z_STRLEN_P(arg); + + // Prefix if necissary + key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); + + // With a pattern + cmd_len = redis_cmd_format_static(ret, "PUBSUB", "ss", "CHANNELS", sizeof("CHANNELS")-1, + key, key_len); + + // Free the channel name if we prefixed it + if(key_free) efree(key); + + // Return command length + return cmd_len; + } else { + // No pattern + return redis_cmd_format_static(ret, "PUBSUB", "s", "CHANNELS", sizeof("CHANNELS")-1); + } + } else if(type == PUBSUB_NUMSUB) { + ht_chan = Z_ARRVAL_P(arg); + + // Add PUBSUB and NUMSUB bits + redis_cmd_init_sstr(&cmd, zend_hash_num_elements(ht_chan)+1, "PUBSUB", sizeof("PUBSUB")-1); + redis_cmd_append_sstr(&cmd, "NUMSUB", sizeof("NUMSUB")-1); + + // Iterate our elements + for(zend_hash_internal_pointer_reset_ex(ht_chan, &ptr); + zend_hash_get_current_data_ex(ht_chan, (void**)&z_ele, &ptr)==SUCCESS; + zend_hash_move_forward_ex(ht_chan, &ptr)) + { + char *key; + int key_len, key_free; + zval *z_tmp = NULL; + + if(Z_TYPE_PP(z_ele) == IS_STRING) { + key = Z_STRVAL_PP(z_ele); + key_len = Z_STRLEN_PP(z_ele); + } else { + MAKE_STD_ZVAL(z_tmp); + *z_tmp = **z_ele; + zval_copy_ctor(z_tmp); + convert_to_string(z_tmp); + + key = Z_STRVAL_P(z_tmp); + key_len = Z_STRLEN_P(z_tmp); + } + + // Apply prefix if required + key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); + + // Append this channel + redis_cmd_append_sstr(&cmd, key, key_len); + + // Free key if prefixed + if(key_free) efree(key); + + // Free our temp var if we converted from something other than a string + if(z_tmp) { + zval_dtor(z_tmp); + efree(z_tmp); + z_tmp = NULL; + } + } + + // Set return + *ret = cmd.c; + return cmd.len; + } else if(type == PUBSUB_NUMPAT) { + return redis_cmd_format_static(ret, "PUBSUB", "s", "NUMPAT", sizeof("NUMPAT")-1); + } + + // Shouldn't ever happen + return -1; +} + +/* + * {{{ proto Redis::pubsub("channels", pattern); + * proto Redis::pubsub("numsub", Array channels); + * proto Redis::pubsub("numpat"); }}} + */ +PHP_METHOD(Redis, pubsub) { + zval *object; + RedisSock *redis_sock; + char *keyword, *cmd; + int kw_len, cmd_len; + PUBSUB_TYPE type; + zval *arg=NULL; + + // Parse arguments + if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Os|z", + &object, redis_ce, &keyword, &kw_len, &arg) + ==FAILURE) + { + RETURN_FALSE; + } + + // Validate our sub command keyword, and that we've got proper arguments + if(!strncasecmp(keyword, "channels", sizeof("channels"))) { + // One (optional) string argument + if(arg && Z_TYPE_P(arg) != IS_STRING) { + RETURN_FALSE; + } + type = PUBSUB_CHANNELS; + } else if(!strncasecmp(keyword, "numsub", sizeof("numsub"))) { + // One array argument + if(ZEND_NUM_ARGS() < 2 || Z_TYPE_P(arg) != IS_ARRAY || + zend_hash_num_elements(Z_ARRVAL_P(arg))==0) + { + RETURN_FALSE; + } + type = PUBSUB_NUMSUB; + } else if(!strncasecmp(keyword, "numpat", sizeof("numpat"))) { + type = PUBSUB_NUMPAT; + } else { + // Invalid keyword + RETURN_FALSE; + } + + // Grab our socket context object + if(redis_sock_get(object, &redis_sock TSRMLS_CC, 0)<0) { + RETURN_FALSE; + } + + // Construct our "PUBSUB" command + cmd_len = redis_build_pubsub_cmd(redis_sock, &cmd, type, arg TSRMLS_CC); + + REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); + + if(type == PUBSUB_NUMSUB) { + IF_ATOMIC() { + if(redis_sock_read_multibulk_reply_zipped(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL)<0) { + RETURN_FALSE; + } + } + REDIS_PROCESS_RESPONSE(redis_sock_read_multibulk_reply_zipped); + } else { + IF_ATOMIC() { + if(redis_read_variant_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL)<0) { + RETURN_FALSE; + } + } + REDIS_PROCESS_RESPONSE(redis_read_variant_reply); + } +} + // Construct an EVAL or EVALSHA command, with option argument array and number of arguments that are keys parameter PHP_REDIS_API int redis_build_eval_cmd(RedisSock *redis_sock, char **ret, char *keyword, char *value, int val_len, zval *args, int keys_count TSRMLS_DC) { @@ -6291,18 +6600,20 @@ PHP_METHOD(Redis, restore) { } /* - * {{{ proto Redis::migrate(host port key dest-db timeout) + * {{{ proto Redis::migrate(host port key dest-db timeout [bool copy, bool replace]) */ PHP_METHOD(Redis, migrate) { zval *object; RedisSock *redis_sock; char *cmd, *host, *key; int cmd_len, host_len, key_len, key_free; + zend_bool copy=0, replace=0; long port, dest_db, timeout; // Parse arguments - if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oslsll", &object, redis_ce, - &host, &host_len, &port, &key, &key_len, &dest_db, &timeout) == FAILURE) { + if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oslsll|bb", &object, redis_ce, + &host, &host_len, &port, &key, &key_len, &dest_db, &timeout, + ©, &replace) == FAILURE) { RETURN_FALSE; } @@ -6313,7 +6624,26 @@ PHP_METHOD(Redis, migrate) { // Prefix our key if we need to, build our command key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); - cmd_len = redis_cmd_format_static(&cmd, "MIGRATE", "sdsdd", host, host_len, port, key, key_len, dest_db, timeout); + + // Construct our command + if(copy && replace) { + cmd_len = redis_cmd_format_static(&cmd, "MIGRATE", "sdsddss", host, host_len, port, + key, key_len, dest_db, timeout, "COPY", + sizeof("COPY")-1, "REPLACE", sizeof("REPLACE")-1); + } else if(copy) { + cmd_len = redis_cmd_format_static(&cmd, "MIGRATE", "sdsdds", host, host_len, port, + key, key_len, dest_db, timeout, "COPY", + sizeof("COPY")-1); + } else if(replace) { + cmd_len = redis_cmd_format_static(&cmd, "MIGRATE", "sdsdds", host, host_len, port, + key, key_len, dest_db, timeout, "REPLACE", + sizeof("REPLACE")-1); + } else { + cmd_len = redis_cmd_format_static(&cmd, "MIGRATE", "sdsdd", host, host_len, port, + key, key_len, dest_db, timeout); + } + + // Free our key if we prefixed it if(key_free) efree(key); // Kick off our MIGRATE request @@ -6352,6 +6682,36 @@ PHP_METHOD(Redis, _prefix) { } } +/* + * {{{ proto Redis::_serialize(value) + */ +PHP_METHOD(Redis, _serialize) { + zval *object; + RedisSock *redis_sock; + zval *z_val; + char *val; + int val_len; + + // Parse arguments + if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oz", + &object, redis_ce, &z_val) == FAILURE) + { + RETURN_FALSE; + } + + // Grab socket + if(redis_sock_get(object, &redis_sock TSRMLS_CC, 0) < 0) { + RETURN_FALSE; + } + + // Serialize, which will return a value even if no serializer is set + redis_serialize(redis_sock, z_val, &val, &val_len TSRMLS_CC); + + // Return serialized value. Tell PHP to make a copy as some can be interned. + RETVAL_STRINGL(val, val_len, 1); + STR_FREE(val); +} + /* * {{{ proto Redis::_unserialize(value) */ @@ -6634,4 +6994,169 @@ PHP_METHOD(Redis, client) { } } +/** + * Helper to format any combination of SCAN arguments + */ +PHP_REDIS_API int +redis_build_scan_cmd(char **cmd, REDIS_SCAN_TYPE type, char *key, int key_len, + int iter, char *pattern, int pattern_len, int count) +{ + char *keyword; + int arg_count, cmd_len; + + // Count our arguments +1 for key if it's got one, and + 2 for pattern + // or count given that they each carry keywords with them. + arg_count = 1 + (key_len>0) + (pattern_len>0?2:0) + (count>0?2:0); + + // Turn our type into a keyword + switch(type) { + case TYPE_SCAN: + keyword = "SCAN"; + break; + case TYPE_SSCAN: + keyword = "SSCAN"; + break; + case TYPE_HSCAN: + keyword = "HSCAN"; + break; + case TYPE_ZSCAN: + default: + keyword = "ZSCAN"; + break; + } + + // Start the command + cmd_len = redis_cmd_format_header(cmd, keyword, arg_count); + + // Add the key in question if we have one + if(key_len) { + cmd_len = redis_cmd_append_str(cmd, cmd_len, key, key_len); + } + + // Add our iterator + cmd_len = redis_cmd_append_int(cmd, cmd_len, iter); + + // Append COUNT if we've got it + if(count) { + cmd_len = redis_cmd_append_str(cmd, cmd_len, "COUNT", sizeof("COUNT")-1); + cmd_len = redis_cmd_append_int(cmd, cmd_len, count); + } + + // Append MATCH if we've got it + if(pattern_len) { + cmd_len = redis_cmd_append_str(cmd, cmd_len, "MATCH", sizeof("MATCH")-1); + cmd_len = redis_cmd_append_str(cmd, cmd_len, pattern, pattern_len); + } + + // Return our command length + return cmd_len; +} + +/** + * {{{ proto redis::scan(&$iterator, [pattern, [count]]) + */ +PHP_REDIS_API void +generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) { + zval *object, *z_iter; + RedisSock *redis_sock; + HashTable *hash; + char *pattern=NULL, *cmd, *key=NULL; + int cmd_len, key_len=0, pattern_len=0, num_elements, key_free=0; + long count=0, iter; + + // Different prototype depending on if this is a key based scan + if(type != TYPE_SCAN) { + // Requires a key + if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Osz/|s!l", + &object, redis_ce, &key, &key_len, &z_iter, + &pattern, &pattern_len, &count)==FAILURE) + { + RETURN_FALSE; + } + } else { + // Doesn't require a key + if(zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oz/|s!l", + &object, redis_ce, &z_iter, &pattern, &pattern_len, + &count) == FAILURE) + { + RETURN_FALSE; + } + } + + // Grab our socket + if(redis_sock_get(object, &redis_sock TSRMLS_CC, 0) < 0) { + RETURN_FALSE; + } + + // Calling this in a pipeline makes no sense + IF_NOT_ATOMIC() { + php_error_docref(NULL TSRMLS_CC, E_ERROR, "Can't call SCAN commands in multi or pipeline mode!"); + RETURN_FALSE; + } + + // The iterator should be passed in as NULL for the first iteration, but we can treat + // any NON LONG value as NULL for these purposes as we've seperated the variable anyway. + if(Z_TYPE_P(z_iter) != IS_LONG || Z_LVAL_P(z_iter)<0) { + // Convert to long + convert_to_long(z_iter); + iter = 0; + } else if(Z_LVAL_P(z_iter)!=0) { + // Update our iterator value for the next passthru + iter = Z_LVAL_P(z_iter); + } else { + // We're done, back to iterator zero + RETURN_FALSE; + } + + // Prefix our key if we've got one and we have a prefix set + if(key_len) { + key_free = redis_key_prefix(redis_sock, &key, &key_len TSRMLS_CC); + } + + /** + * Redis can return to us empty keys, especially in the case where there are a large + * number of keys to scan, and we're matching against a pattern. PHPRedis can be set + * up to abstract this from the user, by setting OPT_SCAN to REDIS_SCAN_RETRY. Otherwise + * we will return empty keys and the user will need to make subsequent calls with + * an updated iterator. + */ + do { + // Format our SCAN command + cmd_len = redis_build_scan_cmd(&cmd, type, key, key_len, (int)iter, + pattern, pattern_len, count); + + // Execute our command getting our new iterator value + REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); + if(redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, + redis_sock,type,&iter)<0) + { + if(key_free) efree(key); + RETURN_FALSE; + } + + // Get the number of elements + hash = Z_ARRVAL_P(return_value); + num_elements = zend_hash_num_elements(hash); + } while(redis_sock->scan == REDIS_SCAN_RETRY && iter != 0 && num_elements == 0); + + // Free our key if it was prefixed + if(key_free) efree(key); + + // Update our iterator reference + Z_LVAL_P(z_iter) = iter; +} + +PHP_METHOD(Redis, scan) { + generic_scan_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, TYPE_SCAN); +} +PHP_METHOD(Redis, hscan) { + generic_scan_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, TYPE_HSCAN); +} +PHP_METHOD(Redis, sscan) { + generic_scan_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, TYPE_SSCAN); +} +PHP_METHOD(Redis, zscan) { + generic_scan_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, TYPE_ZSCAN); +} + /* vim: set tabstop=4 softtabstops=4 noexpandtab shiftwidth=4: */ diff --git a/redis_array.c b/redis_array.c index 60d1022b..e9a56e7a 100644 --- a/redis_array.c +++ b/redis_array.c @@ -32,6 +32,12 @@ #include "redis_array.h" #include "redis_array_impl.h" +/* Simple macro to detect failure in a RedisArray call */ +#define RA_CALL_FAILED(rv, cmd) \ + ((Z_TYPE_P(rv) == IS_BOOL && Z_BVAL_P(rv) == 0) || \ + (Z_TYPE_P(rv) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(rv)) == 0) || \ + (Z_TYPE_P(rv) == IS_LONG && Z_LVAL_P(rv) == 0 && !strcasecmp(cmd, "TYPE"))) \ + extern zend_class_entry *redis_ce; zend_class_entry *redis_array_ce; @@ -76,43 +82,50 @@ zend_function_entry redis_array_functions[] = { {NULL, NULL, NULL} }; -int le_redis_array; -void redis_destructor_redis_array(zend_rsrc_list_entry * rsrc TSRMLS_DC) -{ +static void redis_array_free(RedisArray *ra) { int i; - RedisArray *ra = (RedisArray*)rsrc->ptr; - /* delete Redis objects */ - for(i = 0; i < ra->count; ++i) { + // Redis objects + for(i=0;icount;i++) { zval_dtor(ra->redis[i]); efree(ra->redis[i]); - - /* remove host too */ efree(ra->hosts[i]); } efree(ra->redis); efree(ra->hosts); - /* delete function */ + /* delete hash function */ if(ra->z_fun) { zval_dtor(ra->z_fun); efree(ra->z_fun); } - /* delete distributor */ + /* Distributor */ if(ra->z_dist) { zval_dtor(ra->z_dist); efree(ra->z_dist); } - /* delete list of pure commands */ + /* Delete pur commands */ zval_dtor(ra->z_pure_cmds); efree(ra->z_pure_cmds); - /* free container */ + // Free structure itself efree(ra); } +int le_redis_array; +void redis_destructor_redis_array(zend_rsrc_list_entry * rsrc TSRMLS_DC) +{ + RedisArray *ra = (RedisArray*)rsrc->ptr; + + /* Free previous ring if it's set */ + if(ra->prev) redis_array_free(ra->prev); + + /* Free parent array */ + redis_array_free(ra); +} + /** * redis_array_get */ @@ -199,6 +212,8 @@ PHP_METHOD(RedisArray, __construct) long l_retry_interval = 0; zend_bool b_lazy_connect = 0; zval **z_retry_interval_pp; + double d_connect_timeout = 0; + zval **z_connect_timeout_pp; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|a", &z0, &z_opts) == FAILURE) { RETURN_FALSE; @@ -261,6 +276,18 @@ PHP_METHOD(RedisArray, __construct) if(FAILURE != zend_hash_find(hOpts, "lazy_connect", sizeof("lazy_connect"), (void**)&zpData) && Z_TYPE_PP(zpData) == IS_BOOL) { b_lazy_connect = Z_BVAL_PP(zpData); } + + /* extract connect_timeout option */ + if (FAILURE != zend_hash_find(hOpts, "connect_timeout", sizeof("connect_timeout"), (void**)&z_connect_timeout_pp)) { + if (Z_TYPE_PP(z_connect_timeout_pp) == IS_DOUBLE || Z_TYPE_PP(z_connect_timeout_pp) == IS_STRING) { + if (Z_TYPE_PP(z_connect_timeout_pp) == IS_DOUBLE) { + d_connect_timeout = Z_DVAL_PP(z_connect_timeout_pp); + } + else { + d_connect_timeout = atof(Z_STRVAL_PP(z_connect_timeout_pp)); + } + } + } } /* extract either name of list of hosts from z0 */ @@ -270,7 +297,7 @@ PHP_METHOD(RedisArray, __construct) break; case IS_ARRAY: - ra = ra_make_array(Z_ARRVAL_P(z0), z_fun, z_dist, hPrev, b_index, b_pconnect, l_retry_interval, b_lazy_connect TSRMLS_CC); + ra = ra_make_array(Z_ARRVAL_P(z0), z_fun, z_dist, hPrev, b_index, b_pconnect, l_retry_interval, b_lazy_connect, d_connect_timeout TSRMLS_CC); break; default: @@ -280,6 +307,8 @@ PHP_METHOD(RedisArray, __construct) if(ra) { ra->auto_rehash = b_autorehash; + ra->connect_timeout = d_connect_timeout; + if(ra->prev) ra->prev->auto_rehash = b_autorehash; #if PHP_VERSION_ID >= 50400 id = zend_list_insert(ra, le_redis_array TSRMLS_CC); #else @@ -366,22 +395,14 @@ ra_forward_call(INTERNAL_FUNCTION_PARAMETERS, RedisArray *ra, const char *cmd, i } else { /* call directly through. */ call_user_function(&redis_ce->function_table, &redis_inst, &z_fun, return_value, argc, z_callargs TSRMLS_CC); - failed = 0; - if((Z_TYPE_P(return_value) == IS_BOOL && Z_BVAL_P(return_value) == 0) || - (Z_TYPE_P(return_value) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(return_value)) == 0) || - (Z_TYPE_P(return_value) == IS_LONG && Z_LVAL_P(return_value) == 0 && !strcasecmp(cmd, "TYPE"))) - - { - failed = 1; - } - /* check if we have an error. */ - if(failed && ra->prev && !b_write_cmd) { /* there was an error reading, try with prev ring. */ + if(RA_CALL_FAILED(return_value,cmd) && ra->prev && !b_write_cmd) { /* there was an error reading, try with prev ring. */ /* ERROR, FALLBACK TO PREVIOUS RING and forward a reference to the first redis instance we were looking at. */ ra_forward_call(INTERNAL_FUNCTION_PARAM_PASSTHRU, ra->prev, cmd, cmd_len, z_args, z_new_target?z_new_target:redis_inst); } - if(!failed && !b_write_cmd && z_new_target && ra->auto_rehash) { /* move key from old ring to new ring */ + /* Autorehash if the key was found on the previous node if this is a read command and auto rehashing is on */ + if(!RA_CALL_FAILED(return_value,cmd) && !b_write_cmd && z_new_target && ra->auto_rehash) { /* move key from old ring to new ring */ ra_move_key(key, key_len, redis_inst, z_new_target TSRMLS_CC); } } @@ -876,6 +897,9 @@ PHP_METHOD(RedisArray, mget) /* calls */ for(n = 0; n < ra->count; ++n) { /* for each node */ + /* We don't even need to make a call to this node if no keys go there */ + if(!argc_each[n]) continue; + /* copy args for MGET call on node. */ MAKE_STD_ZVAL(z_argarray); array_init(z_argarray); diff --git a/redis_array.h b/redis_array.h index 3b1163bf..2a8baf44 100644 --- a/redis_array.h +++ b/redis_array.h @@ -50,6 +50,7 @@ typedef struct RedisArray_ { zval *z_fun; /* key extractor, callable */ zval *z_dist; /* key distributor, callable */ zval *z_pure_cmds; /* hash table */ + double connect_timeout; /* socket connect timeout */ struct RedisArray_ *prev; } RedisArray; diff --git a/redis_array_impl.c b/redis_array_impl.c index 10c7dc81..ef499e1a 100644 --- a/redis_array_impl.c +++ b/redis_array_impl.c @@ -44,7 +44,9 @@ ra_load_hosts(RedisArray *ra, HashTable *hosts, long retry_interval, zend_bool b /* init connections */ for(i = 0; i < count; ++i) { - if(FAILURE == zend_hash_quick_find(hosts, NULL, 0, i, (void**)&zpData)) { + if(FAILURE == zend_hash_quick_find(hosts, NULL, 0, i, (void**)&zpData) || + Z_TYPE_PP(zpData) != IS_STRING) + { efree(ra); return NULL; } @@ -70,7 +72,7 @@ ra_load_hosts(RedisArray *ra, HashTable *hosts, long retry_interval, zend_bool b call_user_function(&redis_ce->function_table, &ra->redis[i], &z_cons, &z_ret, 0, NULL TSRMLS_CC); /* create socket */ - redis_sock = redis_sock_create(host, host_len, port, 0, ra->pconnect, NULL, retry_interval, b_lazy_connect); + redis_sock = redis_sock_create(host, host_len, port, ra->connect_timeout, ra->pconnect, NULL, retry_interval, b_lazy_connect); if (!b_lazy_connect) { @@ -166,12 +168,14 @@ RedisArray *ra_load_array(const char *name TSRMLS_DC) { zval *z_params_autorehash; zval *z_params_retry_interval; zval *z_params_pconnect; + zval *z_params_connect_timeout; zval *z_params_lazy_connect; RedisArray *ra = NULL; zend_bool b_index = 0, b_autorehash = 0, b_pconnect = 0; long l_retry_interval = 0; zend_bool b_lazy_connect = 0; + double d_connect_timeout = 0; HashTable *hHosts = NULL, *hPrev = NULL; /* find entry */ @@ -258,7 +262,8 @@ RedisArray *ra_load_array(const char *name TSRMLS_DC) { b_pconnect = 1; } } - /* find retry interval option */ + + /* find lazy connect option */ MAKE_STD_ZVAL(z_params_lazy_connect); array_init(z_params_lazy_connect); sapi_module.treat_data(PARSE_STRING, estrdup(INI_STR("redis.arrays.lazyconnect")), z_params_lazy_connect TSRMLS_CC); @@ -268,9 +273,25 @@ RedisArray *ra_load_array(const char *name TSRMLS_DC) { } } + /* find connect timeout option */ + MAKE_STD_ZVAL(z_params_connect_timeout); + array_init(z_params_connect_timeout); + sapi_module.treat_data(PARSE_STRING, estrdup(INI_STR("redis.arrays.connecttimeout")), z_params_connect_timeout TSRMLS_CC); + if (zend_hash_find(Z_ARRVAL_P(z_params_connect_timeout), name, strlen(name) + 1, (void **) &z_data_pp) != FAILURE) { + if (Z_TYPE_PP(z_data_pp) == IS_DOUBLE || Z_TYPE_PP(z_data_pp) == IS_STRING) { + if (Z_TYPE_PP(z_data_pp) == IS_DOUBLE) { + d_connect_timeout = Z_DVAL_PP(z_data_pp); + } + else { + d_connect_timeout = atof(Z_STRVAL_PP(z_data_pp)); + } + } + } + /* create RedisArray object */ - ra = ra_make_array(hHosts, z_fun, z_dist, hPrev, b_index, b_pconnect, l_retry_interval, b_lazy_connect TSRMLS_CC); + ra = ra_make_array(hHosts, z_fun, z_dist, hPrev, b_index, b_pconnect, l_retry_interval, b_lazy_connect, d_connect_timeout TSRMLS_CC); ra->auto_rehash = b_autorehash; + if(ra->prev) ra->prev->auto_rehash = b_autorehash; /* cleanup */ zval_dtor(z_params_hosts); @@ -287,6 +308,8 @@ RedisArray *ra_load_array(const char *name TSRMLS_DC) { efree(z_params_retry_interval); zval_dtor(z_params_pconnect); efree(z_params_pconnect); + zval_dtor(z_params_connect_timeout); + efree(z_params_connect_timeout); zval_dtor(z_params_lazy_connect); efree(z_params_lazy_connect); @@ -294,7 +317,7 @@ RedisArray *ra_load_array(const char *name TSRMLS_DC) { } RedisArray * -ra_make_array(HashTable *hosts, zval *z_fun, zval *z_dist, HashTable *hosts_prev, zend_bool b_index, zend_bool b_pconnect, long retry_interval, zend_bool b_lazy_connect TSRMLS_DC) { +ra_make_array(HashTable *hosts, zval *z_fun, zval *z_dist, HashTable *hosts_prev, zend_bool b_index, zend_bool b_pconnect, long retry_interval, zend_bool b_lazy_connect, double connect_timeout TSRMLS_DC) { int count = zend_hash_num_elements(hosts); @@ -308,6 +331,8 @@ ra_make_array(HashTable *hosts, zval *z_fun, zval *z_dist, HashTable *hosts_prev ra->z_multi_exec = NULL; ra->index = b_index; ra->auto_rehash = 0; + ra->pconnect = b_pconnect; + ra->connect_timeout = connect_timeout; /* init array data structures */ ra_init_function_table(ra); @@ -315,7 +340,7 @@ ra_make_array(HashTable *hosts, zval *z_fun, zval *z_dist, HashTable *hosts_prev if(NULL == ra_load_hosts(ra, hosts, retry_interval, b_lazy_connect TSRMLS_CC)) { return NULL; } - ra->prev = hosts_prev ? ra_make_array(hosts_prev, z_fun, z_dist, NULL, b_index, b_pconnect, retry_interval, b_lazy_connect TSRMLS_CC) : NULL; + ra->prev = hosts_prev ? ra_make_array(hosts_prev, z_fun, z_dist, NULL, b_index, b_pconnect, retry_interval, b_lazy_connect, connect_timeout TSRMLS_CC) : NULL; /* copy function if provided */ if(z_fun) { @@ -603,6 +628,7 @@ ra_index_key(const char *key, int key_len, zval *z_redis TSRMLS_DC) { /* don't dtor z_ret, since we're returning z_redis */ efree(z_args[0]); + zval_dtor(z_args[1]); efree(z_args[1]); } @@ -965,6 +991,7 @@ ra_move_string(const char *key, int key_len, zval *z_from, zval *z_to, long ttl ZVAL_STRINGL(z_args[0], key, key_len, 0); ZVAL_LONG(z_args[1], ttl); ZVAL_STRINGL(z_args[2], Z_STRVAL(z_ret), Z_STRLEN(z_ret), 1); /* copy z_ret to arg 1 */ + zval_dtor(&z_ret); /* free memory from our previous call */ call_user_function(&redis_ce->function_table, &z_to, &z_fun_set, &z_ret, 3, z_args TSRMLS_CC); /* cleanup */ efree(z_args[1]); @@ -975,6 +1002,7 @@ ra_move_string(const char *key, int key_len, zval *z_from, zval *z_to, long ttl ZVAL_STRINGL(&z_fun_set, "SET", 3, 0); ZVAL_STRINGL(z_args[0], key, key_len, 0); ZVAL_STRINGL(z_args[1], Z_STRVAL(z_ret), Z_STRLEN(z_ret), 1); /* copy z_ret to arg 1 */ + zval_dtor(&z_ret); /* free memory from our previous return value */ call_user_function(&redis_ce->function_table, &z_to, &z_fun_set, &z_ret, 2, z_args TSRMLS_CC); /* cleanup */ zval_dtor(z_args[1]); diff --git a/redis_array_impl.h b/redis_array_impl.h index 8f106542..06b5332a 100644 --- a/redis_array_impl.h +++ b/redis_array_impl.h @@ -12,7 +12,7 @@ RedisArray *ra_load_hosts(RedisArray *ra, HashTable *hosts, long retry_interval, zend_bool b_lazy_connect TSRMLS_DC); RedisArray *ra_load_array(const char *name TSRMLS_DC); -RedisArray *ra_make_array(HashTable *hosts, zval *z_fun, zval *z_dist, HashTable *hosts_prev, zend_bool b_index, zend_bool b_pconnect, long retry_interval, zend_bool b_lazy_connect TSRMLS_DC); +RedisArray *ra_make_array(HashTable *hosts, zval *z_fun, zval *z_dist, HashTable *hosts_prev, zend_bool b_index, zend_bool b_pconnect, long retry_interval, zend_bool b_lazy_connect, double connect_timeout TSRMLS_DC); zval *ra_find_node_by_name(RedisArray *ra, const char *host, int host_len TSRMLS_DC); zval *ra_find_node(RedisArray *ra, const char *key, int key_len, int *out_pos TSRMLS_DC); void ra_init_function_table(RedisArray *ra); diff --git a/rpm/php-redis.spec b/rpm/php-redis.spec index 4f04fb18..5363d1ee 100644 --- a/rpm/php-redis.spec +++ b/rpm/php-redis.spec @@ -3,7 +3,7 @@ %global php_version %(php-config --version 2>/dev/null || echo 0) Name: php-redis -Version: 2.2.4 +Version: 2.2.5 Release: 1%{?dist} Summary: The phpredis extension provides an API for communicating with the Redis key-value store. diff --git a/tests/TestRedis.php b/tests/TestRedis.php index 2a684754..ad577202 100644 --- a/tests/TestRedis.php +++ b/tests/TestRedis.php @@ -72,6 +72,47 @@ class Redis_Test extends TestSuite $this->assertTrue(is_array($ret) && count($ret) === 1 && $ret[0] >= 0); } + // Run some simple tests against the PUBSUB command. This is problematic, as we + // can't be sure what's going on in the instance, but we can do some things. + public function testPubSub() { + // Only available since 2.8.0 + if(version_compare($this->version, "2.8.0", "lt")) { + $this->markTestSkipped(); + return; + } + + // PUBSUB CHANNELS ... + $result = $this->redis->pubsub("channels", "*"); + $this->assertTrue(is_array($result)); + $result = $this->redis->pubsub("channels"); + $this->assertTrue(is_array($result)); + + // PUBSUB NUMSUB + + $c1 = uniqid() . '-' . rand(1,100); + $c2 = uniqid() . '-' . rand(1,100); + + $result = $this->redis->pubsub("numsub", Array($c1, $c2)); + + // Should get an array back, with two elements + $this->assertTrue(is_array($result)); + $this->assertEquals(count($result), 2); + + // Make sure the elements are correct, and have zero counts + foreach(Array($c1,$c2) as $channel) { + $this->assertTrue(isset($result[$channel])); + $this->assertEquals($result[$channel], "0"); + } + + // PUBSUB NUMPAT + $result = $this->redis->pubsub("numpat"); + $this->assertTrue(is_int($result)); + + // Invalid calls + $this->assertFalse($this->redis->pubsub("notacommand")); + $this->assertFalse($this->redis->pubsub("numsub", "not-an-array")); + } + public function testBitsets() { $this->redis->delete('key'); @@ -110,6 +151,31 @@ class Redis_Test extends TestSuite // values above 1 are changed to 1 but don't overflow on bits to the right. $this->assertTrue(0 === $this->redis->setBit('key', 0, 0xff)); $this->assertTrue("\x9f" === $this->redis->get('key')); + + // Verify valid offset ranges + $this->assertFalse($this->redis->getBit('key', -1)); + $this->assertFalse($this->redis->getBit('key', 4294967296)); + $this->assertFalse($this->redis->setBit('key', -1, 1)); + $this->assertFalse($this->redis->setBit('key', 4294967296, 1)); + } + + public function testBitPos() { + if(version_compare($this->version, "2.8.7", "lt")) { + $this->MarkTestSkipped(); + return; + } + + $this->redis->del('bpkey'); + + $this->redis->set('bpkey', "\xff\xf0\x00"); + $this->assertEquals($this->redis->bitpos('bpkey', 0), 12); + + $this->redis->set('bpkey', "\x00\xff\xf0"); + $this->assertEquals($this->redis->bitpos('bpkey', 1, 0), 8); + $this->assertEquals($this->redis->bitpos('bpkey', 1, 1), 8); + + $this->redis->set('bpkey', "\x00\x00\x00"); + $this->assertEquals($this->redis->bitpos('bpkey', 1), -1); } public function test1000() { @@ -258,6 +324,12 @@ class Redis_Test extends TestSuite $this->assertTrue($this->redis->set('foo','barbaz', Array('not-valid','nx','invalid','ex'=>200))); $this->assertEquals($this->redis->ttl('foo'), 200); $this->assertEquals($this->redis->get('foo'), 'barbaz'); + + /* Pass NULL as the optional arguments which should be ignored */ + $this->redis->del('foo'); + $this->redis->set('foo','bar', NULL); + $this->assertEquals($this->redis->get('foo'), 'bar'); + $this->assertTrue($this->redis->ttl('foo')<0); } public function testGetSet() { @@ -475,6 +547,16 @@ class Redis_Test extends TestSuite $this->redis->incrbyfloat('key', -1.5); $this->assertTrue("abc" === $this->redis->get('key')); + + // Test with prefixing + $this->redis->setOption(Redis::OPT_PREFIX, 'someprefix:'); + $this->redis->del('key'); + $this->redis->incrbyfloat('key',1.8); + $this->assertEquals('1.8', $this->redis->get('key')); + $this->redis->setOption(Redis::OPT_PREFIX, ''); + $this->assertTrue($this->redis->exists('someprefix:key')); + $this->redis->del('someprefix:key'); + } public function testDecr() @@ -1144,6 +1226,28 @@ class Redis_Test extends TestSuite break; } } + + // + // With and without count, while serializing + // + + $this->redis->delete('set0'); + $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); + for($i=0;$i<5;$i++) { + $member = "member:$i"; + $this->redis->sAdd('set0', $member); + $mems[] = $member; + } + + $member = $this->redis->srandmember('set0'); + $this->assertTrue(in_array($member, $mems)); + + $rmembers = $this->redis->srandmember('set0', $i); + foreach($rmembers as $reply_mem) { + $this->assertTrue(in_array($reply_mem, $mems)); + } + + $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); } public function testSRandMemberWithCount() { @@ -1759,6 +1863,34 @@ class Redis_Test extends TestSuite $this->assertFalse($this->redis->slowlog('notvalid')); } + public function testWait() { + // Closest we can check based on redis commmit history + if(version_compare($this->version, '2.9.11', 'lt')) { + $this->markTestSkipped(); + return; + } + + // We could have slaves here, so determine that + $arr_slaves = $this->redis->info(); + $i_slaves = $arr_slaves['connected_slaves']; + + // Send a couple commands + $this->redis->set('wait-foo', 'over9000'); + $this->redis->set('wait-bar', 'revo9000'); + + // Make sure we get the right replication count + $this->assertEquals($this->redis->wait($i_slaves, 100), $i_slaves); + + // Pass more slaves than are connected + $this->redis->set('wait-foo','over9000'); + $this->redis->set('wait-bar','revo9000'); + $this->assertTrue($this->redis->wait($i_slaves+1, 100) < $i_slaves+1); + + // Make sure when we pass with bad arguments we just get back false + $this->assertFalse($this->redis->wait(-1, -1)); + $this->assertFalse($this->redis->wait(-1, 20)); + } + public function testinfo() { $info = $this->redis->info(); @@ -4356,6 +4488,34 @@ class Redis_Test extends TestSuite $this->assertTrue(1 === $this->redis->evalsha($sha)); } + public function testSerialize() { + $vals = Array(1, 1.5, 'one', Array('here','is','an','array')); + + // Test with no serialization at all + $this->assertTrue($this->redis->_serialize('test') === 'test'); + $this->assertTrue($this->redis->_serialize(1) === '1'); + $this->assertTrue($this->redis->_serialize(Array()) === 'Array'); + $this->assertTrue($this->redis->_serialize(new stdClass) === 'Object'); + + $arr_serializers = Array(Redis::SERIALIZER_PHP); + if(defined('Redis::SERIALIZER_IGBINARY')) { + $arr_serializers[] = Redis::SERIALIZER_IGBINARY; + } + + foreach($arr_serializers as $mode) { + $arr_enc = Array(); + $arr_dec = Array(); + + foreach($vals as $k => $v) { + $enc = $this->redis->_serialize($v); + $dec = $this->redis->_unserialize($enc); + + // They should be the same + $this->assertTrue($enc == $dec); + } + } + } + public function testUnserialize() { $vals = Array( 1,1.5,'one',Array('this','is','an','array') @@ -4462,6 +4622,190 @@ class Redis_Test extends TestSuite $this->assertTrue($this->redis->getAuth() === self::AUTH); } + /** + * Scan and variants + */ + + protected function get_keyspace_count($str_db) { + $arr_info = $this->redis->info(); + $arr_info = $arr_info[$str_db]; + $arr_info = explode(',', $arr_info); + $arr_info = explode('=', $arr_info[0]); + return $arr_info[1]; + } + + public function testScan() { + if(version_compare($this->version, "2.8.0", "lt")) { + $this->markTestSkipped(); + return; + } + + // Key count + $i_key_count = $this->get_keyspace_count('db0'); + + // Have scan retry + $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); + + // Scan them all + $it = NULL; + while($arr_keys = $this->redis->scan($it)) { + $i_key_count -= count($arr_keys); + } + // Should have iterated all keys + $this->assertEquals(0, $i_key_count); + + // Unique keys, for pattern matching + $str_uniq = uniqid() . '-' . uniqid(); + for($i=0;$i<10;$i++) { + $this->redis->set($str_uniq . "::$i", "bar::$i"); + } + + // Scan just these keys using a pattern match + $it = NULL; + while($arr_keys = $this->redis->scan($it, "*$str_uniq*")) { + $i -= count($arr_keys); + } + $this->assertEquals(0, $i); + } + + public function testHScan() { + if(version_compare($this->version, "2.8.0", "lt")) { + $this->markTestSkipped(); + return; + } + + // Never get empty sets + $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); + + $this->redis->del('hash'); + $i_foo_mems = 0; + + for($i=0;$i<100;$i++) { + if($i>3) { + $this->redis->hset('hash', "member:$i", "value:$i"); + } else { + $this->redis->hset('hash', "foomember:$i", "value:$i"); + $i_foo_mems++; + } + } + + // Scan all of them + $it = NULL; + while($arr_keys = $this->redis->hscan('hash', $it)) { + $i -= count($arr_keys); + } + $this->assertEquals(0, $i); + + // Scan just *foomem* (should be 4) + $it = NULL; + while($arr_keys = $this->redis->hscan('hash', $it, '*foomember*')) { + $i_foo_mems -= count($arr_keys); + foreach($arr_keys as $str_mem => $str_val) { + $this->assertTrue(strpos($str_mem, 'member')!==FALSE); + $this->assertTrue(strpos($str_val, 'value')!==FALSE); + } + } + $this->assertEquals(0, $i_foo_mems); + } + + public function testSScan() { + if(version_compare($this->version, "2.8.0", "lt")) { + $this->markTestSkipped(); + return; + } + + $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); + + $this->redis->del('set'); + for($i=0;$i<100;$i++) { + $this->redis->sadd('set', "member:$i"); + } + + // Scan all of them + $it = NULL; + while($arr_keys = $this->redis->sscan('set', $it)) { + $i -= count($arr_keys); + foreach($arr_keys as $str_mem) { + $this->assertTrue(strpos($str_mem,'member')!==FALSE); + } + } + $this->assertEquals(0, $i); + + // Scan just ones with zero in them (0, 10, 20, 30, 40, 50, 60, 70, 80, 90) + $it = NULL; + $i_w_zero = 0; + while($arr_keys = $this->redis->sscan('set', $it, '*0*')) { + $i_w_zero += count($arr_keys); + } + $this->assertEquals(10, $i_w_zero); + } + + public function testZScan() { + if(version_compare($this->version, "2.8.0", "lt")) { + $this->markTestSkipped(); + return; + } + + $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); + + $this->redis->del('zset'); + $i_tot_score = 0; + $i_p_score = 0; + $i_p_count = 0; + for($i=0;$i<2000;$i++) { + if($i<10) { + $this->redis->zadd('zset', $i, "pmem:$i"); + $i_p_score += $i; + $i_p_count += 1; + } else { + $this->redis->zadd('zset', $i, "mem:$i"); + } + + $i_tot_score += $i; + } + + // Scan them all + $it = NULL; + while($arr_keys = $this->redis->zscan('zset', $it)) { + foreach($arr_keys as $str_mem => $f_score) { + $i_tot_score -= $f_score; + $i--; + } + } + $this->assertEquals(0, $i); + $this->assertEquals(0, $i_tot_score); + + // Just scan "pmem" members + $it = NULL; + $i_p_score_old = $i_p_score; + $i_p_count_old = $i_p_count; + while($arr_keys = $this->redis->zscan('zset', $it, "*pmem*")) { + foreach($arr_keys as $str_mem => $f_score) { + $i_p_score -= $f_score; + $i_p_count -= 1; + } + } + $this->assertEquals(0, $i_p_score); + $this->assertEquals(0, $i_p_count); + + // Turn off retrying and we should get some empty results + $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_NORETRY); + $i_skips = 0; + $i_p_score = $i_p_score_old; + $i_p_count = $i_p_count_old; + $it = NULL; + while(($arr_keys = $this->redis->zscan('zset', $it, "*pmem*")) !== FALSE) { + if(count($arr_keys) == 0) $i_skips++; + foreach($arr_keys as $str_mem => $f_score) { + $i_p_score -= $f_score; + $i_p_count -= 1; + } + } + // We should still get all the keys, just with several empty results + $this->assertTrue($i_skips > 0); + $this->assertEquals(0, $i_p_score); + $this->assertEquals(0, $i_p_count); + } } exit(TestSuite::run("Redis_Test")); -- cgit v1.2.3