Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0e3493e
zset: fix NULL deref in zslIsInLexRange on empty skiplist
rainsupreme Apr 26, 2026
3d2a789
ordered-index: add interface, skiplist backend, and unit tests
rainsupreme Apr 26, 2026
bd0c609
ordered-index: address interface review feedback
rainsupreme May 13, 2026
62db345
skiplist: fix zslDetachNode search and deleteRange callback ownership
rainsupreme May 16, 2026
268687c
ordered-index: address PR 2 self-review feedback
rainsupreme May 18, 2026
02b9f6c
tests: fix CI failures (clang-format, macOS warnings, ASAN leaks)
rainsupreme May 19, 2026
517895b
ci: fix clang-format and macOS -Wfloat-equal warnings
rainsupreme May 19, 2026
4016e00
zset: convert t_zset.c to OrderedIndex interface
rainsupreme May 15, 2026
d6c1746
zset: convert ZUNION/ZINTER/ZDIFF to OrderedIndex detached-item API
rainsupreme May 16, 2026
578c6a1
zset: convert all remaining files to OrderedIndex interface
rainsupreme May 18, 2026
7b4edd0
tests: add integration tests for skiplist-encoded zset paths
rainsupreme Apr 27, 2026
bb7d41d
tests: add cluster:skip tag to skiplist integration tests
rainsupreme May 19, 2026
8b9797c
zset: fix sds assumptions on opaque OrderedIndex elements
rainsupreme May 26, 2026
7494ab0
defrag: use orderedIndexScanDefrag instead of raw skiplist access
rainsupreme May 26, 2026
add9eea
server.h: remove dead zskiplist forward declaration
rainsupreme May 26, 2026
f14e3db
skiplist: change zslNext/zslPrev to return pointer directly
rainsupreme May 26, 2026
a8fd1c5
debug: use skiplistGetHeight instead of reaching into opaque struct
rainsupreme May 26, 2026
2802d20
skiplist.h: remove unused stdbool.h include
rainsupreme May 26, 2026
5a4fbbd
geo: simplify score range iteration to plain while loop
rainsupreme May 26, 2026
e4b5522
server.c: fix stale skiplist comment in zsetHashtableType
rainsupreme May 26, 2026
9da4089
t_zset.c: update stale skiplist references in comments
rainsupreme May 26, 2026
f8aeac5
zset: relocate range comparison utils from skiplist to t_zset
rainsupreme May 27, 2026
8ac098e
skiplist_ordered_index: use OrderedIndexDefragCallback typedef
rainsupreme May 27, 2026
15a2bfe
t_zset: change zzlInsertAt to accept ptr+len instead of sds
rainsupreme May 27, 2026
81e69f7
t_zset: eliminate temp sds in listpack to ordered index conversion
rainsupreme May 27, 2026
083712c
t_zset: move misplaced ZREMRANGE comment to correct function
rainsupreme May 27, 2026
77a02e4
zset: refactor lex comparison to accept ptr+len
rainsupreme May 27, 2026
9ef73da
ci: fix unused variable, uninitialized warning, and clang-format
rainsupreme May 27, 2026
d80a7f3
tests: add fixture helpers and reduce boilerplate in ordered index tests
rainsupreme May 27, 2026
1e28aa1
tests: DRY refactoring and clean coding improvements for ordered inde…
rainsupreme May 27, 2026
ae4d0ab
unit tests: reduce C++ idioms in ordered index tests
rainsupreme May 28, 2026
140665a
fix: assert defrag hashtable remap + guard errmsg buffer
rainsupreme May 28, 2026
6738494
tests: add with_config helper for failure-safe config restoration
rainsupreme May 28, 2026
97fa9fc
fix: use borrowed bytes path in zuiNext for ordered index elements
rainsupreme May 28, 2026
f43f63f
fix: clarify orderedIndexUpdateScore return contract
rainsupreme May 28, 2026
0f6c18a
unit tests: use --seed flag for fuzz test reproducibility
rainsupreme May 28, 2026
11370f7
ordered-index: fix OnDelete callback contract to notification-before-…
rainsupreme Jun 10, 2026
e3fdcbd
tests: add LexRangeSentinels test for shared.minstring/maxstring
rainsupreme Jun 10, 2026
b695c0c
Fix ASAN memory leaks in LexRangeSentinels unit test
rainsupreme Jun 11, 2026
64fd590
Address review comments: underflow guard and const fix
rainsupreme Jun 11, 2026
4192dbe
Add O(1) orderedIndexGetFirst/GetLast, use in ZPOP
rainsupreme Jun 11, 2026
7a7c206
Eliminate C++ STL from ordered index unit tests
rainsupreme Jun 11, 2026
a6ec45f
Add unit test for orderedIndexGetFirst/GetLast
rainsupreme Jun 11, 2026
ec350f3
Use explicit (ptr,len) hash/compare for zset hashtable
rainsupreme Jun 16, 2026
de108c0
Parameterize OnDelete and RangeDelete tests over OrderedIndexTestApi
rainsupreme Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmake/Modules/SourceFiles.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ set(VALKEY_SERVER_SRCS
${CMAKE_SOURCE_DIR}/src/t_set.c
${CMAKE_SOURCE_DIR}/src/t_zset.c
${CMAKE_SOURCE_DIR}/src/skiplist.c
${CMAKE_SOURCE_DIR}/src/skiplist_ordered_index.c
${CMAKE_SOURCE_DIR}/src/ordered_index.c
${CMAKE_SOURCE_DIR}/src/t_hash.c
${CMAKE_SOURCE_DIR}/src/config.c
${CMAKE_SOURCE_DIR}/src/aof.c
Expand Down
2 changes: 2 additions & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,8 @@ ENGINE_SERVER_OBJ = \
sha1.o \
sha256.o \
siphash.o \
skiplist_ordered_index.o \
ordered_index.o \
socket.o \
sort.o \
sparkline.o \
Expand Down
10 changes: 6 additions & 4 deletions src/aof.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*/

#include "server.h"
#include "skiplist.h"
#include "ordered_index.h"
#include "bio.h"
#include "rio.h"
#include "functions.h"
Expand Down Expand Up @@ -2027,7 +2027,7 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
hashtableInitIterator(&iter, zs->ht, 0);
void *next;
while (hashtableNext(&iter, &next)) {
zskiplistNode *node = next;
OrderedIndexItem *node = next;
if (count == 0) {
int cmd_items = (items > AOF_REWRITE_ITEMS_PER_CMD) ? AOF_REWRITE_ITEMS_PER_CMD : items;

Expand All @@ -2037,8 +2037,10 @@ int rewriteSortedSetObject(rio *r, robj *key, robj *o) {
return 0;
}
}
sds ele = zslGetNodeElement(node);
if (!rioWriteBulkDouble(r, node->score) || !rioWriteBulkString(r, ele, sdslen(ele))) {
const char *ele;
size_t ele_len;
orderedIndexGetElementRaw(node, &ele, &ele_len);
if (!rioWriteBulkDouble(r, orderedIndexGetScore(node)) || !rioWriteBulkString(r, ele, ele_len)) {
hashtableCleanupIterator(&iter);
return 0;
}
Expand Down
21 changes: 13 additions & 8 deletions src/db.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*/

#include "server.h"
#include "skiplist.h"
#include "ordered_index.h"
#include "cluster.h"
#include "cluster_migrateslots.h"
#include "latency.h"
Expand Down Expand Up @@ -1052,6 +1052,8 @@ void hashtableScanCallback(void *privdata, void *entry) {
scanData *data = (scanData *)privdata;
stringRef val = {NULL, 0};
sds key = NULL;
const char *zset_ptr = NULL;
size_t zset_ele_len = 0;

robj *o = data->o;
data->sampled++;
Expand All @@ -1064,9 +1066,8 @@ void hashtableScanCallback(void *privdata, void *entry) {
if (o->type == OBJ_SET) {
key = (sds)entry;
} else if (o->type == OBJ_ZSET) {
zskiplistNode *node = (zskiplistNode *)entry;
key = zslGetNodeElement(node);
/* zset data is copied after filtering by key */
orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &zset_ptr, &zset_ele_len);
/* zset data is copied after filtering */
} else if (o->type == OBJ_HASH) {
key = entryGetField(entry);
if (!data->only_keys) {
Expand All @@ -1078,7 +1079,9 @@ void hashtableScanCallback(void *privdata, void *entry) {

/* Filter element if it does not match the pattern. */
if (data->pattern) {
if (!stringmatchlen(data->pattern, sdslen(data->pattern), key, sdslen(key), 0)) {
const char *match_ptr = (o->type == OBJ_ZSET) ? zset_ptr : key;
size_t match_len = (o->type == OBJ_ZSET) ? zset_ele_len : sdslen(key);
if (!stringmatchlen(data->pattern, sdslen(data->pattern), match_ptr, match_len, 0)) {
return;
}
}
Expand All @@ -1087,11 +1090,13 @@ void hashtableScanCallback(void *privdata, void *entry) {
* allocations. */
if (o->type == OBJ_ZSET) {
/* zset data is copied */
zskiplistNode *node = (zskiplistNode *)entry;
key = sdsdup(zslGetNodeElement(node));
const char *ptr;
size_t ele_len;
orderedIndexGetElementRaw((const OrderedIndexItem *)entry, &ptr, &ele_len);
key = sdsnewlen(ptr, ele_len);
if (!data->only_keys) {
char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf, sizeof(buf), node->score, LD_STR_AUTO);
int len = ld2string(buf, sizeof(buf), orderedIndexGetScore((const OrderedIndexItem *)entry), LD_STR_AUTO);
sds tmp = sdsnewlen(buf, len);
val.buf = (const char *)tmp;
val.len = sdslen(tmp);
Expand Down
19 changes: 12 additions & 7 deletions src/debug.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*/

#include "server.h"
#include "skiplist.h"
#include "ordered_index.h"
#include "util.h"
#include "sha1.h" /* SHA1 is used for DEBUG DIGEST */
#include "crc64.h"
Expand Down Expand Up @@ -217,12 +217,14 @@ void xorObjectDigest(serverDb *db, robj *keyobj, unsigned char *digest, robj *o)

void *next;
while (hashtableNext(&iter, &next)) {
zskiplistNode *node = next;
const int len = fpconv_dtoa(node->score, buf);
OrderedIndexItem *node = next;
const char *ele;
size_t ele_len;
orderedIndexGetElementRaw(node, &ele, &ele_len);
const int len = fpconv_dtoa(orderedIndexGetScore(node), buf);
buf[len] = '\0';
memset(eledigest, 0, 20);
sds ele = zslGetNodeElement(node);
mixDigest(eledigest, ele, sdslen(ele));
mixDigest(eledigest, ele, ele_len);
mixDigest(eledigest, buf, strlen(buf));
xorDigest(digest, eledigest, 20);
}
Expand Down Expand Up @@ -1184,8 +1186,11 @@ void serverLogObjectDebugInfo(const robj *o) {
serverLog(LL_WARNING, "Hash size: %d", (int)hashTypeLength(o));
} else if (o->type == OBJ_ZSET) {
serverLog(LL_WARNING, "Sorted set size: %d", (int)zsetLength(o));
if (o->encoding == OBJ_ENCODING_SKIPLIST)
serverLog(LL_WARNING, "Skiplist level: %d", (int)((const zset *)o->ptr)->zsl->level);
if (o->encoding == OBJ_ENCODING_SKIPLIST) {
/* Not declared in ordered_index.h — debug-only introspection. */
extern int orderedIndexGetDepth(OrderedIndex * oi);
serverLog(LL_WARNING, "Index depth: %d", orderedIndexGetDepth(((const zset *)o->ptr)->oi));
}
} else if (o->type == OBJ_STREAM) {
serverLog(LL_WARNING, "Stream size: %d", (int)streamLength(o));
}
Expand Down
77 changes: 12 additions & 65 deletions src/defrag.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
*/

#include "server.h"
#include "skiplist.h"
#include "ordered_index.h"
#include "hashtable.h"
#include "eval.h"
#include "script.h"
Expand Down Expand Up @@ -237,61 +237,13 @@ robj *activeDefragStringOb(robj *ob) {
return new_robj;
}

/* Internal function used by zslDefrag */
static void zslUpdateNode(zskiplist *zsl, zskiplistNode *oldnode, zskiplistNode *newnode, zskiplistNode **update) {
int i;
for (i = 0; i < zslGetHeight(zsl); i++) {
if (update[i]->level[i].forward == oldnode) update[i]->level[i].forward = newnode;
}
serverAssert(zslGetHeader(zsl) != oldnode);
if (newnode->level[0].forward) {
serverAssert(newnode->level[0].forward->backward == oldnode);
newnode->level[0].forward->backward = newnode;
} else {
serverAssert(zslGetTail(zsl) == oldnode);
zslSetTail(zsl, newnode);
}
}

/* Hashtable scan callback for sorted set. It defragments a single skiplist
* node, updates skiplist pointers, and updates the hashtable pointer to the
* node. */
static void activeDefragZsetNode(void *privdata, void *entry_ref) {
zskiplist *zsl = privdata;
zskiplistNode **node_ref = (zskiplistNode **)entry_ref;
zskiplistNode *node = *node_ref;

size_t allocation_size;
zskiplistNode *newnode = activeDefragAllocWithoutFree(node, &allocation_size);
if (newnode == NULL) return;

const double score = node->score;

/* find skiplist pointers that need to be updated if we end up moving the
* skiplist node. */
sds ele = zslGetNodeElement(node);
zskiplistNode *update[ZSKIPLIST_MAXLEVEL];
zskiplistNode *x = zslGetHeader(zsl);
for (int i = zslGetHeight(zsl) - 1; i >= 0; i--) {
/* stop when we've reached the end of this level or the next node comes
* after our target in sorted order. Even though defrag replacements does not impact the skip list order,
* when scores are equal, we MUST compare elements lexicographically to maintain correct skip list ordering.
* Otherwise we might miss locating the entry. */
zskiplistNode *next = x->level[i].forward;
while (next &&
(next->score < score ||
(next->score == score && sdscmp(zslGetNodeElement(next), ele) < 0))) {
x = next;
next = x->level[i].forward;
}
update[i] = x;
}
/* should have arrived at intended node */
serverAssert(x->level[0].forward == node);

zslUpdateNode(zsl, node, newnode, update);
*node_ref = newnode; /* update hashtable pointer */
allocatorDefragFree(node, allocation_size);
/* Callback for orderedIndexScanDefrag — when a node is reallocated, update
* the hashtable's pointer to it. */
static void defragZsetNodeCallback(OrderedIndexItem *old_item, OrderedIndexItem *new_item, void *ctx) {
hashtable *ht = ctx;
bool replaced = hashtableReplaceReallocatedEntry(ht, old_item, new_item);
serverAssert(replaced);
server.stat_active_defrag_scanned++;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#define DEFRAG_SDS_DICT_NO_VAL 0
Expand Down Expand Up @@ -434,15 +386,10 @@ static long scanLaterList(robj *ob, unsigned long *cursor, monotime endtime) {
return 0;
}

static void scanLaterZsetCallback(void *privdata, void *element_ref) {
activeDefragZsetNode(privdata, element_ref);
server.stat_active_defrag_scanned++;
}

static void scanLaterZset(robj *ob, unsigned long *cursor) {
serverAssert(ob->type == OBJ_ZSET && ob->encoding == OBJ_ENCODING_SKIPLIST);
zset *zs = (zset *)objectGetVal(ob);
*cursor = hashtableScanDefrag(zs->ht, *cursor, scanLaterZsetCallback, zs->zsl, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF);
*cursor = orderedIndexScanDefrag(zs->oi, *cursor, defragZsetNodeCallback, zs->ht, activeDefragAlloc);
}

/* Used as hashtable scan callback when all we need is to defrag the hashtable
Expand Down Expand Up @@ -482,12 +429,12 @@ static void defragZsetSkiplist(robj *ob) {
zset *zs = (zset *)objectGetVal(ob);

zset *newzs;
zskiplist *newzsl;
if ((newzs = activeDefragAlloc(zs))) {
objectSetVal(ob, newzs);
zs = newzs;
}
if ((newzsl = activeDefragAlloc(zs->zsl))) zs->zsl = newzsl;
OrderedIndex *newoi = orderedIndexDefragInternals(zs->oi, activeDefragAlloc);
if (newoi) zs->oi = newoi;

hashtable *newtable;
if ((newtable = hashtableDefragTables(zs->ht, activeDefragAlloc))) zs->ht = newtable;
Expand All @@ -497,7 +444,7 @@ static void defragZsetSkiplist(robj *ob) {
else {
unsigned long cursor = 0;
do {
cursor = hashtableScanDefrag(zs->ht, cursor, activeDefragZsetNode, zs->zsl, activeDefragAlloc, HASHTABLE_SCAN_EMIT_REF);
cursor = orderedIndexScanDefrag(zs->oi, cursor, defragZsetNodeCallback, zs->ht, activeDefragAlloc);
} while (cursor != 0);
}
}
Expand Down
34 changes: 16 additions & 18 deletions src/geo.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@
*/

#include "geo.h"
#include "skiplist.h"
#include "ordered_index.h"
#include "geohash_helper.h"
#include "debugmacro.h"
#include "pqsort.h"

/* Things exported from t_zset.c only for geo.c, since it is the only other
* part of the server that requires close zset introspection. */
unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range);
int zslValueLteMax(double value, zrangespec *spec);

/* ====================================================================
* This file implements the following commands:
Expand Down Expand Up @@ -295,7 +294,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo
score = zzlGetScore(sptr);

/* If we fell out of range, break. */
if (!zslValueLteMax(score, &range)) break;
if (!zsetScoreLteMax(score, &range)) break;

vstr = lpGetValue(eptr, &vlen, &vlong);
if (geoWithinShape(shape, score, xy, &distance) == C_OK) {
Expand All @@ -308,26 +307,25 @@ int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, geo
}
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = objectGetVal(zobj);
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
OrderedIndexIterator iter;
orderedIndexInitIterator(&iter, zs->oi);
orderedIndexSeekToScoreRange(&iter, range.min, range.max, range.minex, range.maxex, 0);
OrderedIndexItem *ln;

if ((ln = zslNthInRange(zsl, &range, 0, NULL)) == NULL) {
/* Nothing exists starting at our min. No results. */
return 0;
}

while (ln) {
while ((ln = orderedIndexNext(&iter)) != NULL) {
double xy[2];
double distance = 0;
double score = orderedIndexGetScore(ln);
/* Abort when the node is no longer in range. */
if (!zslValueLteMax(ln->score, &range)) break;
if (geoWithinShape(shape, ln->score, xy, &distance) == C_OK) {
if (!zsetScoreLteMax(score, &range)) break;
if (geoWithinShape(shape, score, xy, &distance) == C_OK) {
/* Append the new element. */
sds ele = zslGetNodeElement(ln);
geoArrayAppend(ga, xy, distance, ln->score, sdsdup(ele));
const char *ele;
size_t ele_len;
orderedIndexGetElementRaw(ln, &ele, &ele_len);
geoArrayAppend(ga, xy, distance, score, sdsnewlen(ele, ele_len));
}
if (ga->used && limit && ga->used >= limit) break;
ln = ln->level[0].forward;
}
}
return ga->used - origincount;
Expand Down Expand Up @@ -833,15 +831,15 @@ void georadiusGeneric(client *c, int srcKeyIndex, int flags) {
}

for (i = 0; i < returned_items; i++) {
zskiplistNode *znode;
OrderedIndexItem *znode;
geoPoint *gp = ga.array + i;
gp->dist /= shape.conversion; /* Fix according to unit. */
double score = storedist ? gp->dist : gp->score;
size_t elelen = sdslen(gp->member);

if (maxelelen < elelen) maxelelen = elelen;
totelelen += elelen;
znode = zslInsert(zs->zsl, score, gp->member);
znode = orderedIndexInsert(zs->oi, score, gp->member, elelen);
serverAssert(hashtableAdd(zs->ht, znode));
sdsfree(gp->member);
gp->member = NULL;
Expand Down
4 changes: 2 additions & 2 deletions src/lazyfree.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include "server.h"
#include "skiplist.h"
#include "ordered_index.h"
#include "bio.h"
#include "functions.h"
#include "cluster.h"
Expand Down Expand Up @@ -144,7 +144,7 @@ size_t lazyfreeGetFreeEffort(robj *key, robj *obj, int dbid) {
return hashtableSize(ht);
} else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = objectGetVal(obj);
return zslGetLength(zs->zsl);
return orderedIndexLength(zs->oi);
} else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HASHTABLE) {
hashtable *ht = objectGetVal(obj);
return hashtableSize(ht);
Expand Down
Loading
Loading