From d6e469644a172b0deb311205780639cb49e207c9 Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Mon, 15 Jun 2026 19:42:56 -0700 Subject: [PATCH 1/2] raft: return error for rejected conf change --- raft.go | 10 ++++++++++ raft_test.go | 6 +++++- testdata/confchange_v2_add_single_explicit.txt | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/raft.go b/raft.go index 5d92f7b36..dedd255e8 100644 --- a/raft.go +++ b/raft.go @@ -87,6 +87,10 @@ const noLimit = math.MaxUint64 // so that the proposer can be notified and fail fast. var ErrProposalDropped = errors.New("raft proposal dropped") +// ErrConfChangeRejected is returned when a configuration change proposal is +// rejected by raft validation and converted to a no-op entry. +var ErrConfChangeRejected = errors.New("raft conf change rejected") + // lockedRand is a small wrapper around rand.Rand to provide // synchronization among multiple raft groups. Only the methods needed // by the code are exposed (e.g. Intn). @@ -1306,6 +1310,8 @@ func stepLeader(r *raft, m *pb.Message) error { return ErrProposalDropped } + rejectedConfChange := false + for i := range m.GetEntries() { e := m.GetEntries()[i] var cc pb.ConfChangeI @@ -1337,6 +1343,7 @@ func stepLeader(r *raft, m *pb.Message) error { } if failedCheck != "" && !r.disableConfChangeValidation { + rejectedConfChange = true r.logger.Infof("%x ignoring conf change %s at config %s: %s", r.id, DescribeConfChange(cc), r.trk.Config, failedCheck) m.GetEntries()[i] = &pb.Entry{Type: pb.EntryNormal.Enum()} } else { @@ -1350,6 +1357,9 @@ func stepLeader(r *raft, m *pb.Message) error { return ErrProposalDropped } r.bcastAppend() + if rejectedConfChange { + return ErrConfChangeRejected + } return nil case pb.MsgReadIndex: // only one voting member (the leader) in the cluster diff --git a/raft_test.go b/raft_test.go index 8d82cf842..a39e59a3a 100644 --- a/raft_test.go +++ b/raft_test.go @@ -2807,10 +2807,14 @@ func TestStepIgnoreConfig(t *testing.T) { r := newTestRaft(1, 10, 1, newTestMemoryStorage(withPeers(1, 2))) r.becomeCandidate() r.becomeLeader() + r.Step(&pb.Message{From: new(uint64(1)), To: new(uint64(1)), Type: pb.MsgProp.Enum(), Entries: []*pb.Entry{{Type: pb.EntryConfChange.Enum()}}}) index := r.raftLog.lastIndex() pendingConfIndex := r.pendingConfIndex - r.Step(&pb.Message{From: new(uint64(1)), To: new(uint64(1)), Type: pb.MsgProp.Enum(), Entries: []*pb.Entry{{Type: pb.EntryConfChange.Enum()}}}) + + err := r.Step(&pb.Message{From: new(uint64(1)), To: new(uint64(1)), Type: pb.MsgProp.Enum(), Entries: []*pb.Entry{{Type: pb.EntryConfChange.Enum()}}}) + require.Equal(t, ErrConfChangeRejected, err) + wents := []*pb.Entry{{Type: pb.EntryNormal.Enum(), Term: new(uint64(1)), Index: new(uint64(3)), Data: nil}} ents, err := r.raftLog.entries(index+1, noLimit) require.NoError(t, err) diff --git a/testdata/confchange_v2_add_single_explicit.txt b/testdata/confchange_v2_add_single_explicit.txt index 012827690..f8408a069 100644 --- a/testdata/confchange_v2_add_single_explicit.txt +++ b/testdata/confchange_v2_add_single_explicit.txt @@ -103,6 +103,7 @@ propose-conf-change 1 v3 v4 v5 ---- INFO 1 ignoring conf change transition:ConfChangeTransitionAuto changes:{type:ConfChangeAddNode node_id:3} changes:{type:ConfChangeAddNode node_id:4} changes:{type:ConfChangeAddNode node_id:5} at config voters=(1 2)&&(1): must transition out of joint config first +raft conf change rejected # Propose a transition out of the joint config. We'll see this at index 6 below. propose-conf-change 1 @@ -165,6 +166,7 @@ stabilize propose-conf-change 1 ---- INFO 1 ignoring conf change transition:ConfChangeTransitionAuto at config voters=(1 2): not in joint state; refusing empty conf change +raft conf change rejected # Finishes work for the empty entry we just proposed. stabilize From 4e7ec16b82f369d37a258aadbcb35db2d7e24604 Mon Sep 17 00:00:00 2001 From: Nasit Sarwar Sony Date: Tue, 16 Jun 2026 19:34:13 -0700 Subject: [PATCH 2/2] raft: match exact storage errors in bootstrap test --- rawnode_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rawnode_test.go b/rawnode_test.go index 24b92a36f..af35178a4 100644 --- a/rawnode_test.go +++ b/rawnode_test.go @@ -606,13 +606,13 @@ func TestRawNodeStart(t *testing.T) { _, err = storage.Entries(fi, fi, math.MaxUint64) // TODO(tbg): match exact error - require.Error(t, err, "should not have been able to load first index") + require.Error(t, err, ErrCompacted) li, err := storage.LastIndex() require.NoError(t, err) _, err = storage.Entries(li, li, math.MaxUint64) - require.Error(t, err, "should not have been able to load last index") + require.Error(t, err, ErrUnavailable) hs, ics, err := storage.InitialState() require.NoError(t, err)