This commit is contained in:
Filipe Oliveira (Redis) 2025-07-16 13:17:57 +01:00 committed by GitHub
commit abc5962e15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 391 additions and 324 deletions

340
src/db.c
View File

@ -1286,9 +1286,16 @@ void keysCommand(client *c) {
setDeferredArrayLen(c,replylen,numkeys);
}
#define SCAN_KEYS_INITIAL_CAPACITY 64
/* Data used by the dict scan callback. */
typedef struct {
list *keys; /* elements that collect from dict */
/* Dynamic array for all scan optimizations */
void **array_items; /* pointer to items array (stack or heap allocated) */
int array_count; /* number of items collected */
int array_capacity; /* current capacity of items array */
void **array_stack_buffer; /* reference to original stack buffer for cleanup check */
int array_stores_pairs; /* 1 if storing key-value pairs, 0 if single items */
robj *o; /* o must be a hash/set/zset object, NULL means current db */
long long type; /* the particular type when scan the db */
sds pattern; /* pattern string, NULL means no pattern */
@ -1299,6 +1306,28 @@ typedef struct {
redisDb *db; /* database reference for expiration checks */
} scanData;
/* Helper function to add an item to the scan results (for all scan commands) */
static void scanItemsPush(scanData *data, void *item) {
if (unlikely(data->array_count >= data->array_capacity)) {
/* Need to grow the array */
int new_capacity = data->array_capacity * 2;
if (data->array_items == data->array_stack_buffer) {
/* First time growing from stack buffer - need to allocate and copy */
void **new_items = zmalloc(new_capacity * sizeof(void*));
memcpy(new_items, data->array_items, data->array_count * sizeof(void*));
data->array_items = new_items;
} else {
/* Already heap allocated - use zrealloc */
data->array_items = zrealloc(data->array_items, new_capacity * sizeof(void*));
}
data->array_capacity = new_capacity;
}
data->array_items[data->array_count++] = item;
}
/* Helper function to compare key type in scan commands */
int objectTypeCompare(robj *o, long long target) {
if (o->type != OBJ_MODULE) {
@ -1319,7 +1348,6 @@ int objectTypeCompare(robj *o, long long target) {
void scanCallback(void *privdata, const dictEntry *de, dictEntryLink plink) {
UNUSED(plink);
scanData *data = (scanData *)privdata;
list *keys = data->keys;
robj *o = data->o;
sds val = NULL;
void *key = NULL; /* if OBJ_HASH then key is of type `hfield`. Otherwise, `sds` */
@ -1360,28 +1388,47 @@ void scanCallback(void *privdata, const dictEntry *de, dictEntryLink plink) {
}
if (o == NULL) {
key = keyStr;
/* SCAN command - single keys */
data->array_stores_pairs = 0;
scanItemsPush(data, keyStr);
return;
} else if (o->type == OBJ_SET) {
key = keyStr;
/* SSCAN command - single members */
data->array_stores_pairs = 0;
scanItemsPush(data, keyStr);
return;
} else if (o->type == OBJ_HASH) {
key = keyStr;
val = dictGetVal(de);
/* If field is expired, then ignore */
if (hfieldIsExpired(key))
/* HSCAN command - field-value pairs */
if (hfieldIsExpired(keyStr))
return;
if (data->no_values) {
data->array_stores_pairs = 0;
scanItemsPush(data, key);
} else {
data->array_stores_pairs = 1;
scanItemsPush(data, key);
scanItemsPush(data, val);
}
return;
} else if (o->type == OBJ_ZSET) {
/* ZSCAN command - member-score pairs */
char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf, sizeof(buf), *(double *)dictGetVal(de), LD_STR_AUTO);
key = sdsdup(keyStr);
val = sdsnewlen(buf, len);
data->array_stores_pairs = 1;
scanItemsPush(data, key);
scanItemsPush(data, val);
return;
} else {
serverPanic("Type not handled in SCAN callback.");
}
listAddNodeTail(keys, key);
if (val && !data->no_values) listAddNodeTail(keys, val);
}
/* Try to parse a SCAN cursor stored at object 'o':
@ -1436,129 +1483,80 @@ char *getObjectTypeName(robj *o) {
}
}
/* This command implements SCAN, HSCAN and SSCAN commands.
* If object 'o' is passed, then it must be a Hash, Set or Zset object, otherwise
* if 'o' is NULL the command will operate on the dictionary associated with
* the current database.
*
* When 'o' is not NULL the function assumes that the first argument in
* the client arguments vector is a key so it skips it before iterating
* in order to parse options.
*
* In the case of a Hash object the function returns both the field and value
* of every element on the Hash. */
void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
int isKeysHfield = 0;
int i, j;
listNode *node;
long count = 10;
sds pat = NULL;
sds typename = NULL;
long long type = LLONG_MAX;
int patlen = 0, use_pattern = 0, no_values = 0;
dict *ht;
/* Parse SCAN command options (COUNT, MATCH, TYPE, NOVALUES).
* Does NOT parse cursor - that should be done separately.
* Returns C_OK on success, C_ERR on error (reply already sent). */
int parseScanOptionsOrReply(client *c, robj *o, int start_argc, scanOptions *opts) {
int i = start_argc;
int j;
/* Object must be NULL (to iterate keys names), or the type of the object
* must be Set, Sorted Set, or Hash. */
serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH ||
o->type == OBJ_ZSET);
/* Initialize option defaults (cursor should already be set) */
opts->count = 10;
opts->pattern = NULL;
opts->patlen = 0;
opts->use_pattern = 0;
opts->type = LLONG_MAX;
opts->typename = NULL;
opts->no_values = 0;
/* Set i to the first option argument. The previous one is the cursor. */
i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */
/* Step 1: Parse options. */
/* Parse options starting from start_argc */
while (i < c->argc) {
j = c->argc - i;
if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL)
!= C_OK)
{
return;
if (getLongFromObjectOrReply(c, c->argv[i+1], &opts->count, NULL) != C_OK) {
return C_ERR;
}
if (count < 1) {
if (opts->count < 1) {
addReplyErrorObject(c, shared.syntaxerr);
return;
return C_ERR;
}
i += 2;
} else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
pat = c->argv[i+1]->ptr;
patlen = sdslen(pat);
opts->pattern = c->argv[i+1]->ptr;
opts->patlen = sdslen(opts->pattern);
/* The pattern always matches if it is exactly "*", so it is
* equivalent to disabling it. */
use_pattern = !(patlen == 1 && pat[0] == '*');
opts->use_pattern = !(opts->patlen == 1 && opts->pattern[0] == '*');
i += 2;
} else if (!strcasecmp(c->argv[i]->ptr, "type") && o == NULL && j >= 2) {
/* SCAN for a particular type only applies to the db dict */
typename = c->argv[i+1]->ptr;
type = getObjectTypeByName(typename);
if (type == LLONG_MAX) {
opts->typename = c->argv[i+1]->ptr;
opts->type = getObjectTypeByName(opts->typename);
if (opts->type == LLONG_MAX) {
/* TODO: uncomment in redis 8.0
addReplyErrorFormat(c, "unknown type name '%s'", typename);
return; */
addReplyErrorFormat(c, "unknown type name '%s'", opts->typename);
return C_ERR; */
}
i += 2;
} else if (!strcasecmp(c->argv[i]->ptr, "novalues")) {
if (!o || o->type != OBJ_HASH) {
addReplyError(c, "NOVALUES option can only be used in HSCAN");
return;
return C_ERR;
}
no_values = 1;
opts->no_values = 1;
i++;
} else {
addReplyErrorObject(c, shared.syntaxerr);
return;
return C_ERR;
}
}
/* Step 2: Iterate the collection.
*
* Note that if the object is encoded with a listpack, intset, or any other
* representation that is not a hash table, we are sure that it is also
* composed of a small number of elements. So to avoid taking state we
* just return everything inside the object in a single call, setting the
* cursor to zero to signal the end of the iteration. */
/* Handle the case of a hash table. */
ht = NULL;
if (o == NULL) {
ht = NULL;
} else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
ht = o->ptr;
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
isKeysHfield = 1;
ht = o->ptr;
} else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
ht = zs->dict;
return C_OK;
}
list *keys = listCreate();
/* Set a free callback for the contents of the collected keys list.
* For the main keyspace dict, and when we scan a key that's dict encoded
* (we have 'ht'), we don't need to define free method because the strings
* in the list are just a shallow copy from the pointer in the dictEntry.
* When scanning a key with other encodings (e.g. listpack), we need to
* free the temporary strings we add to that list.
* The exception to the above is ZSET, where we do allocate temporary
* strings even when scanning a dict. */
if (o && (!ht || o->type == OBJ_ZSET)) {
listSetFreeMethod(keys, sdsfreegeneric);
}
/* Scan hash table (used by SCAN, SSCAN, HSCAN, ZSCAN with hash table encodings) */
void scanHashTable(client *c, robj *o, dict *ht, scanOptions *opts, int isKeysHfield) {
scanData data;
void *items_stack_buffer[SCAN_KEYS_INITIAL_CAPACITY];
/* For main dictionary scan or data structure using hashtable. */
if (!o || ht) {
/* We set the max number of iterations to ten times the specified
* COUNT, so if the hash table is in a pathological state (very
* sparsely populated) we avoid to block too much time at the cost
* of returning no or very few elements. */
long maxiterations = count*10;
long maxiterations = opts->count * 10;
/* We pass scanData which have three pointers to the callback:
* 1. data.keys: the list to which it will add new elements;
/* We pass scanData to the callback with:
* 1. data.array_items: the dynamic array to collect scan results;
* 2. data.o: the object containing the dictionary so that
* it is possible to fetch more data in a type-dependent way;
* 3. data.type: the specified type scan in the db, LLONG_MAX means
@ -1570,46 +1568,87 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
* to prevent a long hang time caused by filtering too many keys;
* 6. data.no_values: to control whether values will be returned or
* only keys are returned. */
scanData data = {
.keys = keys,
data = (scanData) {
.array_items = items_stack_buffer,
.array_capacity = SCAN_KEYS_INITIAL_CAPACITY,
.array_stack_buffer = items_stack_buffer,
.array_count = 0,
.array_stores_pairs = 0, /* Will be set by scanCallback */
.o = o,
.type = type,
.pattern = use_pattern ? pat : NULL,
.type = opts->type,
.pattern = opts->use_pattern ? opts->pattern : NULL,
.sampled = 0,
.no_values = no_values,
.no_values = opts->no_values,
.strlen = (isKeysHfield) ? hfieldlen : sdslen,
.typename = typename,
.typename = opts->typename,
.db = c->db,
};
/* A pattern may restrict all matching keys to one cluster slot. */
int onlydidx = -1;
if (o == NULL && use_pattern && server.cluster_enabled) {
onlydidx = patternHashSlot(pat, patlen);
if (o == NULL && opts->use_pattern && server.cluster_enabled) {
onlydidx = patternHashSlot(opts->pattern, opts->patlen);
}
/* Scan the hash table */
do {
/* In cluster mode there is a separate dictionary for each slot.
* If cursor is empty, we should try exploring next non-empty slot. */
if (o == NULL) {
cursor = kvstoreScan(c->db->keys, cursor, onlydidx, scanCallback, NULL, &data);
opts->cursor = kvstoreScan(c->db->keys, opts->cursor, onlydidx, scanCallback, NULL, &data);
} else {
cursor = dictScan(ht, cursor, scanCallback, &data);
opts->cursor = dictScan(ht, opts->cursor, scanCallback, &data);
}
} while (cursor && maxiterations-- && data.sampled < count);
} else if (o->type == OBJ_SET) {
} while (opts->cursor && maxiterations-- && data.sampled < opts->count);
/* Reply to the client. */
addReplyArrayLen(c, 2);
addReplyBulkLongLong(c, opts->cursor);
/* Generate response from array (unified for all scan commands) */
addReplyArrayLen(c, data.array_count);
for (int i = 0; i < data.array_count; i++) {
void *item = data.array_items[i];
/* Special case: HSCAN fields need mstrlen, everything else uses sdslen */
if (o && o->type == OBJ_HASH &&
(!data.array_stores_pairs || (i % 2 == 0))) {
/* HSCAN field or single field (NOVALUES) */
addReplyBulkCBuffer(c, item, mstrlen((char*)item));
} else {
/* Everything else: SCAN keys, SSCAN members, ZSCAN members/scores, HSCAN values */
addReplyBulkCBuffer(c, item, sdslen((sds)item));
}
}
/* Cleanup: free allocated values for ZSCAN */
if (o && o->type == OBJ_ZSET) {
for (int i = 0; i < data.array_count; i++) {
sdsfree((sds)data.array_items[i]);
}
}
/* Cleanup: free heap-allocated array if needed */
if (data.array_items != items_stack_buffer) {
zfree(data.array_items);
}
}
/* Scan set (used by SSCAN with intset encoding) */
void scanIntSet(client *c, robj *o, scanOptions *opts) {
unsigned long array_reply_len = 0;
void *replylen = NULL;
listRelease(keys);
char *str;
char buf[LONG_STR_SIZE];
size_t len;
int64_t llele;
/* Reply to the client. */
addReplyArrayLen(c, 2);
/* Cursor is always 0 given we iterate over all set */
addReplyBulkLongLong(c, 0);
/* If there is no pattern the length is the entire set size, otherwise we defer the reply size */
if (use_pattern)
if (opts->use_pattern)
replylen = addReplyDeferredLen(c);
else {
array_reply_len = setTypeSize(o);
@ -1623,49 +1662,51 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
len = ll2string(buf, sizeof(buf), llele);
}
char *key = str ? str : buf;
if (use_pattern && !stringmatchlen(pat, patlen, key, len, 0)) {
if (opts->use_pattern && !stringmatchlen(opts->pattern, opts->patlen, key, len, 0)) {
continue;
}
addReplyBulkCBuffer(c, key, len);
cur_length++;
}
setTypeReleaseIterator(si);
if (use_pattern)
if (opts->use_pattern)
setDeferredArrayLen(c, replylen, cur_length);
else
serverAssert(cur_length == array_reply_len); /* fail on corrupt data */
return;
} else if ((o->type == OBJ_HASH || o->type == OBJ_ZSET) &&
o->encoding == OBJ_ENCODING_LISTPACK)
{
}
/* Scan listpack (used by HSCAN, ZSCAN with listpack encodings) */
void scanListpack(client *c, robj *o, scanOptions *opts) {
unsigned char *p = lpFirst(o->ptr);
unsigned char *str;
int64_t len;
unsigned long array_reply_len = 0;
unsigned char intbuf[LP_INTBUF_SIZE];
void *replylen = NULL;
listRelease(keys);
/* Reply to the client. */
addReplyArrayLen(c, 2);
/* Cursor is always 0 given we iterate over all set */
/* Cursor is always 0 given we iterate over all listpack */
addReplyBulkLongLong(c, 0);
/* If there is no pattern the length is the entire set size, otherwise we defer the reply size */
if (use_pattern)
/* If there is no pattern the length is the entire collection size, otherwise we defer the reply size */
if (opts->use_pattern)
replylen = addReplyDeferredLen(c);
else {
array_reply_len = o->type == OBJ_HASH ? hashTypeLength(o, 0) : zsetLength(o);
if (!no_values) {
if (!opts->no_values) {
array_reply_len *= 2;
}
addReplyArrayLen(c, array_reply_len);
}
unsigned long cur_length = 0;
while(p) {
str = lpGet(p, &len, intbuf);
/* point to the value */
p = lpNext(o->ptr, p);
if (use_pattern && !stringmatchlen(pat, patlen, (char *)str, len, 0)) {
if (opts->use_pattern && !stringmatchlen(opts->pattern, opts->patlen, (char *)str, len, 0)) {
/* jump to the next key/val pair */
p = lpNext(o->ptr, p);
continue;
@ -1674,19 +1715,22 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
addReplyBulkCBuffer(c, str, len);
cur_length++;
/* add value object */
if (!no_values) {
if (!opts->no_values) {
str = lpGet(p, &len, intbuf);
addReplyBulkCBuffer(c, str, len);
cur_length++;
}
p = lpNext(o->ptr, p);
}
if (use_pattern)
if (opts->use_pattern)
setDeferredArrayLen(c, replylen, cur_length);
else
serverAssert(cur_length == array_reply_len); /* fail on corrupt data */
return;
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_LISTPACK_EX) {
}
/* Scan listpack with expiration (used by HSCAN with listpack_ex encoding) */
void scanListpackEx(client *c, robj *o, scanOptions *opts) {
int64_t len;
long long expire_at;
unsigned char *lp = hashTypeListpackGetLp(o);
@ -1695,10 +1739,9 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
unsigned char intbuf[LP_INTBUF_SIZE];
void *replylen = NULL;
listRelease(keys);
/* Reply to the client. */
addReplyArrayLen(c, 2);
/* Cursor is always 0 given we iterate over all set */
/* Cursor is always 0 given we iterate over all listpack */
addReplyBulkLongLong(c, 0);
/* In the case of OBJ_ENCODING_LISTPACK_EX we always defer the reply size given some fields might be expired */
replylen = addReplyDeferredLen(c);
@ -1713,7 +1756,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
serverAssert(p && lpGetIntegerValue(p, &expire_at));
if (hashTypeIsExpired(o, expire_at) ||
(use_pattern && !stringmatchlen(pat, patlen, (char *)str, len, 0)))
(opts->use_pattern && !stringmatchlen(opts->pattern, opts->patlen, (char *)str, len, 0)))
{
/* jump to the next key/val pair */
p = lpNext(lp, p);
@ -1724,7 +1767,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
addReplyBulkCBuffer(c, str, len);
cur_length++;
/* add value object */
if (!no_values) {
if (!opts->no_values) {
str = lpGet(val, &len, intbuf);
addReplyBulkCBuffer(c, str, len);
cur_length++;
@ -1732,34 +1775,17 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
p = lpNext(lp, p);
}
setDeferredArrayLen(c, replylen, cur_length);
return;
} else {
serverPanic("Not handled encoding in SCAN.");
}
/* Step 3: Reply to the client. */
addReplyArrayLen(c, 2);
addReplyBulkLongLong(c,cursor);
unsigned long long idx = 0;
addReplyArrayLen(c, listLength(keys));
while ((node = listFirst(keys)) != NULL) {
void *key = listNodeValue(node);
/* For HSCAN, list will contain keys value pairs unless no_values arg
* was given. We should call mstrlen for the keys only. */
int hfieldkey = isKeysHfield && (no_values || (idx++ % 2 == 0));
addReplyBulkCBuffer(c, key, hfieldkey ? mstrlen(key) : sdslen(key));
listDelNode(keys, node);
}
listRelease(keys);
}
/* The SCAN command completely relies on scanGenericCommand. */
/* The SCAN command directly uses scanHashTable for database keys. */
void scanCommand(client *c) {
unsigned long long cursor;
if (parseScanCursorOrReply(c,c->argv[1],&cursor) == C_ERR) return;
scanGenericCommand(c,NULL,cursor);
scanOptions opts;
if (parseScanCursorOrReply(c, c->argv[1], &opts.cursor) == C_ERR) return;
if (parseScanOptionsOrReply(c, NULL, 2, &opts) == C_ERR) return;
scanHashTable(c, NULL, NULL, &opts, 0);
}
void dbsizeCommand(client *c) {

View File

@ -3715,12 +3715,27 @@ long long dbTotalServerKeyCount(void);
redisDb *initTempDb(void);
void discardTempDb(redisDb *tempDb);
/* Options for SCAN commands (SCAN, HSCAN, SSCAN, ZSCAN) */
typedef struct {
unsigned long long cursor; /* Cursor position */
long count; /* COUNT option value */
sds pattern; /* MATCH pattern string */
int patlen; /* Pattern length */
int use_pattern; /* Whether to use pattern matching */
long long type; /* TYPE filter for SCAN command */
sds typename; /* TYPE name string */
int no_values; /* NOVALUES option for HSCAN */
} scanOptions;
int selectDb(client *c, int id);
void signalModifiedKey(client *c, redisDb *db, robj *key);
void signalFlushedDb(int dbid, int async);
void scanGenericCommand(client *c, robj *o, unsigned long long cursor);
void scanHashTable(client *c, robj *o, dict *ht, scanOptions *opts, int isKeysHfield);
void scanListpack(client *c, robj *o, scanOptions *opts);
void scanListpackEx(client *c, robj *o, scanOptions *opts);
void scanIntSet(client *c, robj *o, scanOptions *opts);
int parseScanCursorOrReply(client *c, robj *o, unsigned long long *cursor);
int parseScanOptionsOrReply(client *c, robj *o, int start_argc, scanOptions *opts);
int dbAsyncDelete(redisDb *db, robj *key);
void emptyDbAsync(redisDb *db);
size_t lazyfreeGetPendingObjectsCount(void);

View File

@ -3033,14 +3033,25 @@ void hexistsCommand(client *c) {
}
void hscanCommand(client *c) {
kvobj *o;
unsigned long long cursor;
robj *o;
scanOptions opts;
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if (parseScanCursorOrReply(c, c->argv[2], &opts.cursor) == C_ERR) return;
if ((o = lookupKeyReadOrReply(c, c->argv[1], shared.emptyscan)) == NULL ||
checkType(c, o, OBJ_HASH)) return;
scanGenericCommand(c,o,cursor);
if (parseScanOptionsOrReply(c, o, 3, &opts) == C_ERR) return;
/* Handle hash encoding-specific scanning */
if (o->encoding == OBJ_ENCODING_HT) {
scanHashTable(c, o, o->ptr, &opts, 1);
} else if (o->encoding == OBJ_ENCODING_LISTPACK) {
scanListpack(c, o, &opts);
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
scanListpackEx(c, o, &opts);
} else {
serverPanic("Not handled encoding in HSCAN.");
}
}
static void hrandfieldReplyWithListpack(client *c, unsigned int count, listpackEntry *keys, listpackEntry *vals) {

View File

@ -1731,11 +1731,18 @@ void sdiffstoreCommand(client *c) {
}
void sscanCommand(client *c) {
kvobj *set;
unsigned long long cursor;
robj *set;
scanOptions opts;
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if (parseScanCursorOrReply(c, c->argv[2], &opts.cursor) == C_ERR) return;
if ((set = lookupKeyReadOrReply(c, c->argv[1], shared.emptyscan)) == NULL ||
checkType(c, set, OBJ_SET)) return;
scanGenericCommand(c,set,cursor);
if (parseScanOptionsOrReply(c, set, 3, &opts) == C_ERR) return;
if(set->encoding == OBJ_ENCODING_HT) {
scanHashTable(c, set, set->ptr, &opts, 0);
} else {
scanIntSet(c,set, &opts);
}
}

View File

@ -3900,13 +3900,21 @@ void zrevrankCommand(client *c) {
}
void zscanCommand(client *c) {
kvobj *o;
unsigned long long cursor;
robj *o;
scanOptions opts;
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if (parseScanCursorOrReply(c, c->argv[2], &opts.cursor) == C_ERR) return;
if ((o = lookupKeyReadOrReply(c, c->argv[1], shared.emptyscan)) == NULL ||
checkType(c, o, OBJ_ZSET)) return;
scanGenericCommand(c,o,cursor);
if (parseScanOptionsOrReply(c, o, 3, &opts) == C_ERR) return;
if(o->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
scanHashTable(c, o, zs->dict, &opts, 0);
} else {
scanListpack(c, o, &opts);
}
}
/* This command implements the generic zpop operation, used by: