From fa40a99cda2277abcf64d318a98a7f9159083166 Mon Sep 17 00:00:00 2001 From: Oran Agra Date: Thu, 22 Oct 2026 11:05:20 +0300 Subject: [PATCH] CLUSTER RESET on a replica: propagate the flush to the AOF and sub-replicas CLUSTER RESET turns a replica into an empty master: clusterReset() calls replicationUnsetMaster() and then emptyData(). emptyData() only empties memory; nothing is written to the AOF or to the replication stream. So: - The AOF still holds the old dataset. When the node next restarts from it, the old keys come back, although the node was reset to empty (and, being a master now, it may later own slots those keys don't belong to). - Sub-replicas keep their link across replicationUnsetMaster() (the replid is shifted, not reset), so they keep the old dataset too. Fix: after emptyData(), propagate a FLUSHALL (AOF + replication), as FLUSHALL itself would. CLUSTER RESET runs as a command, so it is propagated when the command completes (also inside MULTI/EXEC). Tests: - tests/unit/cluster/cluster-reset-aof.tcl: a replica with an AOF is reset and restarted from its AOF; its dataset must stay empty. Without the fix all its keys come back. - tests/unit/cluster/resharding.tcl: verify that the masters hold exactly the test's keys (sum of DBSIZE), not just that those keys hold the right values, before and after the AOF restart. Co-Authored-By: Claude Opus 5.5 --- src/cluster_legacy.c | 7 +++++++ tests/unit/cluster/resharding.tcl | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/cluster_legacy.c b/src/cluster_legacy.c index a5dac190f1..a89f389344 100644 --- a/src/cluster_legacy.c +++ b/src/cluster_legacy.c @@ -1092,6 +1092,13 @@ void clusterReset(int hard) { clusterSetNodeAsMaster(myself); replicationUnsetMaster(); emptyData(-1,EMPTYDB_NO_FLAGS,NULL); + /* emptyData() only empties memory. Propagate a FLUSHALL so that the AOF + * (which would otherwise resurrect the old dataset on restart) and any + * sub-replicas (which keep their link across replicationUnsetMaster) are + * emptied too. */ + robj *flushall = createStringObject("FLUSHALL",8); + alsoPropagate(-1,&flushall,1,PROPAGATE_AOF|PROPAGATE_REPL); + decrRefCount(flushall); } /* Close slots, reset manual failover state. */ diff --git a/tests/unit/cluster/resharding.tcl b/tests/unit/cluster/resharding.tcl index 6b49bb68f3..d3425bbe35 100644 --- a/tests/unit/cluster/resharding.tcl +++ b/tests/unit/cluster/resharding.tcl @@ -111,6 +111,18 @@ test "Cluster consistency during live resharding" { } } +# The masters must hold exactly our logical content: checking only our keys would +# miss leftovers, e.g. an old dataset resurrected by an AOF restart. +proc verify_no_extra_keys {expected} { + set total 0 + for {set id 0} {$id < [llength $::servers]} {incr id} { + if {[lindex [R $id role] 0] eq {master}} { + incr total [R $id dbsize] + } + } + assert_equal $expected $total +} + test "Verify $numkeys keys for consistency with logical content" { # Check that the Redis Cluster content matches our logical content. foreach {key value} [array get content] { @@ -118,6 +130,7 @@ test "Verify $numkeys keys for consistency with logical content" { fail "Key $key expected to hold '$value' but actual content is [$cluster lrange $key 0 -1]" } } + verify_no_extra_keys [array size content] } test "Terminate and restart all the instances" { @@ -141,6 +154,7 @@ test "Verify $numkeys keys after the restart" { fail "Key $key expected to hold '$value' but actual content is [$cluster lrange $key 0 -1]" } } + verify_no_extra_keys [array size content] } test "Disable AOF in all the instances" {