Context (#517 shard-owners, found by the PR4 adversarial review)
In cluster_mode = shard-owners the node advertises its N internal shards as N cluster nodes (one per port). The keyed data path matches Redis Cluster exactly (owner port serves locally, wrong port gets -MOVED, cross-slot gets -CROSSSLOT). But the WHOLE-KEYSPACE commands are exempt from cluster redirect (CommandClass::WholeKeyspace returns early in cluster_redirect, serve.rs) and fan out across ALL internal shards regardless of which per-shard port the request arrived on:
KEYS / SCAN return the full keyspace from every port (a per-node aggregator sees every key N times)
DBSIZE returns the GLOBAL count from every port (a per-node sum over-counts by N)
RANDOMKEY samples globally instead of node-locally
FLUSHDB / FLUSHALL flush everything from any port (matches operator intent, but a per-node flush tool would flush N times -- idempotent, harmless)
A real Redis Cluster node answers these only for ITS OWN slots. redis-cli --cluster, and cluster client libraries that iterate SCAN per node, will observe the divergence.
Why the fix is natural post-#520
Since the slot-owner alignment (#520), shard i's store contains EXACTLY the keys in its slot range (every write routes to the owning shard). So "scope whole-keyspace commands to the connecting port's shard" is precisely "answer from the HOME shard only, skip the fan-out" -- the data is already partitioned correctly. No per-key slot filtering is needed.
Proposed change
In shard-owners mode only (ctx.cluster_mode() == ShardOwners), route WholeKeyspace commands as HOME-ONLY instead of fan-out:
DBSIZE / KEYS / RANDOMKEY: read the home shard's store only.
SCAN: the composite cursor already encodes the shard index; in shard-owners mode pin the cursor to the home shard (start there, end when the home shard is exhausted rather than advancing to the next shard).
FLUSHDB / FLUSHALL: home-only to match per-node Redis Cluster semantics (an operator flushing the whole dataset uses a cluster-aware tool that visits every node, exactly as with real Redis Cluster).
- Static/Raft/non-cluster modes: byte-unchanged (fan-out stays).
Also noted (pre-existing, lower priority)
Sharded pub/sub (SPUBLISH/SSUBSCRIBE) broadcasts across all shards rather than per-slot-owner (a fidelity gap delivering a superset of messages; no loss). Consider scoping alongside this work or splitting out.
Acceptance
- In shard-owners mode with N=4:
DBSIZE on port base+i returns only shard i's count; the sum over the 4 ports equals the total; KEYS * per port returns disjoint sets whose union is the keyspace; SCAN on a port enumerates exactly that port's keys and terminates; FLUSHDB on one port leaves the other shards' keys intact.
- Static mode: all whole-keyspace tests pass unchanged.
- The CHANGELOG "KNOWN DIVERGENCE" note is replaced by the fixed semantics.
Context (#517 shard-owners, found by the PR4 adversarial review)
In
cluster_mode = shard-ownersthe node advertises its N internal shards as N cluster nodes (one per port). The keyed data path matches Redis Cluster exactly (owner port serves locally, wrong port gets-MOVED, cross-slot gets-CROSSSLOT). But the WHOLE-KEYSPACE commands are exempt from cluster redirect (CommandClass::WholeKeyspacereturns early incluster_redirect, serve.rs) and fan out across ALL internal shards regardless of which per-shard port the request arrived on:KEYS/SCANreturn the full keyspace from every port (a per-node aggregator sees every key N times)DBSIZEreturns the GLOBAL count from every port (a per-node sum over-counts by N)RANDOMKEYsamples globally instead of node-locallyFLUSHDB/FLUSHALLflush everything from any port (matches operator intent, but a per-node flush tool would flush N times -- idempotent, harmless)A real Redis Cluster node answers these only for ITS OWN slots.
redis-cli --cluster, and cluster client libraries that iterate SCAN per node, will observe the divergence.Why the fix is natural post-#520
Since the slot-owner alignment (#520), shard
i's store contains EXACTLY the keys in its slot range (every write routes to the owning shard). So "scope whole-keyspace commands to the connecting port's shard" is precisely "answer from the HOME shard only, skip the fan-out" -- the data is already partitioned correctly. No per-key slot filtering is needed.Proposed change
In shard-owners mode only (
ctx.cluster_mode() == ShardOwners), routeWholeKeyspacecommands as HOME-ONLY instead of fan-out:DBSIZE/KEYS/RANDOMKEY: read the home shard's store only.SCAN: the composite cursor already encodes the shard index; in shard-owners mode pin the cursor to the home shard (start there, end when the home shard is exhausted rather than advancing to the next shard).FLUSHDB/FLUSHALL: home-only to match per-node Redis Cluster semantics (an operator flushing the whole dataset uses a cluster-aware tool that visits every node, exactly as with real Redis Cluster).Also noted (pre-existing, lower priority)
Sharded pub/sub (
SPUBLISH/SSUBSCRIBE) broadcasts across all shards rather than per-slot-owner (a fidelity gap delivering a superset of messages; no loss). Consider scoping alongside this work or splitting out.Acceptance
DBSIZEon port base+i returns only shard i's count; the sum over the 4 ports equals the total;KEYS *per port returns disjoint sets whose union is the keyspace;SCANon a port enumerates exactly that port's keys and terminates;FLUSHDBon one port leaves the other shards' keys intact.