Update SCAN to handle very large cursor values.

Technically Redis may return any unsigned 64 bit integer as a scan
cursor.  This presents a problem for PHP in that PHP's integers are
signed.  Because of that if a scan cursor is > 2^63 it will overflow and
fail to work properly.

This commit updates our SCAN family of commands to deliver cursors in
their string form.

```php
public function scan(null|int|string $iterator, ...);
```

On initial entry into our SCAN family we convert either a NULL or empty
string cursor to zero, and send the initial scan command.

As Redis replies with cursors we either represent them as a long (if
they are <= ZEND_ULONG_MAX) and as a string if greater.  This should
mean the fix is minimally breaking as the following code will still
work:

```php
$it = NULL;
do {
    print_r($redis->scan($it));
} while ($it !== 0);
```

The `$it !== 0` still works because the zero cursor will be represented
as an integer.  Only absurdly large (> 2^63) values are represented as a
string.

Fixes #2454
This commit is contained in:
michael-grunder
2024-03-14 21:50:28 -07:00
committed by Michael Grunder
parent fa1a283ac9
commit e52f0afaed
10 changed files with 79 additions and 45 deletions

View File

@@ -401,7 +401,7 @@ redis_check_eof(RedisSock *redis_sock, zend_bool no_retry, zend_bool no_throw)
PHP_REDIS_API int
redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
REDIS_SCAN_TYPE type, zend_long *iter)
REDIS_SCAN_TYPE type, uint64_t *cursor)
{
REDIS_REPLY_TYPE reply_type;
long reply_info;
@@ -434,7 +434,7 @@ redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
}
/* Push the iterator out to the caller */
*iter = atol(p_iter);
*cursor = strtoull(p_iter, NULL, 10);
efree(p_iter);
/* Read our actual keys/members/etc differently depending on what kind of

View File

@@ -100,7 +100,7 @@ PHP_REDIS_API int redis_mbulk_reply_zipped_keys_dbl(INTERNAL_FUNCTION_PARAMETERS
PHP_REDIS_API int redis_mbulk_reply_assoc(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
PHP_REDIS_API int redis_mbulk_reply_double(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, zend_long *iter);
PHP_REDIS_API int redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, REDIS_SCAN_TYPE type, uint64_t *cursor);
PHP_REDIS_API int redis_xrange_reply(INTERNAL_FUNCTION_PARAMETERS,

74
redis.c
View File

@@ -2744,24 +2744,60 @@ redis_build_scan_cmd(char **cmd, REDIS_SCAN_TYPE type, char *key, int key_len,
return cmdstr.len;
}
/* Update a zval with the current 64 bit scan cursor. This presents a problem
* because we can only represent up to 63 bits in a PHP integer. So depending
* on the cursor value, we may need to represent it as a string. */
static void updateScanCursorZVal(zval *zv, uint64_t cursor) {
char tmp[21];
size_t len;
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
Z_TYPE_P(zv) == IS_STRING));
if (Z_TYPE_P(zv) == IS_STRING)
zend_string_release(Z_STR_P(zv));
if (cursor > ZEND_LONG_MAX) {
len = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)cursor);
ZVAL_STRINGL(zv, tmp, len);
} else {
ZVAL_LONG(zv, cursor);
}
}
static uint64_t getScanCursorZVal(zval *zv, zend_bool *was_zero) {
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
Z_TYPE_P(zv) == IS_STRING));;
if (Z_TYPE_P(zv) == IS_STRING) {
*was_zero = Z_STRLEN_P(zv) == 1 && Z_STRVAL_P(zv)[0] == '0';
return strtoull(Z_STRVAL_P(zv), NULL, 10);
} else {
*was_zero = Z_LVAL_P(zv) == 0;
return Z_LVAL_P(zv);
}
}
/* {{{ proto redis::scan(&$iterator, [pattern, [count, [type]]]) */
PHP_REDIS_API void
generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
zval *object, *z_iter;
zval *object, *z_cursor;
RedisSock *redis_sock;
HashTable *hash;
char *pattern = NULL, *cmd, *key = NULL;
int cmd_len, num_elements, key_free = 0, pattern_free = 0;
size_t key_len = 0, pattern_len = 0;
zend_string *match_type = NULL;
zend_long count = 0, iter;
zend_long count = 0;
zend_bool completed;
uint64_t cursor;
/* 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(), getThis(),
"Os!z/|s!l", &object, redis_ce, &key,
&key_len, &z_iter, &pattern,
&key_len, &z_cursor, &pattern,
&pattern_len, &count)==FAILURE)
{
RETURN_FALSE;
@@ -2769,7 +2805,7 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
} else {
// Doesn't require a key
if(zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(),
"Oz/|s!lS!", &object, redis_ce, &z_iter,
"Oz/|s!lS!", &object, redis_ce, &z_cursor,
&pattern, &pattern_len, &count, &match_type)
== FAILURE)
{
@@ -2789,19 +2825,17 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
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
// separated 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);
/* If our cursor is NULL (it can only be null|int|string), convert it to a
* long and initialize it to zero for oure initial SCAN. Otherwise et the
* uint64_t value from the zval which can either be in the form of a long or
* a string (if the cursor is too large to fit in a zend_long). */
if (Z_TYPE_P(z_cursor) == IS_NULL) {
convert_to_long(z_cursor);
cursor = 0;
} else {
/* We're done, back to iterator zero */
RETURN_FALSE;
cursor = getScanCursorZVal(z_cursor, &completed);
if (completed)
RETURN_FALSE;
}
/* Prefix our key if we've got one and we have a prefix set */
@@ -2830,13 +2864,13 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
}
// Format our SCAN command
cmd_len = redis_build_scan_cmd(&cmd, type, key, key_len, (long)iter,
cmd_len = redis_build_scan_cmd(&cmd, type, key, key_len, (long)cursor,
pattern, pattern_len, count, match_type);
/* 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)
redis_sock,type, &cursor) < 0)
{
if(key_free) efree(key);
RETURN_FALSE;
@@ -2845,7 +2879,7 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
/* 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 &&
} while (redis_sock->scan & REDIS_SCAN_RETRY && cursor != 0 &&
num_elements == 0);
/* Free our pattern if it was prefixed */
@@ -2855,7 +2889,7 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
if(key_free) efree(key);
/* Update our iterator reference */
Z_LVAL_P(z_iter) = iter;
updateScanCursorZVal(z_cursor, cursor);
}
PHP_METHOD(Redis, scan) {

View File

@@ -1880,7 +1880,7 @@ class Redis {
* }
* } while ($it != 0);
*/
public function hscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): Redis|array|bool;
public function hscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): Redis|array|bool;
/**
* Increment a key's value, optionally by a specific amount.
@@ -2922,7 +2922,7 @@ class Redis {
* }
* }
*/
public function scan(?int &$iterator, ?string $pattern = null, int $count = 0, ?string $type = null): array|false;
public function scan(null|int|string &$iterator, ?string $pattern = null, int $count = 0, ?string $type = null): array|false;
/**
* Retrieve the number of members in a Redis set.
@@ -3312,7 +3312,7 @@ class Redis {
* }
* }
*/
public function sscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): array|false;
public function sscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): array|false;
/**
* Subscribes the client to the specified shard channels.
@@ -4571,7 +4571,7 @@ class Redis {
* NOTE: See Redis::scan() for detailed example code on how to call SCAN like commands.
*
*/
public function zscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): Redis|array|false;
public function zscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): Redis|array|false;
/**
* Retrieve the union of one or more sorted sets

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 685a816f4e46c30d8a9ae787948c69d8a20213b1 */
* Stub hash: d6839707b66ecf4460374deea10a528bf0c5ea41 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null")
@@ -458,7 +458,7 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_hscan, 0, 2, Redis, MAY_BE_ARRAY|MAY_BE_BOOL)
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()
@@ -757,7 +757,7 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Redis_save arginfo_class_Redis_bgSave
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_Redis_scan, 0, 1, MAY_BE_ARRAY|MAY_BE_FALSE)
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_STRING, 1, "null")
@@ -850,7 +850,7 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_Redis_sscan, 0, 2, MAY_BE_ARRAY|MAY_BE_FALSE)
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()
@@ -1150,7 +1150,7 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_zscan, 0, 2, Redis, MAY_BE_ARRAY|MAY_BE_FALSE)
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()

View File

@@ -1123,11 +1123,11 @@ ra_generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, const char *kw, int kw_len)
{
RedisArray *ra;
zend_string *key, *pattern = NULL;
zval *object, *redis_inst, *z_iter, z_fun, z_args[4];
zval *object, *redis_inst, *z_cursor, z_fun, z_args[4];
zend_long count = 0;
if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "OSz/|S!l",
&object, redis_array_ce, &key, &z_iter, &pattern, &count) == FAILURE) {
&object, redis_array_ce, &key, &z_cursor, &pattern, &count) == FAILURE) {
RETURN_FALSE;
}
@@ -1141,7 +1141,7 @@ ra_generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, const char *kw, int kw_len)
}
ZVAL_STR(&z_args[0], key);
ZVAL_NEW_REF(&z_args[1], z_iter);
ZVAL_NEW_REF(&z_args[1], z_cursor);
if (pattern) ZVAL_STR(&z_args[2], pattern);
ZVAL_LONG(&z_args[3], count);
@@ -1149,7 +1149,7 @@ ra_generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, const char *kw, int kw_len)
call_user_function(&redis_ce->function_table, redis_inst, &z_fun, return_value, ZEND_NUM_ARGS(), z_args);
zval_dtor(&z_fun);
ZVAL_ZVAL(z_iter, &z_args[1], 0, 1);
ZVAL_ZVAL(z_cursor, &z_args[1], 0, 1);
}
PHP_METHOD(RedisArray, hscan)

View File

@@ -40,7 +40,7 @@ class RedisArray {
public function getOption(int $opt): bool|array;
public function hscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): bool|array;
public function hscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): bool|array;
public function info(): bool|array;
@@ -56,17 +56,17 @@ class RedisArray {
public function save(): bool|array;
public function scan(?int &$iterator, string $node, ?string $pattern = null, int $count = 0): bool|array;
public function scan(null|int|string &$iterator, string $node, ?string $pattern = null, int $count = 0): bool|array;
public function select(int $index): bool|array;
public function setOption(int $opt, string $value): bool|array;
public function sscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): bool|array;
public function sscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): bool|array;
public function unlink(string|array $key, string ...$otherkeys): bool|int;
public function unwatch(): bool|null;
public function zscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): bool|array;
public function zscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): bool|array;
}

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 59943eeb14b3ed78f88117e6923d64a95911b5ff */
* Stub hash: ddb92422452cb767a7d6694aa8ac60d883db6672 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_RedisArray___call, 0, 2, IS_MIXED, 0)
ZEND_ARG_TYPE_INFO(0, function_name, IS_STRING, 0)
@@ -57,7 +57,7 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_RedisArray_hscan, 0, 2, MAY_BE_BOOL|MAY_BE_ARRAY)
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()
@@ -86,7 +86,7 @@ ZEND_END_ARG_INFO()
#define arginfo_class_RedisArray_save arginfo_class_RedisArray__continuum
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_RedisArray_scan, 0, 2, MAY_BE_BOOL|MAY_BE_ARRAY)
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
ZEND_ARG_TYPE_INFO(0, node, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 59943eeb14b3ed78f88117e6923d64a95911b5ff */
* Stub hash: ddb92422452cb767a7d6694aa8ac60d883db6672 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisArray___call, 0, 0, 2)
ZEND_ARG_INFO(0, function_name)

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 685a816f4e46c30d8a9ae787948c69d8a20213b1 */
* Stub hash: d6839707b66ecf4460374deea10a528bf0c5ea41 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0)
ZEND_ARG_INFO(0, options)