From 99ae34dc081d444c6a31b722b8c51badf0cf133d Mon Sep 17 00:00:00 2001 From: Tobias Schottdorf Date: Tue, 9 Jul 2019 16:58:40 +0200 Subject: [PATCH] raft: never remove the last voter Before this change, it was possible for a Raft group to apply a configuration change that removes the last voter. Such a group is stuck and it's unclear that this operation is ever useful. On the flip side, it makes an awkward edge case to allow, one that will be more awkward in the context of joint consensus. Remove it and thereby reduce the mental overhead of reasoning about configuration changes. --- raft/raft_test.go | 22 +++++++++++++--------- raft/tracker/tracker.go | 5 +++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/raft/raft_test.go b/raft/raft_test.go index 8fcc2f5c76da..d4adaaafe98f 100644 --- a/raft/raft_test.go +++ b/raft/raft_test.go @@ -1139,10 +1139,16 @@ func TestCommit(t *testing.T) { storage.Append(tt.logs) storage.hardState = pb.HardState{Term: tt.smTerm} - sm := newTestRaft(1, []uint64{1}, 10, 2, storage) - sm.prs.RemoveAny(1) + var ids []uint64 for j := 0; j < len(tt.matches); j++ { - sm.prs.InitProgress(uint64(j)+1, tt.matches[j], tt.matches[j]+1, false) + ids = append(ids, uint64(j+1)) + } + sm := newTestRaft(1, ids, 10, 2, storage) + + for j := 0; j < len(tt.matches); j++ { + id := uint64(j + 1) + sm.prs.Progress[id].Match = tt.matches[j] + sm.prs.Progress[id].Next = tt.matches[j] + 1 } sm.maybeCommit() if g := sm.raftLog.committed; g != tt.w { @@ -3142,9 +3148,8 @@ func TestRemoveNode(t *testing.T) { t.Errorf("nodes = %v, want %v", g, w) } - // remove all nodes from cluster + // The last remaining node will refuse to remove itself. r.applyConfChange(pb.ConfChange{NodeID: 1, Type: pb.ConfChangeRemoveNode}) - w = []uint64{} if g := r.prs.VoterNodes(); !reflect.DeepEqual(g, w) { t.Errorf("nodes = %v, want %v", g, w) } @@ -3160,14 +3165,13 @@ func TestRemoveLearner(t *testing.T) { t.Errorf("nodes = %v, want %v", g, w) } - w = []uint64{} - if g := r.prs.LearnerNodes(); !reflect.DeepEqual(g, w) { + if w, g := []uint64{}, r.prs.LearnerNodes(); !reflect.DeepEqual(g, w) { t.Errorf("nodes = %v, want %v", g, w) } - // remove all nodes from cluster + // The remaining voter will refuse to remove itself. r.applyConfChange(pb.ConfChange{NodeID: 1, Type: pb.ConfChangeRemoveNode}) - if g := r.prs.VoterNodes(); !reflect.DeepEqual(g, w) { + if w, g := []uint64{1}, r.prs.VoterNodes(); !reflect.DeepEqual(g, w) { t.Errorf("nodes = %v, want %v", g, w) } } diff --git a/raft/tracker/tracker.go b/raft/tracker/tracker.go index 4b3396fbe170..24c30563c598 100644 --- a/raft/tracker/tracker.go +++ b/raft/tracker/tracker.go @@ -157,6 +157,11 @@ func (p *ProgressTracker) RemoveAny(id uint64) { panic(fmt.Sprintf("peer %x is both voter and learner", id)) } + if okV1 && len(p.Voters[0]) == 1 { + // Never remove the last voter. + return + } + delete(p.Voters[0], id) delete(p.Voters[1], id) delete(p.Learners, id)