CLUSTERSCAN Command#2934
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## unstable #2934 +/- ##
=============================================
+ Coverage 0 74.83% +74.83%
=============================================
Files 0 129 +129
Lines 0 71630 +71630
=============================================
+ Hits 0 53602 +53602
- Misses 0 18028 +18028
🚀 New features to boost your workflow:
|
b9f30db to
3bbe5d2
Compare
527be33 to
9c8b1de
Compare
There was a problem hiding this comment.
Pull request overview
This PR implements the CLUSTERSCAN command to enable topology-aware scanning across cluster slots, addressing Issue #33. Unlike the node-local SCAN command, CLUSTERSCAN allows clients to scan across the entire cluster by automatically advancing through slots using cursor-encoded slot information.
Changes:
- Implements CLUSTERSCAN command with cursor format
version-{hashtag}-localcursorfor slot-aware routing - Refactors CRC16 slot table from header to source file with accessor function
clusterGetSlotHashtag() - Extends
scanGenericCommandto support slot-specific scanning with custom cursor prefixes - Adds comprehensive test coverage for basic functionality, MATCH/COUNT/TYPE options, MOVED redirects, and SLOT argument
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/cluster.c | Core CLUSTERSCAN implementation including cursor parsing, slot validation, and redirect handling |
| src/db.c | Extended scanGenericCommand signature to support slot filtering and cursor prefixing for CLUSTERSCAN |
| src/crc16_slottable.c | Moved CRC16 slot hashtag table from header to source with accessor function |
| src/crc16_slottable.h | Simplified header to declare clusterGetSlotHashtag function |
| src/commands/clusterscan.json | Command metadata defining arguments, ACL categories, and reply schema |
| src/commands.def | Auto-generated command table entry |
| src/server.h | Added function declarations for clusterscanCommand and clusterscanGetKeys |
| src/module.c | Updated to use clusterGetSlotHashtag accessor |
| src/valkey-benchmark.c | Updated to use clusterGetSlotHashtag accessor |
| src/Makefile | Added crc16_slottable.o to build |
| cmake/Modules/SourceFiles.cmake | Added crc16_slottable.c to CMake build |
| tests/unit/cluster/clusterscan.tcl | Comprehensive test suite covering cursor validation, SLOT argument, redirects, and options |
| tests/support/cluster.tcl | Added clusterscan to plain commands list for cluster client |
| src/t_zset.c, src/t_set.c, src/t_hash.c | Updated scanGenericCommand calls with new parameters |
| .config/typos.toml | Updated to exclude new source file from spell checking |
|
Core meeting:
@valkey-io/core-team Pinging ya'll for the major decision. We discussed it today in the US/EU meeting, but pinging other folks. |
be9e960 to
27316db
Compare
Unlike `SCAN` which is local to a single node, `CLUSTERSCAN` provides a
mechanism that helps clients iterate across slot boundaries and handles
`MOVED` redirections.
**Key details**
* Global cluster iteration via `fingerprint-{hashtag}-cursor`
* Scan one slot at a time
* Start the CLUSTERSCAN with 0
* SLOT argument for parallel scanning of multiple slots
* Re-use scanGenericCommand for the response
**Cursor format:** `fingerprint-{hashtag}-localcursor`
- Fingerprint is a hash of the node's DB seed that identifies the
current memory layout. On mismatch, scan restarts from cursor 0
rather than returning an error.
- Fingerprint 0 indicates a cross slot cursor (e.g., initial cursor
or slot transition) where validation is skipped.
- Hashtag encodes the target slot
- Local cursor tracks position within the slot
**Usage:**
CLUSTERSCAN <cursor> [MATCH pattern] [COUNT count] [TYPE type] [SLOT number]
CLUSTERSCAN 0 # Start scanning from slot 0
CLUSTERSCAN <cursor> # Continue from cursor
CLUSTERSCAN 0 SLOT 1000 # Start scanning specific slot
CLUSTERSCAN <cursor> MATCH user:* COUNT 100
This implements valkey-io#33
Signed-off-by: nmvk <r@nmvk.com>
NOT_KEY handles this Signed-off-by: nmvk <r@nmvk.com>
Update tests/unit/cluster/clusterscan.tcl Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech> Signed-off-by: Raghav <r@nmvk.com>
Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
|
should we close #33 ? |
Implemented CLUSTERSCAN command for topology-aware scanning
Unlike `SCAN` which is local to a single node, `CLUSTERSCAN` provides a
mechanism that helps clients iterate across slot boundaries and handles
`MOVED` redirections.
**Key details**
* Global cluster iteration via `fingerprint-{hashtag}-cursor`
* Scan one slot at a time
* Start the CLUSTERSCAN with 0
* SLOT argument for parallel scanning of multiple slots
* Re-use scanGenericCommand for the response
**Cursor format:** `fingerprint-{hashtag}-localcursor`
- Fingerprint is a hash of the node's DB seed that identifies the
current memory layout. On mismatch, scan restarts from cursor 0
rather than returning an error.
- Fingerprint 0 indicates a cross slot cursor (e.g., initial cursor
or slot transition) where validation is skipped.
- Hashtag encodes the target slot
- Local cursor tracks position within the slot
**Usage:**
```
CLUSTERSCAN <cursor> [MATCH pattern] [COUNT count] [TYPE type] [SLOT number]
```
```
CLUSTERSCAN 0 # Start scanning from slot 0
CLUSTERSCAN <cursor> # Continue from cursor
CLUSTERSCAN 0 SLOT 1000 # Start scanning specific slot
CLUSTERSCAN <cursor> MATCH user:* COUNT 100
```
---------
Signed-off-by: nmvk <r@nmvk.com>
Signed-off-by: Raghav <r@nmvk.com>
Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Co-authored-by: Madelyn Olson <madelyneolson@gmail.com>
Signed-off-by: Daniel Lemire <daniel@lemire.me>
This reverts commit 8a4a7b2.
Docuementation changes for the CLUSTERSCAN valkey-io/valkey#2934 Signed-off-by: nmvk <r@nmvk.com>
- Fix release date to Mon Mar 18 2026 - Fix typos: duplicate 'load', 'keyes' -> 'keys', duplicate 'INFO' - Remove reverted contributor (arshidkv12, valkey-io#3137) - Add 7 new release-notes entries from upstream/unstable merge: CLUSTERSCAN (valkey-io#2934), MSETEX (valkey-io#3121), availability-zone (valkey-io#3156), stream range optimization (valkey-io#3002), RDB as AOF preamble (valkey-io#1901), unsigned 64-bit module config (valkey-io#1546), fast_float -> ffc (valkey-io#3329) Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
Docuementation changes for the CLUSTERSCAN valkey-io/valkey#2934 Signed-off-by: nmvk <r@nmvk.com>
Implemented CLUSTERSCAN command for topology-aware scanning
Unlike `SCAN` which is local to a single node, `CLUSTERSCAN` provides a
mechanism that helps clients iterate across slot boundaries and handles
`MOVED` redirections.
**Key details**
* Global cluster iteration via `fingerprint-{hashtag}-cursor`
* Scan one slot at a time
* Start the CLUSTERSCAN with 0
* SLOT argument for parallel scanning of multiple slots
* Re-use scanGenericCommand for the response
**Cursor format:** `fingerprint-{hashtag}-localcursor`
- Fingerprint is a hash of the node's DB seed that identifies the
current memory layout. On mismatch, scan restarts from cursor 0
rather than returning an error.
- Fingerprint 0 indicates a cross slot cursor (e.g., initial cursor
or slot transition) where validation is skipped.
- Hashtag encodes the target slot
- Local cursor tracks position within the slot
**Usage:**
```
CLUSTERSCAN <cursor> [MATCH pattern] [COUNT count] [TYPE type] [SLOT number]
```
```
CLUSTERSCAN 0 # Start scanning from slot 0
CLUSTERSCAN <cursor> # Continue from cursor
CLUSTERSCAN 0 SLOT 1000 # Start scanning specific slot
CLUSTERSCAN <cursor> MATCH user:* COUNT 100
```
---------
Signed-off-by: nmvk <r@nmvk.com>
Signed-off-by: Raghav <r@nmvk.com>
Signed-off-by: Madelyn Olson <madelyneolson@gmail.com>
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Co-authored-by: Madelyn Olson <madelyneolson@gmail.com>
Instead of scanning one slot at a time `CLUSTERSCAN` now scans the
entire contiguous range of slots owned by the current node.
Implementation details
* Re-sharding safe as the hash slot is updated based on the local cursor
position.
* Fingerprint remains stable across the entire contiguous slot range
instead of being reset per slot.
* Parsing/validation of parameters for the SCAN commands is refactored
and moved to a separate function.
```
> CLUSTERSCAN 0
"0-{06S}-0" # start at slot 0
> CLUSTERSCAN 0-{06S}-0
"aBcDeF-{06S}-48" # scanning slot 0...
> CLUSTERSCAN aBcDeF-{06S}-48
"aBcDeF-{1Y7}-16" # slot 0 done, continues to slot 6 (same node hence FP is unchanged)
> CLUSTERSCAN aBcDeF-{1Y7}-16
"aBcDeF-{0or}-32" # slot 6 done, continues to slot 100 (same node hence FP is unchanged)
...
> CLUSTERSCAN aBcDeF-{...}-64
"0-{8YG}-0" # Current continuous slot boundary reached hence cross-node transition
```
Follow-up of #2934
---------
Signed-off-by: nmvk <r@nmvk.com>
Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Instead of scanning one slot at a time `CLUSTERSCAN` now scans the
entire contiguous range of slots owned by the current node.
Implementation details
* Re-sharding safe as the hash slot is updated based on the local cursor
position.
* Fingerprint remains stable across the entire contiguous slot range
instead of being reset per slot.
* Parsing/validation of parameters for the SCAN commands is refactored
and moved to a separate function.
```
> CLUSTERSCAN 0
"0-{06S}-0" # start at slot 0
> CLUSTERSCAN 0-{06S}-0
"aBcDeF-{06S}-48" # scanning slot 0...
> CLUSTERSCAN aBcDeF-{06S}-48
"aBcDeF-{1Y7}-16" # slot 0 done, continues to slot 6 (same node hence FP is unchanged)
> CLUSTERSCAN aBcDeF-{1Y7}-16
"aBcDeF-{0or}-32" # slot 6 done, continues to slot 100 (same node hence FP is unchanged)
...
> CLUSTERSCAN aBcDeF-{...}-64
"0-{8YG}-0" # Current continuous slot boundary reached hence cross-node transition
```
Follow-up of #2934
---------
Signed-off-by: nmvk <r@nmvk.com>
Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Instead of scanning one slot at a time `CLUSTERSCAN` now scans the
entire contiguous range of slots owned by the current node.
Implementation details
* Re-sharding safe as the hash slot is updated based on the local cursor
position.
* Fingerprint remains stable across the entire contiguous slot range
instead of being reset per slot.
* Parsing/validation of parameters for the SCAN commands is refactored
and moved to a separate function.
```
> CLUSTERSCAN 0
"0-{06S}-0" # start at slot 0
> CLUSTERSCAN 0-{06S}-0
"aBcDeF-{06S}-48" # scanning slot 0...
> CLUSTERSCAN aBcDeF-{06S}-48
"aBcDeF-{1Y7}-16" # slot 0 done, continues to slot 6 (same node hence FP is unchanged)
> CLUSTERSCAN aBcDeF-{1Y7}-16
"aBcDeF-{0or}-32" # slot 6 done, continues to slot 100 (same node hence FP is unchanged)
...
> CLUSTERSCAN aBcDeF-{...}-64
"0-{8YG}-0" # Current continuous slot boundary reached hence cross-node transition
```
Follow-up of #2934
---------
Signed-off-by: nmvk <r@nmvk.com>
Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
This bug was introduced in #3366. Before PR #3366, hash-seed config was applied directly via hashtableSetHashFunctionSeed(), so clusterscanFingerprint() correctly used hash_function_seed to derive the fingerprint. ```c if (server.hash_seed != NULL) { memset(hashseed, 0, sizeof(hashseed)); getHashSeedFromString(hashseed, sizeof(hashseed), server.hash_seed); hashtableSetHashFunctionSeed(hashseed); } ``` PR #3366 introduced a separate configurable_hash_seed for data hashtables and kept hash_function_seed as a random per-process value. ```c /* Set the configured hash seed used by data hashtables (keys, sets, zsets, * hashes) or use the random seed if not configured. */ if (server.hash_seed) { uint8_t seed[16] = {0}; getHashSeedFromString(seed, sizeof(seed), server.hash_seed); setConfigurableHashSeed(seed); } else { setConfigurableHashSeed(hashtableGetHashFunctionSeed()); } ``` However, clusterscanFingerprint() was not updated accordingly — it still reads hash_function_seed, which is now random on every node. This makes fingerprints differ across nodes even when they share the same hash-seed config, causing cursors to restart on failover. CLUSTERSCAN was introduced in #2934. Signed-off-by: Binbin <binloveplay1314@qq.com>
…3675) No bug exists — all NOT_KEY commands (CLUSTERSCAN, SSUBSCRIBE, SPUBLISH, SUNSUBSCRIBE) have NULL getkeys_proc, so doesCommandHaveKeys returns 0 and ACL correctly skips key checks. This PR makes the handling more explicit and defensive for future commands that may combine getkeys_proc with NOT_KEY key-specs. Changes: - Refactor doesCommandHaveKeys() from a one-line ternary into clearer if-else branches. Add an explicit check: if a command has getkeys_proc but all its key-specs are NOT_KEY, treat it as having no real keys. - Add a defensive NOT_KEY check in ACLSelectorCheckKey() to skip key-pattern ACL validation for NOT_KEY entries, consistent with the existing skip in getKeysUsingKeySpecs(). - Add tests: verify COMMAND GETKEYS/GETKEYSANDFLAGS report "no key arguments" for NOT_KEY commands; verify restricted ACL users can run CLUSTERSCAN without key-permission errors. Related: #2934, #3699 Signed-off-by: Binbin <binloveplay1314@qq.com>
Implemented CLUSTERSCAN command for topology-aware scanning
Unlike
SCANwhich is local to a single node,CLUSTERSCANprovides amechanism that helps clients iterate across slot boundaries and handles
MOVEDredirections.Key details
fingerprint-{hashtag}-cursorCursor format:
fingerprint-{hashtag}-localcursorcurrent memory layout. On mismatch, scan restarts from cursor 0
rather than returning an error.
or slot transition) where validation is skipped.
Usage:
This closes #33