mirror of https://mirror.osredm.com/root/redis.git
Fixes ZPOPMIN/ZPOPMAX wrong replies when count is 0 with non-zset (#9711)
Moves ZPOP ... 0 fast exit path after type check to reply with WRONGTYPE. In the past it will return an empty array. Also now count is not allowed to be negative. see #9680 before: ``` 127.0.0.1:6379> set zset str OK 127.0.0.1:6379> zpopmin zset 0 (empty array) 127.0.0.1:6379> zpopmin zset -1 (empty array) ``` after: ``` 127.0.0.1:6379> set zset str OK 127.0.0.1:6379> zpopmin zset 0 (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> zpopmin zset -1 (error) ERR value is out of range, must be positive ```
This commit is contained in:
parent
32215e7889
commit
91e77a0cfb
|
@ -363,7 +363,7 @@ void serveClientsBlockedOnSortedSetKey(robj *o, readyList *rl) {
|
||||||
argv[0] = where == ZSET_MIN ? shared.zpopmin : shared.zpopmax;
|
argv[0] = where == ZSET_MIN ? shared.zpopmin : shared.zpopmax;
|
||||||
argv[1] = rl->key;
|
argv[1] = rl->key;
|
||||||
incrRefCount(rl->key);
|
incrRefCount(rl->key);
|
||||||
if (count != 0) {
|
if (count != -1) {
|
||||||
/* Replicate it as command with COUNT. */
|
/* Replicate it as command with COUNT. */
|
||||||
robj *count_obj = createStringObjectFromLongLong((count > llen) ? llen : count);
|
robj *count_obj = createStringObjectFromLongLong((count > llen) ? llen : count);
|
||||||
argv[2] = count_obj;
|
argv[2] = count_obj;
|
||||||
|
@ -371,7 +371,7 @@ void serveClientsBlockedOnSortedSetKey(robj *o, readyList *rl) {
|
||||||
}
|
}
|
||||||
propagate(receiver->db->id, argv, argc, PROPAGATE_AOF|PROPAGATE_REPL);
|
propagate(receiver->db->id, argv, argc, PROPAGATE_AOF|PROPAGATE_REPL);
|
||||||
decrRefCount(argv[1]);
|
decrRefCount(argv[1]);
|
||||||
if (count != 0) decrRefCount(argv[2]);
|
if (count != -1) decrRefCount(argv[2]);
|
||||||
|
|
||||||
/* The zset is empty and has been deleted. */
|
/* The zset is empty and has been deleted. */
|
||||||
if (deleted) break;
|
if (deleted) break;
|
||||||
|
|
|
@ -6249,7 +6249,7 @@ RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdF
|
||||||
"Blocking module command called from transaction");
|
"Blocking module command called from transaction");
|
||||||
} else {
|
} else {
|
||||||
if (keys) {
|
if (keys) {
|
||||||
blockForKeys(c,BLOCKED_MODULE,keys,numkeys,0,timeout,NULL,NULL,NULL);
|
blockForKeys(c,BLOCKED_MODULE,keys,numkeys,-1,timeout,NULL,NULL,NULL);
|
||||||
} else {
|
} else {
|
||||||
blockClient(c,BLOCKED_MODULE);
|
blockClient(c,BLOCKED_MODULE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -846,7 +846,7 @@ typedef struct multiState {
|
||||||
* The fields used depend on client->btype. */
|
* The fields used depend on client->btype. */
|
||||||
typedef struct blockingState {
|
typedef struct blockingState {
|
||||||
/* Generic fields. */
|
/* Generic fields. */
|
||||||
long count; /* Elements to pop if count was specified (BLMPOP), 0 otherwise. */
|
long count; /* Elements to pop if count was specified (BLMPOP/BZMPOP), -1 otherwise. */
|
||||||
mstime_t timeout; /* Blocking operation timeout. If UNIX current time
|
mstime_t timeout; /* Blocking operation timeout. If UNIX current time
|
||||||
* is > timeout then the operation timed out. */
|
* is > timeout then the operation timed out. */
|
||||||
|
|
||||||
|
|
18
src/t_list.c
18
src/t_list.c
|
@ -1040,9 +1040,9 @@ void serveClientBlockedOnList(client *receiver, robj *o, robj *key, robj *dstkey
|
||||||
* 'numkeys' is the number of keys.
|
* 'numkeys' is the number of keys.
|
||||||
* 'timeout_idx' parameter position of block timeout.
|
* 'timeout_idx' parameter position of block timeout.
|
||||||
* 'where' LIST_HEAD for LEFT, LIST_TAIL for RIGHT.
|
* 'where' LIST_HEAD for LEFT, LIST_TAIL for RIGHT.
|
||||||
* 'count' is the number of elements requested to pop, or 0 for plain single pop.
|
* 'count' is the number of elements requested to pop, or -1 for plain single pop.
|
||||||
*
|
*
|
||||||
* When count is 0, a reply of a single bulk-string will be used.
|
* When count is -1, a reply of a single bulk-string will be used.
|
||||||
* When count > 0, an array reply will be used. */
|
* When count > 0, an array reply will be used. */
|
||||||
void blockingPopGenericCommand(client *c, robj **keys, int numkeys, int where, int timeout_idx, long count) {
|
void blockingPopGenericCommand(client *c, robj **keys, int numkeys, int where, int timeout_idx, long count) {
|
||||||
robj *o;
|
robj *o;
|
||||||
|
@ -1067,7 +1067,7 @@ void blockingPopGenericCommand(client *c, robj **keys, int numkeys, int where, i
|
||||||
/* Empty list, move to next key. */
|
/* Empty list, move to next key. */
|
||||||
if (llen == 0) continue;
|
if (llen == 0) continue;
|
||||||
|
|
||||||
if (count != 0) {
|
if (count != -1) {
|
||||||
/* BLMPOP, non empty list, like a normal [LR]POP with count option.
|
/* BLMPOP, non empty list, like a normal [LR]POP with count option.
|
||||||
* The difference here we pop a range of elements in a nested arrays way. */
|
* The difference here we pop a range of elements in a nested arrays way. */
|
||||||
listPopRangeAndReplyWithKey(c, o, key, where, count, NULL);
|
listPopRangeAndReplyWithKey(c, o, key, where, count, NULL);
|
||||||
|
@ -1112,12 +1112,12 @@ void blockingPopGenericCommand(client *c, robj **keys, int numkeys, int where, i
|
||||||
|
|
||||||
/* BLPOP <key> [<key> ...] <timeout> */
|
/* BLPOP <key> [<key> ...] <timeout> */
|
||||||
void blpopCommand(client *c) {
|
void blpopCommand(client *c) {
|
||||||
blockingPopGenericCommand(c,c->argv+1,c->argc-2,LIST_HEAD,c->argc-1,0);
|
blockingPopGenericCommand(c,c->argv+1,c->argc-2,LIST_HEAD,c->argc-1,-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BRPOP <key> [<key> ...] <timeout> */
|
/* BRPOP <key> [<key> ...] <timeout> */
|
||||||
void brpopCommand(client *c) {
|
void brpopCommand(client *c) {
|
||||||
blockingPopGenericCommand(c,c->argv+1,c->argc-2,LIST_TAIL,c->argc-1,0);
|
blockingPopGenericCommand(c,c->argv+1,c->argc-2,LIST_TAIL,c->argc-1,-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void blmoveGenericCommand(client *c, int wherefrom, int whereto, mstime_t timeout) {
|
void blmoveGenericCommand(client *c, int wherefrom, int whereto, mstime_t timeout) {
|
||||||
|
@ -1132,7 +1132,7 @@ void blmoveGenericCommand(client *c, int wherefrom, int whereto, mstime_t timeou
|
||||||
} else {
|
} else {
|
||||||
/* The list is empty and the client blocks. */
|
/* The list is empty and the client blocks. */
|
||||||
struct blockPos pos = {wherefrom, whereto};
|
struct blockPos pos = {wherefrom, whereto};
|
||||||
blockForKeys(c,BLOCKED_LIST,c->argv + 1,1,0,timeout,c->argv[2],&pos,NULL);
|
blockForKeys(c,BLOCKED_LIST,c->argv + 1,1,-1,timeout,c->argv[2],&pos,NULL);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* The list exists and has elements, so
|
/* The list exists and has elements, so
|
||||||
|
@ -1171,7 +1171,7 @@ void lmpopGenericCommand(client *c, int numkeys_idx, int is_block) {
|
||||||
long j;
|
long j;
|
||||||
long numkeys = 0; /* Number of keys. */
|
long numkeys = 0; /* Number of keys. */
|
||||||
int where = 0; /* HEAD for LEFT, TAIL for RIGHT. */
|
int where = 0; /* HEAD for LEFT, TAIL for RIGHT. */
|
||||||
long count = 0; /* Reply will consist of up to count elements, depending on the list's length. */
|
long count = -1; /* Reply will consist of up to count elements, depending on the list's length. */
|
||||||
|
|
||||||
/* Parse the numkeys. */
|
/* Parse the numkeys. */
|
||||||
if (getRangeLongFromObjectOrReply(c, c->argv[numkeys_idx], 1, LONG_MAX,
|
if (getRangeLongFromObjectOrReply(c, c->argv[numkeys_idx], 1, LONG_MAX,
|
||||||
|
@ -1192,7 +1192,7 @@ void lmpopGenericCommand(client *c, int numkeys_idx, int is_block) {
|
||||||
char *opt = c->argv[j]->ptr;
|
char *opt = c->argv[j]->ptr;
|
||||||
int moreargs = (c->argc - 1) - j;
|
int moreargs = (c->argc - 1) - j;
|
||||||
|
|
||||||
if (count == 0 && !strcasecmp(opt, "COUNT") && moreargs) {
|
if (count == -1 && !strcasecmp(opt, "COUNT") && moreargs) {
|
||||||
j++;
|
j++;
|
||||||
if (getRangeLongFromObjectOrReply(c, c->argv[j], 1, LONG_MAX,
|
if (getRangeLongFromObjectOrReply(c, c->argv[j], 1, LONG_MAX,
|
||||||
&count,"count should be greater than 0") != C_OK)
|
&count,"count should be greater than 0") != C_OK)
|
||||||
|
@ -1203,7 +1203,7 @@ void lmpopGenericCommand(client *c, int numkeys_idx, int is_block) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count == 0) count = 1;
|
if (count == -1) count = 1;
|
||||||
|
|
||||||
if (is_block) {
|
if (is_block) {
|
||||||
/* BLOCK. We will handle CLIENT_DENY_BLOCKING flag in blockingPopGenericCommand. */
|
/* BLOCK. We will handle CLIENT_DENY_BLOCKING flag in blockingPopGenericCommand. */
|
||||||
|
|
|
@ -2182,7 +2182,7 @@ void xreadCommand(client *c) {
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
blockForKeys(c, BLOCKED_STREAM, c->argv+streams_arg, streams_count,
|
blockForKeys(c, BLOCKED_STREAM, c->argv+streams_arg, streams_count,
|
||||||
0, timeout, NULL, NULL, ids);
|
-1, timeout, NULL, NULL, ids);
|
||||||
/* If no COUNT is given and we block, set a relatively small count:
|
/* If no COUNT is given and we block, set a relatively small count:
|
||||||
* in case the ID provided is too low, we do not want the server to
|
* in case the ID provided is too low, we do not want the server to
|
||||||
* block just to serve this client a huge stream of messages. */
|
* block just to serve this client a huge stream of messages. */
|
||||||
|
|
41
src/t_zset.c
41
src/t_zset.c
|
@ -3776,7 +3776,7 @@ void zscanCommand(client *c) {
|
||||||
* behavior of BZPOP[MIN|MAX], since we can block into multiple keys.
|
* behavior of BZPOP[MIN|MAX], since we can block into multiple keys.
|
||||||
* Or in ZMPOP/BZMPOP, because we also can take multiple keys.
|
* Or in ZMPOP/BZMPOP, because we also can take multiple keys.
|
||||||
*
|
*
|
||||||
* 'count' is the number of elements requested to pop, or 0 for plain single pop.
|
* 'count' is the number of elements requested to pop, or -1 for plain single pop.
|
||||||
*
|
*
|
||||||
* 'use_nested_array' when false it generates a flat array (with or without key name).
|
* 'use_nested_array' when false it generates a flat array (with or without key name).
|
||||||
* When true, it generates a nested 2 level array of field + score pairs, or 3 level when emitkey is set.
|
* When true, it generates a nested 2 level array of field + score pairs, or 3 level when emitkey is set.
|
||||||
|
@ -3819,10 +3819,16 @@ void genericZpopCommand(client *c, robj **keyv, int keyc, int where, int emitkey
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
/* ZPOPMIN/ZPOPMAX with count 0. */
|
||||||
|
addReply(c, shared.emptyarray);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
long result_count = 0;
|
long result_count = 0;
|
||||||
|
|
||||||
/* When count is 0, we need to correct it to 1 for plain single pop. */
|
/* When count is -1, we need to correct it to 1 for plain single pop. */
|
||||||
if (count == 0) count = 1;
|
if (count == -1) count = 1;
|
||||||
|
|
||||||
long llen = zsetLength(zobj);
|
long llen = zsetLength(zobj);
|
||||||
long rangelen = (count > llen) ? llen : count;
|
long rangelen = (count > llen) ? llen : count;
|
||||||
|
@ -3926,20 +3932,13 @@ void zpopMinMaxCommand(client *c, int where) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long count = 0; /* 0 for plain single pop. */
|
long count = -1; /* -1 for plain single pop. */
|
||||||
if (c->argc == 3) {
|
if (c->argc == 3 && getPositiveLongFromObjectOrReply(c, c->argv[2], &count, NULL) != C_OK)
|
||||||
if (getLongFromObjectOrReply(c, c->argv[2], &count, NULL) != C_OK)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (count <= 0) {
|
/* Respond with a single (flat) array in RESP2 or if count is -1
|
||||||
addReply(c, shared.emptyarray);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Respond with a single (flat) array in RESP2 or if count is 0
|
|
||||||
* (returning a single element). In RESP3, when count > 0 use nested array. */
|
* (returning a single element). In RESP3, when count > 0 use nested array. */
|
||||||
int use_nested_array = (c->resp > 2 && count != 0);
|
int use_nested_array = (c->resp > 2 && count != -1);
|
||||||
|
|
||||||
genericZpopCommand(c, &c->argv[1], 1, where, 0, count, use_nested_array, 0, NULL);
|
genericZpopCommand(c, &c->argv[1], 1, where, 0, count, use_nested_array, 0, NULL);
|
||||||
}
|
}
|
||||||
|
@ -3962,7 +3961,7 @@ void zpopmaxCommand(client *c) {
|
||||||
*
|
*
|
||||||
* 'where' ZSET_MIN or ZSET_MAX.
|
* 'where' ZSET_MIN or ZSET_MAX.
|
||||||
*
|
*
|
||||||
* 'count' is the number of elements requested to pop, or 0 for plain single pop.
|
* 'count' is the number of elements requested to pop, or -1 for plain single pop.
|
||||||
*
|
*
|
||||||
* 'use_nested_array' when false it generates a flat array (with or without key name).
|
* 'use_nested_array' when false it generates a flat array (with or without key name).
|
||||||
* When true, it generates a nested 3 level array of keyname, field + score pairs.
|
* When true, it generates a nested 3 level array of keyname, field + score pairs.
|
||||||
|
@ -3992,7 +3991,7 @@ void blockingGenericZpopCommand(client *c, robj **keys, int numkeys, int where,
|
||||||
/* Non empty zset, this is like a normal ZPOP[MIN|MAX]. */
|
/* Non empty zset, this is like a normal ZPOP[MIN|MAX]. */
|
||||||
genericZpopCommand(c, &key, 1, where, 1, count, use_nested_array, reply_nil_when_empty, NULL);
|
genericZpopCommand(c, &key, 1, where, 1, count, use_nested_array, reply_nil_when_empty, NULL);
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == -1) {
|
||||||
/* Replicate it as ZPOP[MIN|MAX] instead of BZPOP[MIN|MAX]. */
|
/* Replicate it as ZPOP[MIN|MAX] instead of BZPOP[MIN|MAX]. */
|
||||||
rewriteClientCommandVector(c,2,
|
rewriteClientCommandVector(c,2,
|
||||||
(where == ZSET_MAX) ? shared.zpopmax : shared.zpopmin,
|
(where == ZSET_MAX) ? shared.zpopmax : shared.zpopmin,
|
||||||
|
@ -4023,12 +4022,12 @@ void blockingGenericZpopCommand(client *c, robj **keys, int numkeys, int where,
|
||||||
|
|
||||||
// BZPOPMIN key [key ...] timeout
|
// BZPOPMIN key [key ...] timeout
|
||||||
void bzpopminCommand(client *c) {
|
void bzpopminCommand(client *c) {
|
||||||
blockingGenericZpopCommand(c, c->argv+1, c->argc-2, ZSET_MIN, c->argc-1, 0, 0, 0);
|
blockingGenericZpopCommand(c, c->argv+1, c->argc-2, ZSET_MIN, c->argc-1, -1, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// BZPOPMAX key [key ...] timeout
|
// BZPOPMAX key [key ...] timeout
|
||||||
void bzpopmaxCommand(client *c) {
|
void bzpopmaxCommand(client *c) {
|
||||||
blockingGenericZpopCommand(c, c->argv+1, c->argc-2, ZSET_MAX, c->argc-1, 0, 0, 0);
|
blockingGenericZpopCommand(c, c->argv+1, c->argc-2, ZSET_MAX, c->argc-1, -1, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void zarndmemberReplyWithListpack(client *c, unsigned int count, listpackEntry *keys, listpackEntry *vals) {
|
static void zarndmemberReplyWithListpack(client *c, unsigned int count, listpackEntry *keys, listpackEntry *vals) {
|
||||||
|
@ -4287,7 +4286,7 @@ void zmpopGenericCommand(client *c, int numkeys_idx, int is_block) {
|
||||||
long j;
|
long j;
|
||||||
long numkeys = 0; /* Number of keys. */
|
long numkeys = 0; /* Number of keys. */
|
||||||
int where = 0; /* ZSET_MIN or ZSET_MAX. */
|
int where = 0; /* ZSET_MIN or ZSET_MAX. */
|
||||||
long count = 0; /* Reply will consist of up to count elements, depending on the zset's length. */
|
long count = -1; /* Reply will consist of up to count elements, depending on the zset's length. */
|
||||||
|
|
||||||
/* Parse the numkeys. */
|
/* Parse the numkeys. */
|
||||||
if (getRangeLongFromObjectOrReply(c, c->argv[numkeys_idx], 1, LONG_MAX,
|
if (getRangeLongFromObjectOrReply(c, c->argv[numkeys_idx], 1, LONG_MAX,
|
||||||
|
@ -4314,7 +4313,7 @@ void zmpopGenericCommand(client *c, int numkeys_idx, int is_block) {
|
||||||
char *opt = c->argv[j]->ptr;
|
char *opt = c->argv[j]->ptr;
|
||||||
int moreargs = (c->argc - 1) - j;
|
int moreargs = (c->argc - 1) - j;
|
||||||
|
|
||||||
if (count == 0 && !strcasecmp(opt, "COUNT") && moreargs) {
|
if (count == -1 && !strcasecmp(opt, "COUNT") && moreargs) {
|
||||||
j++;
|
j++;
|
||||||
if (getRangeLongFromObjectOrReply(c, c->argv[j], 1, LONG_MAX,
|
if (getRangeLongFromObjectOrReply(c, c->argv[j], 1, LONG_MAX,
|
||||||
&count,"count should be greater than 0") != C_OK)
|
&count,"count should be greater than 0") != C_OK)
|
||||||
|
@ -4325,7 +4324,7 @@ void zmpopGenericCommand(client *c, int numkeys_idx, int is_block) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count == 0) count = 1;
|
if (count == -1) count = 1;
|
||||||
|
|
||||||
if (is_block) {
|
if (is_block) {
|
||||||
/* BLOCK. We will handle CLIENT_DENY_BLOCKING flag in blockingGenericZpopCommand. */
|
/* BLOCK. We will handle CLIENT_DENY_BLOCKING flag in blockingGenericZpopCommand. */
|
||||||
|
|
|
@ -1003,6 +1003,32 @@ start_server {tags {"zset"}} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach {pop} {ZPOPMIN ZPOPMAX} {
|
||||||
|
test "$pop with the count 0 returns an empty array" {
|
||||||
|
r del zset
|
||||||
|
r zadd zset 1 a 2 b 3 c
|
||||||
|
assert_equal {} [r $pop zset 0]
|
||||||
|
|
||||||
|
# Make sure we can distinguish between an empty array and a null response
|
||||||
|
r readraw 1
|
||||||
|
assert_equal {*0} [r $pop zset 0]
|
||||||
|
r readraw 0
|
||||||
|
|
||||||
|
assert_equal 3 [r zcard zset]
|
||||||
|
}
|
||||||
|
|
||||||
|
test "$pop with negative count" {
|
||||||
|
r set zset foo
|
||||||
|
assert_error "ERR *must be positive" {r $pop zset -1}
|
||||||
|
|
||||||
|
r del zset
|
||||||
|
assert_error "ERR *must be positive" {r $pop zset -2}
|
||||||
|
|
||||||
|
r zadd zset 1 a 2 b 3 c
|
||||||
|
assert_error "ERR *must be positive" {r $pop zset -3}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach {popmin popmax} {ZPOPMIN ZPOPMAX ZMPOP_MIN ZMPOP_MAX} {
|
foreach {popmin popmax} {ZPOPMIN ZPOPMAX ZMPOP_MIN ZMPOP_MAX} {
|
||||||
test "Basic $popmin/$popmax with a single key - $encoding" {
|
test "Basic $popmin/$popmax with a single key - $encoding" {
|
||||||
r del zset
|
r del zset
|
||||||
|
@ -1115,7 +1141,9 @@ start_server {tags {"zset"}} {
|
||||||
test "ZPOP/ZMPOP against wrong type" {
|
test "ZPOP/ZMPOP against wrong type" {
|
||||||
r set foo{t} bar
|
r set foo{t} bar
|
||||||
assert_error "*WRONGTYPE*" {r zpopmin foo{t}}
|
assert_error "*WRONGTYPE*" {r zpopmin foo{t}}
|
||||||
|
assert_error "*WRONGTYPE*" {r zpopmin foo{t} 0}
|
||||||
assert_error "*WRONGTYPE*" {r zpopmax foo{t}}
|
assert_error "*WRONGTYPE*" {r zpopmax foo{t}}
|
||||||
|
assert_error "*WRONGTYPE*" {r zpopmax foo{t} 0}
|
||||||
assert_error "*WRONGTYPE*" {r zpopmin foo{t} 2}
|
assert_error "*WRONGTYPE*" {r zpopmin foo{t} 2}
|
||||||
|
|
||||||
assert_error "*WRONGTYPE*" {r zmpop 1 foo{t} min}
|
assert_error "*WRONGTYPE*" {r zmpop 1 foo{t} min}
|
||||||
|
|
Loading…
Reference in New Issue