From c94bac43e5779bd6802bcec76800ef4f64bacf5a Mon Sep 17 00:00:00 2001 From: dimgigov Date: Fri, 31 Jul 2026 03:02:32 +0300 Subject: [PATCH] fix(raft): ignore intermediate InstallSnapshot chunk replies --- src/barabadb/core/raft.nim | 8 +++++++- tests/test_all.nim | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/barabadb/core/raft.nim b/src/barabadb/core/raft.nim index b96674d..7f1119d 100644 --- a/src/barabadb/core/raft.nim +++ b/src/barabadb/core/raft.nim @@ -679,7 +679,13 @@ proc handleInstallSnapshotReply*(node: RaftNode, peerId: string, if node.state != rsLeader: return - if reply.success: + if reply.success and reply.matchIdx >= node.lastSnapshotIndex: + # The follower has actually adopted the snapshot base. Intermediate chunk + # replies (the T8 follower acks every non-done chunk with success=true and + # matchIdx = its OLD lastSnapshotIndex, below ours) fall through here and + # must be ignored: applying them would regress matchIndex/nextIndex and + # clear the reject streak mid-transfer, causing state flapping until the + # final reply lands. node.matchIndex[peerId] = reply.matchIdx node.nextIndex[peerId] = reply.matchIdx + 1 node.snapRejectStreak.del(peerId) diff --git a/tests/test_all.nim b/tests/test_all.nim index 29f04a9..5d758c5 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -2931,6 +2931,36 @@ suite "Raft InstallSnapshot Send": check node.nextIndex["peer-1"] == 101 check node.snapRejectStreak["peer-1"] == 2 + test "intermediate chunk replies are ignored; final reply advances state": + var node = newRaftNode("leader", @["peer-1"]) + node.currentTerm = 5 + node.state = rsLeader + node.lastSnapshotIndex = 100 + node.lastSnapshotTerm = 4 + node.nextIndex["peer-1"] = 101 + node.matchIndex["peer-1"] = 0 + node.snapRejectStreak["peer-1"] = 2 + node.snapPending.incl("peer-1") + + # Intermediate chunk ack: follower replies success=true with its OLD + # lastSnapshotIndex (40 < our 100). Leader state must not move. + node.handleInstallSnapshotReply("peer-1", RaftMessage( + kind: rmkInstallSnapshotReply, term: 5, senderId: "peer-1", + success: true, matchIdx: 40)) + check node.matchIndex["peer-1"] == 0 + check node.nextIndex["peer-1"] == 101 + check node.snapRejectStreak["peer-1"] == 2 + check "peer-1" in node.snapPending + + # Final reply: follower adopted the snapshot base (matchIdx == 100). + node.handleInstallSnapshotReply("peer-1", RaftMessage( + kind: rmkInstallSnapshotReply, term: 5, senderId: "peer-1", + success: true, matchIdx: 100)) + check node.matchIndex["peer-1"] == 100 + check node.nextIndex["peer-1"] == 101 + check "peer-1" notin node.snapRejectStreak + check "peer-1" notin node.snapPending + test "InstallSnapshotReply term handling matches AppendEntriesReply": var node = newRaftNode("leader", @["peer-1"]) node.currentTerm = 5