mirror of
https://github.com/redis/redis.git
synced 2026-07-16 13:32:51 -04:00
Merge 6a7505545c into 1aa21f97d2
This commit is contained in:
commit
c8fbf86792
2 changed files with 166 additions and 2 deletions
109
src/t_zset.c
109
src/t_zset.c
|
|
@ -3262,6 +3262,11 @@ struct zrange_result_handler {
|
|||
zrangeResultFinalizeFunction finalizeResultEmission;
|
||||
zrangeResultEmitCBufferFunction emitResultFromCBuffer;
|
||||
zrangeResultEmitLongLongFunction emitResultFromLongLong;
|
||||
/* STORE fast path state. See zrangeStoreTryAppend(). */
|
||||
zskiplistNode *bulk_update[ZSKIPLIST_MAXLEVEL];
|
||||
unsigned long bulk_rank[ZSKIPLIST_MAXLEVEL];
|
||||
int bulk_initialized;
|
||||
int bulk_disarmed;
|
||||
};
|
||||
|
||||
/* Result handler methods for responding the ZRANGE to clients.
|
||||
|
|
@ -3333,12 +3338,108 @@ static void zrangeResultBeginStore(zrange_result_handler *handler, long length)
|
|||
handler->dstobj = zsetTypeCreate(length >= 0 ? length : 0, 0);
|
||||
}
|
||||
|
||||
/* STORE-mode tail-append fast path for forward ZRANGESTORE.
|
||||
*
|
||||
* When the destination is skiplist-encoded and incoming (score, ele) is >=
|
||||
* the current tail, append the new node directly using per-level tail
|
||||
* pointers cached on 'handler'. On first use with a non-empty skiplist
|
||||
* (destination was listpack and converted mid-store, e.g. ZRANGESTORE
|
||||
* BYSCORE/BYLEX), the cache seeds from the existing tail chain. Span
|
||||
* bookkeeping mirrors zslInsertNode() verbatim: level-0 spans are implicit,
|
||||
* level>=1 predecessor span = (new_rank - prev_rank) + 1, new-tail span = 0;
|
||||
* bulk_rank[i] = hops-from-header to bulk_update[i] so the formula stays
|
||||
* invariant across calls.
|
||||
*
|
||||
* Returns 1 if handled, 0 to fall back to zsetAdd() (not skiplist, not
|
||||
* monotonic, or unexpected duplicate). */
|
||||
static int zrangeStoreTryAppend(zrange_result_handler *handler, double score, sds ele) {
|
||||
if (handler->bulk_disarmed) return 0;
|
||||
if (handler->dstobj->encoding != OBJ_ENCODING_SKIPLIST) return 0;
|
||||
zset *zs = handler->dstobj->ptr;
|
||||
zskiplist *zsl = zs->zsl;
|
||||
if (zsl->tail != NULL && zslCompareWithNode(score, ele, zsl->tail) < 0) {
|
||||
handler->bulk_disarmed = 1;
|
||||
return 0;
|
||||
}
|
||||
dictEntryLink bucket;
|
||||
if (dictFindLink(zs->dict, ele, &bucket) != NULL) {
|
||||
handler->bulk_disarmed = 1; /* unexpected duplicate */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!handler->bulk_initialized) {
|
||||
if (zsl->length == 0) {
|
||||
for (int i = 0; i < ZSKIPLIST_MAXLEVEL; i++) {
|
||||
handler->bulk_update[i] = zsl->header;
|
||||
handler->bulk_rank[i] = 0;
|
||||
}
|
||||
} else {
|
||||
/* Walk the existing tail chain so update[]/rank[] match what
|
||||
* zslInsertNode() would have computed; otherwise the first
|
||||
* stitch would overwrite header.forward on a non-empty chain. */
|
||||
zskiplistNode *x = zsl->header;
|
||||
unsigned long r = 0;
|
||||
for (int i = zsl->level - 1; i >= 0; i--) {
|
||||
while (x->level[i].forward != NULL) {
|
||||
r += zslGetNodeSpanAtLevel(x, i);
|
||||
x = x->level[i].forward;
|
||||
}
|
||||
handler->bulk_update[i] = x;
|
||||
handler->bulk_rank[i] = r;
|
||||
}
|
||||
for (int i = zsl->level; i < ZSKIPLIST_MAXLEVEL; i++) {
|
||||
handler->bulk_update[i] = zsl->header;
|
||||
handler->bulk_rank[i] = 0;
|
||||
}
|
||||
}
|
||||
handler->bulk_initialized = 1;
|
||||
}
|
||||
|
||||
int level = zslRandomLevel();
|
||||
zskiplistNode *node = zslCreateNode(zsl, level, score, ele);
|
||||
unsigned long insert_rank = zsl->length; /* 0-indexed position of new node */
|
||||
|
||||
if (level > zsl->level) {
|
||||
for (int i = zsl->level; i < level; i++) {
|
||||
handler->bulk_update[i] = zsl->header;
|
||||
handler->bulk_rank[i] = 0;
|
||||
zslSetNodeSpanAtLevel(zsl->header, i, zsl->length);
|
||||
}
|
||||
zsl->level = level;
|
||||
zslGetNodeInfo(zsl->header)->levels = level;
|
||||
}
|
||||
|
||||
zskiplistNode *prev_level0 = handler->bulk_update[0];
|
||||
for (int i = 0; i < level; i++) {
|
||||
node->level[i].forward = NULL;
|
||||
handler->bulk_update[i]->level[i].forward = node;
|
||||
zslSetNodeSpanAtLevel(node, i, 0);
|
||||
zslSetNodeSpanAtLevel(handler->bulk_update[i], i,
|
||||
(insert_rank - handler->bulk_rank[i]) + 1);
|
||||
handler->bulk_update[i] = node;
|
||||
handler->bulk_rank[i] = insert_rank + 1;
|
||||
}
|
||||
for (int i = level; i < zsl->level; i++) {
|
||||
zslIncrNodeSpanAtLevel(handler->bulk_update[i], i, 1);
|
||||
}
|
||||
node->backward = (prev_level0 == zsl->header) ? NULL : prev_level0;
|
||||
zsl->tail = node;
|
||||
zsl->length++;
|
||||
|
||||
dictSetKeyAtLink(zs->dict, node, &bucket, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void zrangeResultEmitCBufferForStore(zrange_result_handler *handler,
|
||||
const void *value, size_t value_length_in_bytes, double score)
|
||||
{
|
||||
sds ele = sdsnewlen(value, value_length_in_bytes);
|
||||
if (zrangeStoreTryAppend(handler, score, ele)) {
|
||||
sdsfree(ele);
|
||||
return;
|
||||
}
|
||||
double newscore;
|
||||
int retflags = 0;
|
||||
sds ele = sdsnewlen(value, value_length_in_bytes);
|
||||
int retval = zsetAdd(handler->dstobj, score, ele, ZADD_IN_NONE, &retflags, &newscore);
|
||||
sdsfree(ele);
|
||||
serverAssert(retval);
|
||||
|
|
@ -3347,9 +3448,13 @@ static void zrangeResultEmitCBufferForStore(zrange_result_handler *handler,
|
|||
static void zrangeResultEmitLongLongForStore(zrange_result_handler *handler,
|
||||
long long value, double score)
|
||||
{
|
||||
sds ele = sdsfromlonglong(value);
|
||||
if (zrangeStoreTryAppend(handler, score, ele)) {
|
||||
sdsfree(ele);
|
||||
return;
|
||||
}
|
||||
double newscore;
|
||||
int retflags = 0;
|
||||
sds ele = sdsfromlonglong(value);
|
||||
int retval = zsetAdd(handler->dstobj, score, ele, ZADD_IN_NONE, &retflags, &newscore);
|
||||
sdsfree(ele);
|
||||
serverAssert(retval);
|
||||
|
|
|
|||
|
|
@ -2507,6 +2507,65 @@ start_server {tags {"zset"}} {
|
|||
r config set zset-max-listpack-entries $original_max
|
||||
}
|
||||
|
||||
test {ZRANGESTORE forward large skiplist destination - ZRANK + span integrity} {
|
||||
# Exercises the tail-append fast path on a skiplist destination
|
||||
# pre-sized from the range length. Verifies that span bookkeeping
|
||||
# stays consistent by checking rank-based operations (ZRANK,
|
||||
# ZRANGEBYSCORE LIMIT, ZCOUNT) on every 100th element.
|
||||
set original_max [lindex [r config get zset-max-listpack-entries] 1]
|
||||
r config set zset-max-listpack-entries 128
|
||||
r del z1{t} z2{t}
|
||||
# 500 elements -> destination starts as skiplist directly (above threshold).
|
||||
for {set i 0} {$i < 500} {incr i} { r zadd z1{t} $i "e$i" }
|
||||
assert_equal 500 [r zrangestore z2{t} z1{t} 0 -1]
|
||||
assert_encoding skiplist z2{t}
|
||||
assert_equal 500 [r zcard z2{t}]
|
||||
# Probe rank at several positions. ZRANK walks level-i spans to find
|
||||
# an element's 0-indexed rank; any span off-by-one shows up here.
|
||||
foreach idx {0 1 99 100 249 250 498 499} {
|
||||
assert_equal $idx [r zrank z2{t} "e$idx"]
|
||||
assert_equal [expr {499 - $idx}] [r zrevrank z2{t} "e$idx"]
|
||||
}
|
||||
# ZRANGEBYSCORE LIMIT stresses rank-based offset traversal.
|
||||
assert_equal {e100 e101 e102} [r zrangebyscore z2{t} -inf +inf LIMIT 100 3]
|
||||
assert_equal 101 [r zcount z2{t} 200 300]
|
||||
r config set zset-max-listpack-entries $original_max
|
||||
}
|
||||
|
||||
test {ZRANGESTORE mid-store listpack->skiplist conversion - fast path init} {
|
||||
# Covers the corner case where the destination is created as
|
||||
# listpack (BYSCORE / BYLEX pass 0 as length hint to zsetTypeCreate),
|
||||
# fills past zset-max-listpack-entries, converts to skiplist
|
||||
# mid-store, and the tail-append fast path then activates. Must
|
||||
# not corrupt the existing chain and must keep span bookkeeping
|
||||
# consistent for ZRANK.
|
||||
set original_max [lindex [r config get zset-max-listpack-entries] 1]
|
||||
r config set zset-max-listpack-entries 64
|
||||
r del z1{t} z2{t} z3{t}
|
||||
for {set i 0} {$i < 300} {incr i} { r zadd z1{t} $i "m$i" }
|
||||
# BYSCORE: length is not known upfront, so the destination starts
|
||||
# as listpack and converts to skiplist when it crosses 64 elements.
|
||||
assert_equal 300 [r zrangestore z2{t} z1{t} 0 299 BYSCORE]
|
||||
assert_encoding skiplist z2{t}
|
||||
assert_equal 300 [r zcard z2{t}]
|
||||
foreach idx {0 1 63 64 65 150 298 299} {
|
||||
assert_equal $idx [r zrank z2{t} "m$idx"]
|
||||
}
|
||||
assert_equal {m64 m65 m66} [r zrangebyscore z2{t} 64 66]
|
||||
# BYLEX path (all-equal-score zset so lex range is well-defined):
|
||||
# exercises the same mid-store conversion + fast-path init when
|
||||
# the destination starts as listpack.
|
||||
r del z3{t} z4{t}
|
||||
for {set i 0} {$i < 300} {incr i} { r zadd z3{t} 0 [format "x%03d" $i] }
|
||||
assert_equal 300 [r zrangestore z4{t} z3{t} \[x000 \[x299 BYLEX]
|
||||
assert_encoding skiplist z4{t}
|
||||
assert_equal 300 [r zcard z4{t}]
|
||||
foreach idx {0 1 63 64 65 150 298 299} {
|
||||
assert_equal $idx [r zrank z4{t} [format "x%03d" $idx]]
|
||||
}
|
||||
r config set zset-max-listpack-entries $original_max
|
||||
}
|
||||
|
||||
test {ZRANGE invalid syntax} {
|
||||
catch {r zrange z1{t} 0 -1 limit 1 2} err
|
||||
assert_match "*syntax*" $err
|
||||
|
|
|
|||
Loading…
Reference in a new issue