diff --git a/hphp/system/php.txt b/hphp/system/php.txt index 849fd5d62..e035219e2 100644 --- a/hphp/system/php.txt +++ b/hphp/system/php.txt @@ -44,6 +44,8 @@ hphp/system/php/filter/filter_input_array.php hphp/system/php/sessions/SessionHandlerInterface.php hphp/system/php/sessions/session_set_save_handler.php +hphp/system/php/redis/RedisException.php + # If you have no inheritance relationship, go here in alphabetical order hphp/system/php/DebuggerCommand.php hphp/system/php/XhprofFrame.php @@ -57,6 +59,7 @@ hphp/system/php/file_system/Directory.php hphp/system/php/json/JsonSerializable.php hphp/system/php/lang/ErrorException.php hphp/system/php/pdo/PDOException.php +hphp/system/php/redis/Redis.php hphp/system/php/reflection/reflection.php hphp/system/php/sessions/session_register_shutdown.php hphp/system/php/soap/SoapFault.php diff --git a/hphp/system/php/redis/Redis.php b/hphp/system/php/redis/Redis.php new file mode 100644 index 000000000..d625ac9af --- /dev/null +++ b/hphp/system/php/redis/Redis.php @@ -0,0 +1,1556 @@ +doConnect($host, $port, $timeout, $persistent_id, + $retry_interval, false); + } + + public function pconnect($host, + $port = -1, + $timeout = 0.0, + $persistent_id = '', + $retry_interval = 0) { + return $this->doConnect($host, $port, $timeout, $persistent_id, + $retry_interval, true); + } + + public function auth($password) { + $this->password = $password; + $this->processComment('AUTH', $password); + return $this->processBooleanResponse(); + } + + public function close() { + $this->processCommand('QUIT'); + fclose($this->connection); + $this->connection = null; + } + + public function select($dbNumber) { + $this->dbNumber = (int)$dbNumber; + $this->processCommand("SELECT", (int)$dbNumber); + return $this->processBooleanResponse(); + } + + public function setOption($opt, $value) { + switch ($opt) { + case self::OPT_PREFIX: + $this->prefix = $value; + return true; + + case self::OPT_SERIALIZER: + if (($value !== self::SERIALIZER_NONE) AND + ($value !== self::SERIALIZER_PHP) AND + ($value !== self::SERIALIZER_IGBINARY)) { + throw new RedisException("Invalid serializer option: $value"); + } + $this->serializer = (int)$value; + return true; + + case self::OPT_READ_TIMEOUT: + $this->timeout_seconds = (int)($value - ((int)$value)); + $this->timeout_useconds = (int)(($value - $seconds) * 1000000); + return stream_set_timeout($this->connection, $this->timeout_seconds, + $this->timeout_useconds); + + default: + return false; + } + } + + public function getOption($opt) { + switch ($opt) { + case self::OPT_PREFIX: return $this->prefix; + case self::OPT_SERIALIZER: return $this->serializer; + case self::OPT_READ_TIMEOUT: + return $this->timeout_seconds + ($this->timeout_useconds / 1000000); + } + return false; + } + + /* Server -------------------------------------------------------------- */ + + public function config($op, $key, $val = '') { + if ($op == 'GET') { + $this->processCommand('CONFIG', 'GET', $key); + return $this->processMapResponse(false, false); + } else if ($op == 'SET') { + $this->processCommand('CONFIG', 'SET', $key, $value); + return $this->processBooleanResponse(); + } else { + throw new RedisException('First arg must be GET or SET'); + } + } + + public function info($option = '') { + if ($option) { + $this->processCommand('INFO', $option); + } else { + $this->processCommand('INFO'); + } + return $this->processInfoResponse(); + } + + public function resetStat() { + $this->processCommand('CONFIG', 'RESETSTAT'); + return $this->processBooleanResponse(); + } + + public function slaveOf($host = '', $port = -1) { + if ($host) { + if ($port <= 0) { + $port = self::DEFAULT_PORT; + } + $this->processCommand('SLAVEOF', $host, (int)$port); + } else { + $this->processCommand('SLAVEOF', 'NO', 'ONE'); + } + return $this->processBooleanResponse(); + } + + public function client($cmd, $arg = '') { + if (func_num_args() == 2) { + $this->processCommand('CLIENT', $cmd, $arg); + } else { + $this->processCommand('CLIENT', $cmd); + } + if ($cmd == 'list') { + return $this->processClientListResponse(); + } else { + return $this->processVariantResponse(); + } + } + + /* Strings ------------------------------------------------------------- */ + + public function decr($key, $by = 1) { + if ($by !== 1) { + return $this->decrBy($key, $by); + } + $this->processCommand("DECR", $this->prefix($key)); + return $this->processLongResponse(); + } + + public function decrBy($key, $by) { + if ($by === 1) { + return $this->decr($key); + } + $this->processCommand("DECRBY", $this->prefix($key), (int)$by); + return $this->processLongResponse(); + } + + public function incr($key, $by = 1) { + if ($by !== 1) { + return $this->incrBy($key, $by); + } + $this->processCommand("INCR", $this->prefix($key)); + return $this->processLongResponse(); + } + + public function incrBy($key, $by) { + if ($by === 1) { + return $this->incr($key); + } + $this->processCommand("INCRBY", $this->prefix($key), (int)$by); + return $this->processLongResponse(); + } + + public function incrByFloat($key, $by) { + $this->processCommand("INCRBYFLOAT", $this->prefix($key), + (float)$by); + return $this->processDoubleResponse(); + } + + public function set($key, $value, $expire = -1) { + $key = $this->prefix($key); + $value = $this->serialize($value); + if ($expire > 0) { + $this->processCommand("SETEX", $key, $expire, $value); + } else { + $this->processCommand("SET", $key, $value); + } + return $this->processBooleanResponse(); + } + + /* Keys ---------------------------------------------------------------- */ + + public function sort($key, array $arr = null) { + $args = $this->sortClause($arr, $using_store); + array_unshift($args, $key); + $this->processArrayCommand('SORT', $args); + if ($using_store) { + return $this->processVectorResponse(true); + } else { + return $this->processLongResponse(); + } + } + + public function sortAsc($key, + $pattern = null, + $get = null, + $start = -1, + $count = -1, + $store = null) { + $limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null; + return $this->sort($key, [ + 'by' => $pattern, + 'get' => $get, + 'limit' => $limit, + 'store' => $store, + 'dir' => 'ASC', + ]); + } + + public function sortAscAlpha($key, + $pattern = null, + $get = null, + $start = -1, + $count = -1, + $store = null) { + $limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null; + return $this->sort($key, [ + 'by' => $pattern, + 'get' => $get, + 'limit' => $limit, + 'store' => $store, + 'dir' => 'ASC', + 'alpha' => true, + ]); + } + + public function sortDesc($key, + $pattern = null, + $get = null, + $start = -1, + $count = -1, + $store = null) { + $limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null; + return $this->sort($key, [ + 'by' => $pattern, + 'get' => $get, + 'limit' => $limit, + 'store' => $store, + 'dir' => 'DESC', + ]); + } + + public function sortDescAlpha($key, + $pattern = null, + $get = null, + $start = -1, + $count = -1, + $store = null) { + $limit = (($start > 0) AND ($count > 0)) ? [$start, $count] : null; + return $this->sort($key, [ + 'by' => $pattern, + 'get' => $get, + 'limit' => $limit, + 'store' => $store, + 'dir' => 'DESC', + 'alpha' => true, + ]); + } + + public function object($info, $key) { + $this->processCommand('OBJECT', $info, $this->prefix($key)); + switch ($info) { + case 'refcount': return $this->processLongResponse(); + case 'encoding': return $this->processStringResponse(); + default: return $this->processBooleanResponse(); + } + } + + /* Hashes -------------------------------------------------------------- */ + + public function hMGet($key, array $members) { + $members = array_values($members); + $args = [$this->prefix($key)] + $members; + $this->processArrayCommand('HMGET', $args); + return $this->processAssocResponse($members); + } + + public function hMSet($key, array $pairs) { + $args = [$this->prefix($key)]; + foreach ($pairs as $k => $v) { + $args[] = $k; + $args[] = $this->serialize($v); + } + $this->processArrayCommand('HMSET', $args); + return $this->processBooleanResponse(); + } + + /* zSets --------------------------------------------------------------- */ + + public function zAdd($key, $score, $value/*, $scoreN, $valueN */) { + $args = func_get_args(); + $count = count($args); + if (($count - 1) % 2) { + return false; + } + $args[0] = $this->prefix($args[0]); + for ($i = 1; $i < $count; $i += 2) { + $args[$i ] = (double)$args[$i]; + $args[$i+1] = $this->serialize($args[$i+1]); + } + $this->processArrayCommand('ZADD', $args); + return $this->processLongResponse(); + } + + protected function zInterUnionStore($cmd, + $key, + array $keys, + array $weights = null, + $op = '') { + $args = [ $this->prefix($key), count($keys) ]; + foreach ($keys as $k) { + $args[] = $this->prefix($k); + } + + if ($weights) { + $args[] = 'WEIGHTS'; + foreach ($weights as $weight) { + if (is_int($weight) OR + is_float($weight) OR + ($weight === 'inf') OR + ($weight === '-inf') OR + ($weight === '+inf')) { + $args[] = $weight; + } + } + } + + if ($op) { + $args[] = 'AGGREGATE'; + $args[] = $op; + } + + $this->processArrayCommand($cmd, $args); + return $this->processLongResponse(); + } + + public function zInterStore($key, + array $keys, + array $weights = null, + $op = '') { + return $this->zInterUnionStore('ZINTERSTORE', $key, $keys, $weights, $op); + } + + public function zUnionStore($key, + array $keys, + array $weights = null, + $op = '') { + return $this->zInterUnionStore('ZUNIONSTORE', $key, $keys, $weights, $op); + } + + public function zRange($key, $start, $end, $withscores = false) { + $args = [ + $this->prefix($key), + (int)$start, + (int)$end, + ]; + if ($withscores) { + $args[] = 'WITHSCORES'; + } + $this->processCommand('ZRANGE', $args); + if ($withscores) { + return $this->processMapResponse(true, false); + } else { + return $this->processVectorResponse(true); + } + } + + protected function zRangeByScoreImpl($cmd, + $key, + $start, + $end, + array $opts = null) { + $args = [$this->prefix($key), (int)$start, (int)$end]; + if (isset($opts['limit']) AND + is_array($opts['limit']) AND + (count($opts['limit']) == 2)) { + list($limit_start, $limit_end) = $opts['limit']; + $args[] = 'LIMIT'; + $args[] = $limit_start; + $args[] = $limit_end; + } + if (!empty($opts['withscores'])) { + $args[] = 'WITHSCORES'; + } + $this->processArrayCommand($cmd, $args); + if (!empty($opts['withscores'])) { + return $this->processMapResponse(true, false); + } else { + return $this->processVectorResponse(true); + } + } + + public function zRangeByScore($key, $start, $end, array $opts = null) { + return $this->zRangeByScoreImpl('ZRANGEBYSCORE', + $key, $start, $end, $opts); + } + + public function zRevRangeByScore($key, $start, $end, array $opts = null) { + return $this->zRangeByScoreImpl('ZREVRANGEBYSCORE', + $key, $start, $end, $opts); + } + + public function zRevRange($key, $start, $end, $withscore = false) { + $args = [ + $this->prefix($key), + (int)$start, + (int)$end, + ]; + if ($withscores) { + $args[] = 'WITHSCORES'; + } + $this->processCommand('ZREVRANGE', $args); + if ($withscores) { + return $this->processMapResponse(true, false); + } else { + return $this->processVectorResponse(true); + } + } + + /* Multi --------------------------------------------------------------- */ + + protected function flushCallbacks($multibulk = true) { + if ($multibulk) $this->sockReadData($type); // Response Count + $ret = []; + foreach ($this->multiHandler as $callback) { + $args = isset($callback['args']) ? $callback['args'] : []; + $ret[] = call_user_func_array($callback['cb'], $args); + } + $this->multiHandler = []; + return $ret; + } + + public function multi($mode = self::MULTI) { + if ($mode === self::PIPELINE) { + return $this->pipeline(); + } + if ($mode !== self::MULTI) { + return false; + } + $this->discard(); + $this->processCommand('MULTI'); + $resp = $this->sockReadData($type); + if (($type === self::TYPE_LINE) AND ($resp === 'OK')) { + $this->mode = self::MULTI; + return $this; + } + return false; + } + + public function exec() { + if ($this->mode === self::MULTI) { + $this->mode = self::ATOMIC; + $this->processCommand('EXEC'); + return $this->flushCallbacks(); + } else if ($this->mode === self::PIPELINE) { + $this->mode = self::ATOMIC; + foreach ($this->commands as $cmd) { + $this->processArrayCommand($cmd['cmd'], $cmd['args']); + } + $this->commands = []; + return $this->flushCallbacks(false); + } + } + + public function discard() { + $discard = ($this->mode === self::MULTI); + $this->mode = self::ATOMIC; + $this->commands = []; + $this->multiHandler = []; + if ($discard) { + $this->processCommand('DISCARD'); + return $this->process1Response(); + } + return true; + } + + public function pipeline() { + $this->discard(); + $this->mode = self::PIPELINE; + return $this; + } + + /* Batch --------------------------------------------------------------- */ + + protected function processMSetCommand($cmd, array $data) { + $args = []; + foreach ($data as $key => $val) { + $args[] = $this->prefix($key); + $args[] = $this->serialize($val); + } + $this->processArrayCommand($cmd, $args); + } + + public function mSet(array $data) { + $this->processMSetCommand('MSET', $data); + return $this->processBooleanResponse(); + } + + public function mSetNx(array $data) { + $this->processMSetCommand('MSETNX', $data); + return $this->process1Response(); + } + + /* Scripting ----------------------------------------------------------- */ + + protected function doEval($cmd, $script, array $args, $numKeys) { + foreach($args as &$arg) { + if ($numKeys-- <= 0) break; + $arg = $this->prefix($arg); + } + array_unshift($args, $script); + $this->processArrayCommand($cmd, $args); + return $this->processVariantResponse(); + } + + public function _eval($script, array $args = null, $numKeys = 0) { + return $this->doEval('EVAL', $script, $args, $numKeys); + } + + public function evalSha($sha, array $args = null, $numKeys = 0) { + return $this->doEval('EVALSHA', $sha, $args, $numKeys); + } + + public function script($subcmd/* ... */) { + switch ($subcmd) { + case 'flush': + case 'kill': + $this->processCommand('SCRIPT', $subcmd); + return $this->processVariantResponse(); + case 'load': + if (func_num_args() < 2) { + return false; + } + $script = func_get_arg(1); + if (!is_string($script) OR empty($script)) { + return false; + } + $this->processCommand('SCRIPT', 'load', $script); + return $this->processVariantResponse(); + case 'exists': + $args = func_get_args(); + $args[0] = 'EXISTS'; + $this->processArrayCommand('SCRIPT', $args); + return $this->processVariantResponse(); + default: + return false; + } + } + + /* Introspection ------------------------------------------------------- */ + + public function isConnected() { + return $this->checkConnection(false); + } + + public function getHost() { + return $this->host; + } + + public function getPort() { + return $this->post; + } + + public function getDBNum() { + return $this->dbNumber; + } + + public function getTimeout() { + return $this->getReadTimeout(); + } + + public function getReadTimeout() { + return $this->getOption(self::OPT_READ_TIMEOUT); + } + + public function getPersistentId() { + throw new RedisException('Named persistent connections are '. + 'not supported.'); + } + + public function getPassword() { + return $this->password; + } + + public function getLastError() { + return $this->lastError; + } + + public function clearLastError() { + $this->lastError = ''; + return true; + } + + /* Standard Function Map ----------------------------------------------- */ + + /** + * The majority of the Redis API is implemnted by __call + * which references this list for how the individual command + * should be handled. + * + * By default the name of the method (key in this array) + * is uppercased to become the actual command sent to the redis server. + * + * Example: 'get' becomes the Redis command `GET` + * + * This mapping may be overridden by adding a 'cmd' element such as: + * 'myget' => [ 'cmd' => 'GET' ], + * + * The argument spec is defined by the 'format' subparameter with each + * position in the string specifying what type of param it is. + * 's' => Ordinary string to be passed trough to the server unmodified + * 'k' => The name of a key. Prefixed with $this->prefix. + * 'v' => A piece of user data. Serialized according to $this->serialize. + * 'l' => An integer(long). Explicitly cast from whatever is passed. + * 'd' => A float(double). Explicitly cast from whatever is passed. + * 'b' => A boolean. Explicitly cast from whatever is passed. + * 'p' => Pivot point (self::BEFORE or self::AFTER). Validated. + * + * In lieu of 'format', a mapping may specify 'vararg' for variadic methods. + * The value must be a bitmask of the VAR_* constants. + * See Redis::translateVarArgs() + * + * The method (on this class) called to handle the response is named by the + * 'handler' field. A shortcut for this is the 'return' field which will + * be mapped into 'handler' as: 'process{$return}Response' + * To pass arguments to the handler, use 'retargs'. + * + * Lastly, the 'alias' field (given by itself), will map calls from one + * function directly to another. If the target method actually exists, + * the fcall will be proxied through call_user_func_array(). If the target + * is elsewhere in the map, __call's state will be reset to use the new + * map element. + */ + protected static $map = [ + // Connection + 'open' => [ 'alias' => 'connect' ], + 'popen' => [ 'alias' => 'pconnect' ], + 'ping' => [ 'return' => 'Raw' ], + 'echo' => [ 'format' => 's', 'return' => 'String' ], + + // Server + 'bgrewriteaof' => [ 'return' => 'Boolean' ], + 'bgsave' => [ 'return' => 'Boolean' ], + 'dbsize' => [ 'return' => 'Long' ], + 'flushall' => [ 'return' => 'Boolean' ], + 'flushdb' => [ 'return' => 'Boolean' ], + 'lastsave' => [ 'return' => 'Long' ], + 'save' => [ 'return' => 'Boolean' ], + 'time' => [ 'return' => 'Vector' ], + + // Strings + 'append' => [ 'format' => 'kv', 'return' => 'Long' ], + 'bitcount' => [ 'format' => 'kll', 'return' => 'Long' ], + 'bitop' => [ 'vararg' => self::VAR_KEY_NOT_FIRST, 'return' => 'Long' ], + 'get' => [ 'format' => 'k', 'return' => 'Serialized' ], + 'getbit' => [ 'format' => 'kl', 'return' => 'Long' ], + 'getrange' => [ 'format' => 'kll', 'return' => 'String', 'cmd' => 'RANGE' ], + 'getset' => [ 'format' => 'kv', 'return' => 'Serialized' ], + 'setbit' => [ 'format' => 'klv', 'return' => 'Long' ], + 'setex' => [ 'format' => 'klv', 'return' => 'Boolean' ], + 'psetex' => [ 'format' => 'klv', 'return' => 'Boolean' ], + 'setnx' => [ 'format' => 'kv', 'return' => '1' ], + 'setrange' => [ 'format' => 'klv', 'return' => 'Long' ], + 'strlen' => [ 'format' => 'k', 'return' => 'Long' ], + + // Keys + 'del' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ], + 'delete' => [ 'alias' => 'del' ], + 'dump' => [ 'format' => 'k', 'return' => 'Raw' ], + 'exists' => [ 'format' => 'k', 'return' => '1' ], + 'expire' => [ 'format' => 'kl', 'return' => '1' ], + 'settimeout' => [ 'alias' => 'expire' ], + 'pexpire' => [ 'format' => 'kl', 'return' => '1' ], + 'expireat' => [ 'format' => 'kl', 'return' => '1' ], + 'pexpireat' => [ 'format' => 'kl', 'return' => '1' ], + 'keys' => [ 'format' => 's', 'return' => 'Vector' ], + 'getkeys' => [ 'alias' => 'keys' ], + 'migrate' => [ 'format' => 'slkll', 'return' => 'Boolean' ], + 'move' => [ 'format' => 'kl', 'return' => '1' ], + 'persist' => [ 'format' => 'k', 'return' => '1' ], + 'randomkey' => [ 'return' => 'String' ], + 'rename' => [ 'format' => 'kk', 'return' => 'Boolean' ], + 'renamekey' => [ 'alias' => 'rename' ], + 'renamenx' => [ 'format' => 'kk', 'return' => '1' ], + 'type' => [ 'format' => 'k', 'return' => 'Type' ], + 'ttl' => [ 'format' => 'k', 'return' => 'Long' ], + 'pttl' => [ 'format' => 'k', 'return' => 'Long' ], + 'restore' => [ 'format' => 'kls', 'return' => 'Boolean' ], + + // Hashes + 'hdel' => [ 'vararg' => self::VAR_KEY_FIRST, 'return' => 'Long' ], + 'hexists' => [ 'format' => 'ks', 'return' => '1' ], + 'hget' => [ 'format' => 'ks', 'return' => 'Serialized' ], + 'hgetall' => [ 'format' => 'k', 'return' => 'Map', + 'retargs' => [false,true] ], + 'hincrby' => [ 'format' => 'ksl', 'return' => 'Long' ], + 'hincrbyfloat' => [ 'format' => 'ksd', 'return' => 'Double' ], + 'hkeys' => [ 'format' => 'k', 'return' => 'Vector' ], + 'hlen' => [ 'format' => 'k', 'return' => 'Long' ], + 'hset' => [ 'format' => 'ksv', 'return' => 'Long' ], + 'hsetnx' => [ 'format' => 'ksv', 'return' => '1' ], + 'hvals' => [ 'format' => 'k', 'return' => 'Vector', 'retargs' => [1] ], + + // Lists + 'blpop' => [ 'vararg' => self::VAR_KEY_ALL_AND_TIMEOUT, + 'return' => 'Vector', 'retargs' => [1] ], + 'brpop' => [ 'vararg' => self::VAR_KEY_ALL_AND_TIMEOUT, + 'return' => 'Vector', 'retargs' => [1] ], + 'brpoplpush' => [ 'format' => 'kkl', 'return' => 'Serialized' ], + 'lindex' => [ 'format' => 'kl', 'return' => 'Serialized' ], + 'lget' => [ 'alias' => 'lindex' ], + 'linsert' => [ 'format' => 'kpkv', 'return' => 'Long' ], + 'llen' => [ 'format' => 'k', 'return' => 'Long', 'cmd' => 'LLEN' ], + 'lsize' => [ 'alias' => 'llen' ], + 'lpop' => [ 'format' => 'k', 'return' => 'Serialized' ], + 'lpush' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE, + 'return' => 'Long' ], + 'lpushx' => [ 'format' => 'kl', 'return' => 'Long' ], + 'lrange' => [ 'format' => 'kll', 'return' => 'Vector', 'retargs' => [1] ], + 'lgetrange' => [ 'alias' => 'lrange' ], + 'lrem' => [ 'format' => 'kvl', 'return' => 'Long' ], + 'lremove' => [ 'alias' => 'lrem' ], + 'lset' => [ 'format' => 'klv', 'return' => 'Boolean' ], + 'ltrim' => [ 'format' => 'kll', 'return' => 'Boolean' ], + 'listtrim' => [ 'alias' => 'ltrim' ], + 'rpop' => [ 'format' => 'k', 'return' => 'Serialized' ], + 'rpoplpush' => [ 'format' => 'kk', 'return' => 'Serialized' ], + 'rpush' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE, + 'return' => 'Long' ], + 'rpushx' => [ 'format' => 'kl', 'return' => 'Long' ], + + // Sets + 'sadd' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE, + 'return' => 'Long' ], + 'scard' => [ 'format' => 'k', 'return' => 'Long' ], + 'ssize' => [ 'alias' => 'scard' ], + 'sdiff' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Vector' ], + 'sdiffstore' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ], + 'sinter' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Vector' ], + 'sinterstore' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ], + 'sismember' => [ 'format' => 'kv', 'return' => '1' ], + 'scontains' => [ 'alias' => 'sismember' ], + 'smembers' => [ 'format' => 'k', 'return' => 'Vector' ], + 'sgetmembers' => [ 'alias' => 'smembers' ], + 'smove' => [ 'format' => 'kkv', 'return' => '1' ], + 'spop' => [ 'format' => 'k', 'return' => 'Serialized' ], + 'srandmember' => [ 'format' => 'kl', 'return' => 'Serialized', + 'default' => [ 1 => 1 ] ], + 'srem' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE, + 'return' => 'Long' ], + 'sremove' => [ 'alias' => 'srem' ], + 'sunion' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Vector' ], + 'sunionstore' => [ 'vararg' => self::VAR_KEY_ALL, 'return' => 'Long' ], + + // zSets + 'zcard' => [ 'format' => 'k', 'return' => 'Long' ], + 'zsize' => [ 'alias' => 'zcard' ], + 'zcount' => [ 'format' => 'kss', 'return' => 'Long' ], + 'zincrby' => [ 'format' => 'kdv', 'return' => 'Double' ], + 'zinter' => [ 'alias' => 'zinterstore' ], + 'zunion' => [ 'alias' => 'zunionstore' ], + 'zrank' => [ 'format' => 'kv', 'return' => 'Long' ], + 'zrevrank' => [ 'format' => 'kv', 'return' => 'Long' ], + 'zrem' => [ 'vararg' => self::VAR_KEY_FIRST_AND_SERIALIZE, + 'return' => 'Long' ], + 'zremove' => [ 'alias' => 'zrem' ], + 'zdelete' => [ 'alias' => 'zrem' ], + 'zremrangebyrank' => [ 'format' => 'kll', 'return' => 'Long' ], + 'zdeleterangebyrank' => [ 'alias' => 'zremrangebyrank' ], + 'zremrangebyscore' => [ 'format' => 'kll', 'return' => 'Long' ], + 'zdeleterangebyscore' => [ 'alias' => 'zremrangebyscore' ], + 'zreverserange' => [ 'alias' => 'zrevrange' ], + 'zscore' => [ 'format' => 'kv', 'return' => 'Double' ], + + // Publish + 'publish' => [ 'format' => 'kv', 'return' => 'Long' ], + /* These APIs are listed as "subject to change", avoid for now */ + 'subscribe' => false, + 'psubscribe' => false, + 'unsubscribe' => false, + 'punsubscribe' => false, + + // Introspection + '_prefix' => [ 'alias' => 'prefix' ], + '_unserialize' => [ 'alias' => 'unserialize' ], + + // Batch Ops + 'mget' => [ 'vararg' => self::VAR_KEY_ALL, + 'return' => 'Vector', 'retargs' => [1] ], + 'getmultiple' => [ 'alias' => 'mget' ], + ]; + + + /* Internal Use Only beyond this point --------------------------------- */ + + protected $host = ''; + protected $port = -1; + protected $password = ''; + protected $dbNumber = 0; + protected $last_connect = -1; + protected $retry_interval = 0; + protected $persistent = false; + protected $connection = null; + protected $lastError = ''; + + protected $timeout_connect = 0; + protected $timeout_seconds = 0; + protected $timeout_useconds = 0; + + protected $mode = self::ATOMIC; + protected $multiHandler = []; + protected $commands = []; + protected $prefix = ''; + protected $serializer = self::SERIALIZER_NONE; + + /* protocol ------------------------------------------------------------ */ + + /* Internal use constants for var arg parsing */ + const VAR_KEY_NONE = 0; + const VAR_KEY_FIRST = 1; + const VAR_KEY_NOT_FIRST = 2; + const VAR_KEY_ALL = 3; + const VAR_KEY_MASK = 0x000F; + + const VAR_SERIALIZE = 0x0010; + const VAR_TIMEOUT = 0x0020; + + const VAR_KEY_FIRST_AND_SERIALIZE = 0x0011; + const VAR_KEY_ALL_AND_TIMEOUT = 0x0023; + + /* Returned by reference from Redis::sockReadData() + * Depending on the type of data returned by the server + */ + const TYPE_LINE = '+'; + const TYPE_INT = ':'; + const TYPE_ERR = '-'; + const TYPE_BULK = '$'; + const TYPE_MULTIBULK = '*'; + + protected function checkConnection($auto_reconnect = true) { + if ($this->connection AND !feof($this->connection)) { + // Connection seems fine + return true; + } + + if ((time() - $this->last_connect) < $this->retry_interval) { + // We've tried connecting too recently, don't retry + return false; + } + + if ($auto_reconnect AND + $this->doConnect($this->host, $this->port, + $this->timeout_connect, + null, $this->retry_internal, + $this->persistent)) { + if ($this->password) { + $this->auth($this->password); + } + if ($this->dbNumber) { + $this->select($this->dbNumber); + } + return true; + } + + // Reconnect failed, give up + return false; + } + + protected function sockReadLine() { + if (!$this->checkConnection()) { + return false; + } + $line = fgets($this->connection); + if (substr($line, -2) == "\r\n") { + $line = substr($line, 0, -2); + } + + return $line; + } + + protected function sockReadData(&$type) { + $line = $this->sockReadLine(); + if (strlen($line)) { + $type = $line[0]; + $line = substr($line, 1); + switch ($type) { + case self::TYPE_ERR: + if (!strncmp($line, '-ERR SYNC ', 10)) { + throw new RedisException("Sync with master in progress"); + } + return $line; + case self::TYPE_INT: + case self::TYPE_LINE: + case self::TYPE_MULTIBULK: // Count of elements to follow + return $line; + + case self::TYPE_BULK: + $bytes = (int)$line; + if ($bytes < 0) return null; + $buf = ''; + while (strlen($buf) < ($bytes + 2)) { + $buf .= fread($this->connection, ($bytes + 2) - strlen($buf)); + if (!$this->checkConnection()) { + return null; + } + } + return substr($buf, 0, -2); + + default: + throw new RedisException("protocol error, got '{$type}' ". + "as reply type byte"); + } + } + return null; + } + + /** + * Process arguments for variadic functions based on $flags + * + * Redis::VAR_TIMEOUT indicates that the last argument + * in the list should be treated as an integer timeout + * for the operation + * Redis::VAR_KEY_* indicates which (NONE, FIRST, NOT_FIRST, ALL) + * of the arguments (excluding TIMEOUT, as application) + * should be treated as keys, and thus prefixed with Redis::$prefix + * Redis::VAR_SERIALIZE indicates that all non-timeout/non-key + * fields are data values, and should be serialzed + * (if a serialzied is specified) + */ + protected function translateVarArgs(array $args, $flags) { + // Check alternate vararg schemes first + if (($flags & self::VAR_TIMEOUT) AND + (count($args) == 2) AND + (is_array($args[0])) AND + (is_int($args[1]))) { + $args = $args[0] + [$args[1]]; + } + if ((!($flags & self::VAR_TIMEOUT)) AND + (count($args) == 1) AND + (is_array($args[0]))) { + $args = $args[0]; + } + + // Then prefix, serialie, and cast as needed + if ($flags & self::VAR_TIMEOUT) { + $timeout = array_pop($args); + } + if (($this->prefix AND ($flags & self::VAR_KEY_MASK)) OR + ($flags & self::VAR_SERIALIZE)) { + $first = true; + $varkey = $flags & self::VAR_KEY_MASK; + foreach($args as &$arg) { + if (( $first AND ($varkey == self::VAR_KEY_FIRST)) OR + (!$first AND ($varkey == self::VAR_KEY_NOT_FIRST)) OR + ($varkey == self::VAR_KEY_ALL)) { + $arg = $this->prefix($arg); + } else if ($flags & self::VAR_SERIALIZE) { + $arg = $this->serialize($arg); + } + $first = false; + } + } + if ($flags & self::VAR_TIMEOUT) { + $args[] = (int)$timeout; + } + + return $args; + } + + /** + * Actually send a command to the server. + * assumes all appropriate prefixing and serialization + * has been preformed by the caller and constructs + * a Redis Protocol packet in the form: + * + * *N\r\n + * + * Folled by N instances of: + * + * $L\r\nA + * + * Where L is the length in bytes of argument A. + * + * So for the command `GET somekey` we'd serialize as: + * + * "*2\r\n$3\r\nGET\r\n$7\r\nsomekey\r\n" + */ + protected function processArrayCommand($cmd, array $args) { + if ($this->mode == self::PIPELINE) { + $this->commands[] = [ 'cmd' => $cmd, 'args' => $args ]; + return true; + } + + $clen = strlen($cmd); + $count = count($args) + 1; + $cmd = "*{$count}\r\n\${$clen}\r\n{$cmd}\r\n"; + + while (count($args)) { + $arg = (string)array_shift($args); + $alen = strlen($arg); + $cmd .= "\${$alen}\r\n{$arg}\r\n"; + } + + if (!$this->checkConnection()) { + return false; + } + return (bool)fwrite($this->connection, $cmd); + } + + protected function processCommand($cmd, /* ... */) { + $args = func_get_args(); + array_shift($args); + return $this->processArrayCommand($cmd, $args); + } + + protected function serialize($str) { + switch ($this->serializer) { + case self::SERIALIZER_NONE: + return $str; + case self::SERIALIZER_PHP: + return serialize($str); + case self::SERIALIZER_IGBINARY: + default: + throw new RedisException("Not Iplemented"); + } + } + + protected function unserialize($str) { + switch ($this->serializer) { + case self::SERIALIZER_NONE: + return $str; + case self::SERIALIZER_PHP: + return unserialize($str); + case self::SERIALIZER_IGBINARY: + default: + throw new RedisException("Not Implemented"); + } + } + + protected function processVariantResponse() { + if ($this->mode === self::ATOMIC) { + return $this->sockReadData($type); + } + $this->multiHandler[] = [ 'cb' => [$this,'processVariantResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processClientListResponse() { + if ($this->mode !== self::ATOMIC) { + $this->multiHandler[] = [ 'cb' => [$this,'processClientListResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + $resp = $this->sockReadData($type); + if (($type !== self::TYPE_LINE) AND + ($type !== self::TYPE_BULK)) { + return null; + } + $ret = []; + $pairs = explode(' ', trim($resp)); + foreach ($pairs as $pair) { + $kv = explode('=', $pair, 2); + if (count($kv) == 1) { + $ret[] = $pair; + } else { + list($k, $v) = $kv; + $ret[$k] = $v; + } + } + return $ret; + } + + protected function processSerializedResponse() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + return (($type === self::TYPE_LINE) OR ($type === self::TYPE_BULK)) + ? $this->unserialize($resp) : null; + } + $this->multiHandler[] = [ 'cb' => [$this,'processSerializedResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processBooleanResponse() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + return ($type === self::TYPE_LINE) AND ($resp === 'OK'); + } + $this->multiHandler[] = [ 'cb' => [$this,'processBooleanResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processLongResponse() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + return ($type === self::TYPE_INT) ? ((int)$resp) : null; + } + $this->multiHandler[] = [ 'cb' => [$this,'processLongResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processDoubleResponse() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + return ($type === self::TYPE_INT) ? ((float)$resp) : null; + } + $this->multiHandler[] = [ 'cb' => [$this,'processDoubleResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processStringResponse() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + return (($type === self::TYPE_LINE) OR ($type === self::TYPE_BULK)) + ? ((string)$resp) : null; + } + $this->multiHandler[] = [ 'cb' => [$this,'processStringResponse'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processVectorResponse($unser = 0) { + if ($this->mode !== self::ATOMIC) { + $this->multiHandler[] = [ 'cb' => [$this, 'processVectorResponse'], + 'args' => [$unser] + ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + $count = $this->sockReadData($type); + if ($type !== self::TYPE_MULTIBULK) { + return null; + } + + $ret = []; + $lineNo = 0; + while($count--) { + $lineNo++; + $val = $this->sockReadData($type); + if ($unser AND (($lineNo % $unser) == 0)) { + $val = $this->unserialize($val); + } + $ret[] = $val; + } + return $ret; + } + + protected function processMapResponse($unser_key, $unser_val = true) { + if ($this->mode !== self::ATOMIC) { + $this->multiHandler[] = [ 'cb' => [$this, 'processMapResponse'], + 'args' => [$unser_key,$unser_val] + ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + $count = $this->sockReadData($type); + if ($type !== self::TYPE_MULTIBULK) { + return null; + } + + $ret = []; + while($count > 1) { + $key = $this->sockReadData($type); + if ($unser_key) { + $key = $this->unserialize($key); + } + $val = $this->sockReadData($type); + if ($unser_val) { + $val = $this->unserialize($val); + } + $ret[$key] = $val; + $count -= 2; + } + if ($count > 1) { + $ret[$this->sockReadData($type)] = null; + } + return $ret; + } + + protected function processAssocResponse(array $keys, $unser_val = true) { + if ($this->mode !== self::ATOMIC) { + $this->multiHandler[] = [ 'cb' => [$this, 'processAssocResponse'], + 'args' => [$keys, $unser_val] + ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + $count = $this->sockReadData($type); + if ($type !== self::TYPE_MULTIBULK) { + return null; + } + + $ret = []; + while($count--) { + $key = array_shift($keys); + $val = $this->sockReadLine(); + if ($unser_val) { + $val = $this->unserialize($val); + } + $ret[$key] = $val; + } + return $ret; + } + + protected function process1Response() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + return ($type === self::TYPE_INT) && ($resp === '1'); + } + $this->multiHandler[] = [ 'cb' => [$this,'process1Response'] ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processTypeResponse() { + if ($this->mode === self::ATOMIC) { + $resp = $this->sockReadData($type); + if ($type !== self::TYPE_LINE) { + return self::REDIS_NOT_FOUND; + } + switch($resp) { + case 'string': return self::REDIS_STRING; + case 'set': return self::REDIS_SET; + case 'list': return self::REDIS_LIST; + case 'zset': return self::REDIS_ZSET; + case 'hash': return self::REDIS_HASH; + default: return self::REDIS_NOT_FOUND; + } + } + $this->multiHandler[] = [ 'cb' => 'processTypeResponse' ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processRawResponse() { + if ($this->mode === self::ATOMIC) { + return $this->sockReadLine(); + } + $this->multiHandler[] = [ 'cb' => 'processRawResponse' ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + + protected function processInfoResponse() { + if ($this->mode !== self::ATOMIC) { + $this->multiHandler[] = [ 'cb' => 'processInfoResponse' ]; + if (($this->mode === self::MULTI) && !$this->processQueuedResponse()) { + return false; + } + return $this; + } + $resp = $this->sockReadData($type); + if (($type !== self::TYPE_LINE) AND ($type !== self::TYPE_BULK)) { + return false; + } + + $ret = []; + $lines = preg_split('/[\r\n]+/', $resp); + foreach ($lines as $line) { + if ((substr($line, 0, 1) == '#') OR + !trim($line)) { + continue; + } + $colon = strpos($line, ':'); + if ($colon === false) { + break; + } + list($key, $val) = explode(':', $line, 2); + $ret[$key] = $val; + } + return $ret; + } + + protected function processQueuedResponse() { + $resp = $this->sockReadData($type); + return ($type === self::TYPE_LINE) AND ($resp === 'QUEUED'); + } + + protected function prefix($key) { + return $this->prefix . $key; + } + + /** + * Dispatches all commands in the Redis::$map list + * + * All other commands are handled by explicit implementations + */ + public function __call($fname, $args) { + $fname = strtolower($fname); + if (!isset(self::$map[$fname])) { + error_log("Call to undefined function Redis::$fname", E_USER_ERROR); + return null; + } + $func = self::$map[$fname]; + if ($func === false) { + throw new RedisException("Redis::$fname() is currently unimplemented"); + } + + // Normalize record + if (!empty($func['alias'])) { + if (isset(self::$map[$func['alias']])) { + $fname = $func['alias']; + $func = self::$map[$fname]; + } else { + return call_user_func_array([$this,$func['alias']],func_get_args()); + } + } + if (empty($func['format'])) { + $func['format'] = isset($func['vararg']) ? '...' : ''; + } + if (empty($func['cmd'])) { + $func['cmd'] = strtoupper($fname); + } + if (empty($func['handler'])) { + $func['handler'] = empty($func['return']) + ? null : "process{$func['return']}Response"; + } + if (empty($func['retargs'])) { + $func['retargs'] = []; + } + + $format = $func['format']; + $argc = count($args); + + if ($format == '...') { + $args = $this->translateVarArgs($args, $func['vararg']); + $this->processArrayCommand($func['cmd'], $args); + if (empty($func['handler'])) { + return null; + } + return call_user_func_array([$this, $func['handler']], $func['retargs']); + } + + $flen = strlen($format); + for ($i = 0; $i < $flen; $i++) { + if (!isset($args[$i])) { + if (isset($func['defaults']) AND + array_key_exists($func['defaults'], $i)) { + $args[$i] = $func['defualts'][$i]; + } else { + error_log("Redis::$fname requires at least $flen parameters ". + "$argc given", E_USER_ERROR); + return null; + } + } + switch ($format[$i]) { + case 'k': $args[$i] = $this->prefix($args[$i]); break; + case 'v': $args[$i] = $this->serialize($args[$i]); break; + case 's': $args[$i] = (string)$args[$i]; break; + case 'l': $args[$i] = (int)$args[$i]; break; + case 'd': $args[$i] = (float)$args[$i]; break; + case 'b': $args[$i] = (bool)$arts[$i]; break; + case 'p': + if (($args[$i] !== self::BEFORE) AND ($args[$i] !== self::AFTER)) { + error_log("Argument $i to Redis::$fname must be ". + "'before' or 'after'", E_USER_ERROR); + return null; + } break; + } + } + $this->processArrayCommand($func['cmd'], $args); + if (empty($func['handler'])) { + return null; + } + return call_user_func_array([$this, $func['handler']], $func['retargs']); + } + + /* --------------------------------------------------------------------- */ + + protected function doConnect($host, + $port, + $timeout, + $persistent_id, + $retry_interval, + $persistent = false) { + if (!empty($persistent_id)) { + throw new RedisException("Named persistent connections not supported"); + } + + if (($port <= 0) && (substr($host, 0, 1) != '/')) { + $port = self::DEFAULT_PORT; + } + + if ($persistent) { + $conn = pfsockopen($host, $port, $errno, $errstr, $timeout); + } else { + $conn = fsockopen($host, $port, $errno, $errstr, $timeout); + } + $this->last_connect = time(); + $this->host = $host; + $this->port = $port; + $this->retry_interval = $retry_interval; + $this->timeout_connect = $timeout; + $this->persistent = $persistent; + $this->connection = $conn; + $this->dbNumber = 0; + $this->commands = []; + $this->multiHandler = []; + $this->mode = self::ATOMIC; + + if (!$conn) { + error_log("Failed connecting to redis server at {$host}: {$errstr}", + E_USER_WARNING); + return false; + } + stream_set_blocking($conn, true); + stream_set_timeout($conn, $this->timeout_seconds, $this->timeout_useconds); + + return true; + } + + protected function sortClause(array $arr, &$using_store) { + $using_store = false; + if (!$arr) { + return []; + } + + $ret = []; + foreach(['by','sort','store','get','alpha','limit','dir'] as $k) { + if (isset($arr[$k])) { + $v = $arr[$k]; + } else if (isset($arr[strtoupper($k)])) { + $v = $arr[strtoupper($k)]; + } else { + continue; + } + + if (($k == 'get') AND is_array($v)) { + foreach ($v as $val) { + $ret[] = 'GET'; + $ret[] = $val; + } + continue; + } + + if ($k == 'alpha') { + if ($v === true) { + $ret[] = 'ALPHA'; + } + continue; + } + + if ($k == 'limit') { + if (is_array($val) AND (count($val) == 2)) { + list($off, $cnt) = $val; + $ret[] = 'LIMIT'; + $ret[] = $off; + $ret[] = $cnt; + } + continue; + } + + if ($k == 'store') { + $using_store = true; + } + if ($k == 'dir') { + $ret[] = strtoupper($v); + continue; + } + + $ret[] = strtoupper($k); + $ret[] = $v; + } + + return $ret; + } + + public function __destruct() { + + } +} diff --git a/hphp/system/php/redis/RedisException.php b/hphp/system/php/redis/RedisException.php new file mode 100644 index 000000000..01cc37b86 --- /dev/null +++ b/hphp/system/php/redis/RedisException.php @@ -0,0 +1,3 @@ +setOption(Redis::OPT_PREFIX, GetTestKeyName(__FILE__) . ':'); + +$r->set("A", "\x01\x02\x03\x04"); +$r->set("B", "\x01\x01\x01\x01"); + +foreach (['AND', 'OR', 'XOR'] as $op) { + $r->bitop($op, 'C', 'A', 'B'); + var_dump(bin2hex($r->get('C'))); +} + +$r->bitop('NOT', 'C', 'A'); +var_Dump(bin2hex($r->get('C'))); + + +$r->delete('A', 'B', 'C'); diff --git a/hphp/test/slow/ext_redis/bitop.php.expect b/hphp/test/slow/ext_redis/bitop.php.expect new file mode 100644 index 000000000..61bf42a39 --- /dev/null +++ b/hphp/test/slow/ext_redis/bitop.php.expect @@ -0,0 +1,4 @@ +string(8) "01000100" +string(8) "01030305" +string(8) "00030205" +string(8) "fefdfcfb" diff --git a/hphp/test/slow/ext_redis/config.skipif b/hphp/test/slow/ext_redis/config.skipif new file mode 100644 index 000000000..5aacf3c27 --- /dev/null +++ b/hphp/test/slow/ext_redis/config.skipif @@ -0,0 +1,5 @@ +echo("This is a test")); + +$r->multi(Redis::MULTI); +var_dump($r->echo("This is a multi test") instanceof Redis); +var_dump($r->exec()); + +$r->multi(Redis::PIPELINE); +var_dump($r->echo("This is a pipeline test") instanceof Redis); +var_dump($r->exec()); diff --git a/hphp/test/slow/ext_redis/echo.php.expect b/hphp/test/slow/ext_redis/echo.php.expect new file mode 100644 index 000000000..e3db10783 --- /dev/null +++ b/hphp/test/slow/ext_redis/echo.php.expect @@ -0,0 +1,11 @@ +string(14) "This is a test" +bool(true) +array(1) { + [0]=> + string(20) "This is a multi test" +} +bool(true) +array(1) { + [0]=> + string(23) "This is a pipeline test" +} diff --git a/hphp/test/slow/ext_redis/hsetget.php b/hphp/test/slow/ext_redis/hsetget.php new file mode 100644 index 000000000..e8a2c544c --- /dev/null +++ b/hphp/test/slow/ext_redis/hsetget.php @@ -0,0 +1,19 @@ +setOption(Redis::OPT_PREFIX, GetTestKeyName(__FILE__) . ':'); +$r->delete('A'); + +$r->hSet('A', 'foo', 'bar'); +var_dump($r->hLen('A')); +$r->hSet('A', 'baz', 'boom'); +var_dump($r->hLen('A')); +var_dump($r->hKeys('A')); +var_dump($r->hVals('A')); +var_dump($r->hGetAll('A')); +var_dump($r->hGet('A', 'foo')); +var_dump($r->hGet('A', 'baz')); + +$r->delete('A'); diff --git a/hphp/test/slow/ext_redis/hsetget.php.expect b/hphp/test/slow/ext_redis/hsetget.php.expect new file mode 100644 index 000000000..5757e575b --- /dev/null +++ b/hphp/test/slow/ext_redis/hsetget.php.expect @@ -0,0 +1,22 @@ +int(1) +int(2) +array(2) { + [0]=> + string(3) "foo" + [1]=> + string(3) "baz" +} +array(2) { + [0]=> + string(3) "bar" + [1]=> + string(4) "boom" +} +array(2) { + ["foo"]=> + string(3) "bar" + ["baz"]=> + string(4) "boom" +} +string(3) "bar" +string(4) "boom" diff --git a/hphp/test/slow/ext_redis/info.php b/hphp/test/slow/ext_redis/info.php new file mode 100644 index 000000000..cb60d5f24 --- /dev/null +++ b/hphp/test/slow/ext_redis/info.php @@ -0,0 +1,10 @@ +info(); +var_dump(is_array($info)); +var_dump(count($info) > 35); +var_dump($info['tcp_port'] == REDIS_PORT); +$r->client('setname', 'hhvm-redis-client'); +var_dump($r->client('getname')); diff --git a/hphp/test/slow/ext_redis/info.php.expect b/hphp/test/slow/ext_redis/info.php.expect new file mode 100644 index 000000000..876217555 --- /dev/null +++ b/hphp/test/slow/ext_redis/info.php.expect @@ -0,0 +1,6 @@ +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(17) "hhvm-redis-client" diff --git a/hphp/test/slow/ext_redis/list.php b/hphp/test/slow/ext_redis/list.php new file mode 100644 index 000000000..5edc8f9de --- /dev/null +++ b/hphp/test/slow/ext_redis/list.php @@ -0,0 +1,20 @@ +setOption(Redis::OPT_PREFIX, GetTestKeyName(__FILE__) . ':'); +$r->delete('A'); + +$r->lpush('A', "abc", "easy as 123", "apple", "pear"); +var_dump($r->lSize('A')); +var_dump($r->lRange('A', 1, 3)); +var_dump($r->lpop('A')); +var_dump($r->rpop('A')); +var_dump($r->lSize('A')); + +var_dump($r->rpoplpush('A', 'B')); +var_dump($r->lget('A', 0)); +var_dump($r->lget('B', 0)); + +$r->delete('A', 'B'); diff --git a/hphp/test/slow/ext_redis/list.php.expect b/hphp/test/slow/ext_redis/list.php.expect new file mode 100644 index 000000000..c83621e52 --- /dev/null +++ b/hphp/test/slow/ext_redis/list.php.expect @@ -0,0 +1,15 @@ +int(4) +array(3) { + [0]=> + string(5) "apple" + [1]=> + string(11) "easy as 123" + [2]=> + string(3) "abc" +} +string(4) "pear" +string(3) "abc" +int(2) +string(11) "easy as 123" +string(5) "apple" +string(11) "easy as 123" diff --git a/hphp/test/slow/ext_redis/phpserialize.php b/hphp/test/slow/ext_redis/phpserialize.php new file mode 100644 index 000000000..ba4264233 --- /dev/null +++ b/hphp/test/slow/ext_redis/phpserialize.php @@ -0,0 +1,15 @@ +setOption(Redis::OPT_PREFIX, GetTestKeyName(__FILE__) . ':'); +$r->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); + +foreach ([null, true, false, 123, 456.0, + "A string of words", [1,2,3]] as $val) { + $r->set('A', $val); + var_dump($r->get('A')); +} + +$r->delete('A'); diff --git a/hphp/test/slow/ext_redis/phpserialize.php.expect b/hphp/test/slow/ext_redis/phpserialize.php.expect new file mode 100644 index 000000000..17c3e1177 --- /dev/null +++ b/hphp/test/slow/ext_redis/phpserialize.php.expect @@ -0,0 +1,14 @@ +NULL +bool(true) +bool(false) +int(123) +float(456) +string(17) "A string of words" +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} diff --git a/hphp/test/slow/ext_redis/prefix.php b/hphp/test/slow/ext_redis/prefix.php new file mode 100644 index 000000000..af52b735b --- /dev/null +++ b/hphp/test/slow/ext_redis/prefix.php @@ -0,0 +1,12 @@ +setOption(Redis::OPT_PREFIX, "{$key}:")); +var_dump($r->set("foo", "bar{$key}baz")); +var_dump($r->setOption(Redis::OPT_PREFIX, "")); +var_dump($r->get("{$key}:foo") == "bar{$key}baz"); +var_dump($r->delete("{$key}:foo")); diff --git a/hphp/test/slow/ext_redis/prefix.php.expect b/hphp/test/slow/ext_redis/prefix.php.expect new file mode 100644 index 000000000..c6f940f5a --- /dev/null +++ b/hphp/test/slow/ext_redis/prefix.php.expect @@ -0,0 +1,5 @@ +bool(true) +bool(true) +bool(true) +bool(true) +int(1) diff --git a/hphp/test/slow/ext_redis/redis.inc b/hphp/test/slow/ext_redis/redis.inc new file mode 100644 index 000000000..c0de57167 --- /dev/null +++ b/hphp/test/slow/ext_redis/redis.inc @@ -0,0 +1,27 @@ +connect(REDIS_HOST, REDIS_PORT); + if ($status) var_dump($conn); + $authok = REDIS_PASS ? $r->auth(REDIS_PASS) : true; + if ($status) var_dump($authok); + return $r; +} + +function GetTestKeyName($test, $rand = false) { + $name = 'REDIS_TEST:' . preg_replace('/[^a-zA-Z0-9]/', '-', $test); + if ($rand) $name .= ':' . rand(0,1000000); + return $name; +} diff --git a/hphp/test/slow/ext_redis/set.php b/hphp/test/slow/ext_redis/set.php new file mode 100644 index 000000000..55262883f --- /dev/null +++ b/hphp/test/slow/ext_redis/set.php @@ -0,0 +1,17 @@ +setOption(Redis::OPT_PREFIX, GetTestKeyName(__FILE__) . ':'); + +$r->delete('colors','reds'); +var_dump($r->sAdd('colors', 'red', 'orange', 'yellow', + 'green', 'blue', 'violet')); +var_dump($r->sSize('colors')); +var_dump($r->sContains('colors', 'red')); +var_dump($r->sContains('colors', 'aqua')); + +var_dump($r->sAdd('reds', 'red', 'pink', 'crimson')); +var_dump($r->sInterStore('red-colors', 'colors', 'reds')); +var_dump($r->sMembers('red-colors')); diff --git a/hphp/test/slow/ext_redis/set.php.expect b/hphp/test/slow/ext_redis/set.php.expect new file mode 100644 index 000000000..83cbc6a23 --- /dev/null +++ b/hphp/test/slow/ext_redis/set.php.expect @@ -0,0 +1,10 @@ +int(6) +int(6) +bool(true) +bool(false) +int(3) +int(1) +array(1) { + [0]=> + string(3) "red" +} diff --git a/hphp/test/slow/ext_redis/setgetexistsdelete.php b/hphp/test/slow/ext_redis/setgetexistsdelete.php new file mode 100644 index 000000000..739b9f0d9 --- /dev/null +++ b/hphp/test/slow/ext_redis/setgetexistsdelete.php @@ -0,0 +1,39 @@ +delete($key); + +echo "Atomic\n"; +var_dump($r->exists($key)); +var_dump($r->set($key, "The quick brown fox jumped over the lazy dog.")); +var_dump($r->exists($key)); +var_dump($r->get($key)); +var_dump($r->strlen($key)); +var_dump($r->delete($key)); +var_dump($r->exists($key)); + +echo "Multi\n"; +var_dump($r->multi() + ->exists($key) + ->set($key, "The quick brown fox jumped over the lazy dog.") + ->exists($key) + ->get($key) + ->strlen($key) + ->delete($key) + ->exists($key) + ->exec()); + +echo "Pipeline\n"; +var_dump($r->pipeline() + ->exists($key) + ->set($key, "The quick brown fox jumped over the lazy dog.") + ->exists($key) + ->get($key) + ->strlen($key) + ->delete($key) + ->exists($key) + ->exec()); + diff --git a/hphp/test/slow/ext_redis/setgetexistsdelete.php.expect b/hphp/test/slow/ext_redis/setgetexistsdelete.php.expect new file mode 100644 index 000000000..0cc1bd4ac --- /dev/null +++ b/hphp/test/slow/ext_redis/setgetexistsdelete.php.expect @@ -0,0 +1,42 @@ +Atomic +bool(false) +bool(true) +bool(true) +string(45) "The quick brown fox jumped over the lazy dog." +int(45) +int(1) +bool(false) +Multi +array(7) { + [0]=> + bool(false) + [1]=> + bool(true) + [2]=> + bool(true) + [3]=> + string(45) "The quick brown fox jumped over the lazy dog." + [4]=> + int(45) + [5]=> + int(1) + [6]=> + bool(false) +} +Pipeline +array(7) { + [0]=> + bool(false) + [1]=> + bool(true) + [2]=> + bool(true) + [3]=> + string(45) "The quick brown fox jumped over the lazy dog." + [4]=> + int(45) + [5]=> + int(1) + [6]=> + bool(false) +} diff --git a/hphp/test/slow/ext_redis/time.php b/hphp/test/slow/ext_redis/time.php new file mode 100644 index 000000000..8cb578974 --- /dev/null +++ b/hphp/test/slow/ext_redis/time.php @@ -0,0 +1,12 @@ +time(); +var_dump(count($tm)); +$tm = $tm[0] + ($tm[1] / 1000000); +$now = microtime(true); +$delta = abs($now - $tm); +// Server should be within five seconds of client +var_dump($delta < 5); diff --git a/hphp/test/slow/ext_redis/time.php.expect b/hphp/test/slow/ext_redis/time.php.expect new file mode 100644 index 000000000..508f8c95f --- /dev/null +++ b/hphp/test/slow/ext_redis/time.php.expect @@ -0,0 +1,2 @@ +int(2) +bool(true)