Bläddra i källkod

Merge pull request #11037 from tbg/interactive

raft: proactively probe newly added followers
Tobias Grieger 6 år sedan
förälder
incheckning
4a2b4c8f7e

+ 8 - 4
raft/confchange/confchange.go

@@ -257,11 +257,15 @@ func (c Changer) initProgress(cfg *tracker.Config, prs tracker.ProgressMap, id u
 		nilAwareAdd(&cfg.Learners, id)
 		nilAwareAdd(&cfg.Learners, id)
 	}
 	}
 	prs[id] = &tracker.Progress{
 	prs[id] = &tracker.Progress{
-		// We initialize Progress.Next with lastIndex+1 so that the peer will be
-		// probed without an index first.
+		// Initializing the Progress with the last index means that the follower
+		// can be probed (with the last index).
 		//
 		//
-		// TODO(tbg): verify that, this is just my best guess.
-		Next:      c.LastIndex + 1,
+		// TODO(tbg): seems awfully optimistic. Using the first index would be
+		// better. The general expectation here is that the follower has no log
+		// at all (and will thus likely need a snapshot), though the app may
+		// have applied a snapshot out of band before adding the replica (thus
+		// making the first index the better choice).
+		Next:      c.LastIndex,
 		Match:     0,
 		Match:     0,
 		Inflights: tracker.NewInflights(c.Tracker.MaxInflight),
 		Inflights: tracker.NewInflights(c.Tracker.MaxInflight),
 		IsLearner: isLearner,
 		IsLearner: isLearner,

+ 7 - 7
raft/confchange/testdata/joint_autoleave.txt

@@ -5,16 +5,16 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 # Autoleave is reflected in the config.
 # Autoleave is reflected in the config.
 enter-joint autoleave=true
 enter-joint autoleave=true
 v2 v3
 v2 v3
 ----
 ----
 voters=(1 2 3)&&(1) autoleave
 voters=(1 2 3)&&(1) autoleave
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
-3: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
+3: StateProbe match=0 next=1
 
 
 # Can't enter-joint twice, even if autoleave changes.
 # Can't enter-joint twice, even if autoleave changes.
 enter-joint autoleave=false
 enter-joint autoleave=false
@@ -24,6 +24,6 @@ config is already joint
 leave-joint
 leave-joint
 ----
 ----
 voters=(1 2 3)
 voters=(1 2 3)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
-3: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
+3: StateProbe match=0 next=1

+ 7 - 7
raft/confchange/testdata/joint_idempotency.txt

@@ -5,19 +5,19 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 enter-joint
 enter-joint
 r1 r2 r9 v2 v3 v4 v2 v3 v4 l2 l2 r4 r4 l1 l1
 r1 r2 r9 v2 v3 v4 v2 v3 v4 l2 l2 r4 r4 l1 l1
 ----
 ----
 voters=(3)&&(1) learners=(2) learners_next=(1)
 voters=(3)&&(1) learners=(2) learners_next=(1)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2 learner
-3: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1 learner
+3: StateProbe match=0 next=1
 
 
 leave-joint
 leave-joint
 ----
 ----
 voters=(3) learners=(1 2)
 voters=(3) learners=(1 2)
-1: StateProbe match=0 next=1 learner
-2: StateProbe match=0 next=2 learner
-3: StateProbe match=0 next=2
+1: StateProbe match=0 next=0 learner
+2: StateProbe match=0 next=1 learner
+3: StateProbe match=0 next=1

+ 5 - 5
raft/confchange/testdata/joint_learners_next.txt

@@ -8,17 +8,17 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 enter-joint
 enter-joint
 v2 l1
 v2 l1
 ----
 ----
 voters=(2)&&(1) learners_next=(1)
 voters=(2)&&(1) learners_next=(1)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
 
 
 leave-joint
 leave-joint
 ----
 ----
 voters=(2) learners=(1)
 voters=(2) learners=(1)
-1: StateProbe match=0 next=1 learner
-2: StateProbe match=0 next=2
+1: StateProbe match=0 next=0 learner
+2: StateProbe match=0 next=1

+ 14 - 14
raft/confchange/testdata/joint_safety.txt

@@ -15,7 +15,7 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=4
+1: StateProbe match=0 next=3
 
 
 leave-joint
 leave-joint
 ----
 ----
@@ -25,7 +25,7 @@ can't leave a non-joint config
 enter-joint
 enter-joint
 ----
 ----
 voters=(1)&&(1)
 voters=(1)&&(1)
-1: StateProbe match=0 next=4
+1: StateProbe match=0 next=3
 
 
 enter-joint
 enter-joint
 ----
 ----
@@ -34,7 +34,7 @@ config is already joint
 leave-joint
 leave-joint
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=4
+1: StateProbe match=0 next=3
 
 
 leave-joint
 leave-joint
 ----
 ----
@@ -45,10 +45,10 @@ enter-joint
 r1 v2 v3 l4
 r1 v2 v3 l4
 ----
 ----
 voters=(2 3)&&(1) learners=(4)
 voters=(2 3)&&(1) learners=(4)
-1: StateProbe match=0 next=4
-2: StateProbe match=0 next=10
-3: StateProbe match=0 next=10
-4: StateProbe match=0 next=10 learner
+1: StateProbe match=0 next=3
+2: StateProbe match=0 next=9
+3: StateProbe match=0 next=9
+4: StateProbe match=0 next=9 learner
 
 
 enter-joint
 enter-joint
 ----
 ----
@@ -67,15 +67,15 @@ can't apply simple config change in joint config
 leave-joint
 leave-joint
 ----
 ----
 voters=(2 3) learners=(4)
 voters=(2 3) learners=(4)
-2: StateProbe match=0 next=10
-3: StateProbe match=0 next=10
-4: StateProbe match=0 next=10 learner
+2: StateProbe match=0 next=9
+3: StateProbe match=0 next=9
+4: StateProbe match=0 next=9 learner
 
 
 simple
 simple
 l9
 l9
 ----
 ----
 voters=(2 3) learners=(4 9)
 voters=(2 3) learners=(4 9)
-2: StateProbe match=0 next=10
-3: StateProbe match=0 next=10
-4: StateProbe match=0 next=10 learner
-9: StateProbe match=0 next=15 learner
+2: StateProbe match=0 next=9
+3: StateProbe match=0 next=9
+4: StateProbe match=0 next=9 learner
+9: StateProbe match=0 next=14 learner

+ 15 - 15
raft/confchange/testdata/simple_idempotency.txt

@@ -2,68 +2,68 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 simple
 simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 simple
 simple
 v2
 v2
 ----
 ----
 voters=(1 2)
 voters=(1 2)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=3
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 l1
 l1
 ----
 ----
 voters=(2) learners=(1)
 voters=(2) learners=(1)
-1: StateProbe match=0 next=1 learner
-2: StateProbe match=0 next=3
+1: StateProbe match=0 next=0 learner
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 l1
 l1
 ----
 ----
 voters=(2) learners=(1)
 voters=(2) learners=(1)
-1: StateProbe match=0 next=1 learner
-2: StateProbe match=0 next=3
+1: StateProbe match=0 next=0 learner
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 r1
 r1
 ----
 ----
 voters=(2)
 voters=(2)
-2: StateProbe match=0 next=3
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 r1
 r1
 ----
 ----
 voters=(2)
 voters=(2)
-2: StateProbe match=0 next=3
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 v3
 v3
 ----
 ----
 voters=(2 3)
 voters=(2 3)
-2: StateProbe match=0 next=3
-3: StateProbe match=0 next=8
+2: StateProbe match=0 next=2
+3: StateProbe match=0 next=7
 
 
 simple
 simple
 r3
 r3
 ----
 ----
 voters=(2)
 voters=(2)
-2: StateProbe match=0 next=3
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 r3
 r3
 ----
 ----
 voters=(2)
 voters=(2)
-2: StateProbe match=0 next=3
+2: StateProbe match=0 next=2
 
 
 simple
 simple
 r4
 r4
 ----
 ----
 voters=(2)
 voters=(2)
-2: StateProbe match=0 next=3
+2: StateProbe match=0 next=2

+ 18 - 18
raft/confchange/testdata/simple_promote_demote.txt

@@ -4,22 +4,22 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 simple
 simple
 v2
 v2
 ----
 ----
 voters=(1 2)
 voters=(1 2)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
 
 
 simple
 simple
 v3
 v3
 ----
 ----
 voters=(1 2 3)
 voters=(1 2 3)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
-3: StateProbe match=0 next=3
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
+3: StateProbe match=0 next=2
 
 
 # Can atomically demote and promote without a hitch.
 # Can atomically demote and promote without a hitch.
 # This is pointless, but possible.
 # This is pointless, but possible.
@@ -27,18 +27,18 @@ simple
 l1 v1
 l1 v1
 ----
 ----
 voters=(1 2 3)
 voters=(1 2 3)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
-3: StateProbe match=0 next=3
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
+3: StateProbe match=0 next=2
 
 
 # Can demote a voter.
 # Can demote a voter.
 simple
 simple
 l2
 l2
 ----
 ----
 voters=(1 3) learners=(2)
 voters=(1 3) learners=(2)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2 learner
-3: StateProbe match=0 next=3
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1 learner
+3: StateProbe match=0 next=2
 
 
 # Can atomically promote and demote the same voter.
 # Can atomically promote and demote the same voter.
 # This is pointless, but possible.
 # This is pointless, but possible.
@@ -46,15 +46,15 @@ simple
 v2 l2
 v2 l2
 ----
 ----
 voters=(1 3) learners=(2)
 voters=(1 3) learners=(2)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2 learner
-3: StateProbe match=0 next=3
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1 learner
+3: StateProbe match=0 next=2
 
 
 # Can promote a voter.
 # Can promote a voter.
 simple
 simple
 v2
 v2
 ----
 ----
 voters=(1 2 3)
 voters=(1 2 3)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
-3: StateProbe match=0 next=3
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
+3: StateProbe match=0 next=2

+ 10 - 10
raft/confchange/testdata/simple_safety.txt

@@ -7,15 +7,15 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=2
+1: StateProbe match=0 next=1
 
 
 simple
 simple
 v2 l3
 v2 l3
 ----
 ----
 voters=(1 2) learners=(3)
 voters=(1 2) learners=(3)
-1: StateProbe match=0 next=2
-2: StateProbe match=0 next=3
-3: StateProbe match=0 next=3 learner
+1: StateProbe match=0 next=1
+2: StateProbe match=0 next=2
+3: StateProbe match=0 next=2 learner
 
 
 simple
 simple
 r1 v5
 r1 v5
@@ -46,11 +46,11 @@ simple
 l2 l3 l4 l5
 l2 l3 l4 l5
 ----
 ----
 voters=(1) learners=(2 3 4 5)
 voters=(1) learners=(2 3 4 5)
-1: StateProbe match=0 next=2
-2: StateProbe match=0 next=3 learner
-3: StateProbe match=0 next=3 learner
-4: StateProbe match=0 next=9 learner
-5: StateProbe match=0 next=9 learner
+1: StateProbe match=0 next=1
+2: StateProbe match=0 next=2 learner
+3: StateProbe match=0 next=2 learner
+4: StateProbe match=0 next=8 learner
+5: StateProbe match=0 next=8 learner
 
 
 simple
 simple
 r1
 r1
@@ -61,4 +61,4 @@ simple
 r2 r3 r4 r5
 r2 r3 r4 r5
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=2
+1: StateProbe match=0 next=1

+ 5 - 5
raft/confchange/testdata/update.txt

@@ -6,18 +6,18 @@ simple
 v1
 v1
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0
 
 
 simple
 simple
 v2 u1
 v2 u1
 ----
 ----
 voters=(1 2)
 voters=(1 2)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1
 
 
 simple
 simple
 u1 u2 u3 u1 u2 u3
 u1 u2 u3 u1 u2 u3
 ----
 ----
 voters=(1 2)
 voters=(1 2)
-1: StateProbe match=0 next=1
-2: StateProbe match=0 next=2
+1: StateProbe match=0 next=0
+2: StateProbe match=0 next=1

+ 1 - 1
raft/confchange/testdata/zero.txt

@@ -3,4 +3,4 @@ simple
 v1 r0 v0 l0
 v1 r0 v0 l0
 ----
 ----
 voters=(1)
 voters=(1)
-1: StateProbe match=0 next=1
+1: StateProbe match=0 next=0

+ 3 - 0
raft/interaction_test.go

@@ -22,6 +22,9 @@ import (
 )
 )
 
 
 func TestInteraction(t *testing.T) {
 func TestInteraction(t *testing.T) {
+	// NB: if this test fails, run `go test ./raft -rewrite` and inspect the
+	// diff. Only commit the changes if you understand what caused them and if
+	// they are desired.
 	datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
 	datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
 		env := rafttest.NewInteractionEnv(nil)
 		env := rafttest.NewInteractionEnv(nil)
 		datadriven.RunTest(t, path, func(d *datadriven.TestData) string {
 		datadriven.RunTest(t, path, func(d *datadriven.TestData) string {

+ 40 - 6
raft/raft.go

@@ -1036,10 +1036,36 @@ func stepLeader(r *raft, m pb.Message) error {
 
 
 		for i := range m.Entries {
 		for i := range m.Entries {
 			e := &m.Entries[i]
 			e := &m.Entries[i]
-			if e.Type == pb.EntryConfChange || e.Type == pb.EntryConfChangeV2 {
-				if r.pendingConfIndex > r.raftLog.applied {
-					r.logger.Infof("%x propose conf %s ignored since pending unapplied configuration [index %d, applied %d]",
-						r.id, e, r.pendingConfIndex, r.raftLog.applied)
+			var cc pb.ConfChangeI
+			if e.Type == pb.EntryConfChange {
+				var ccc pb.ConfChange
+				if err := ccc.Unmarshal(e.Data); err != nil {
+					panic(err)
+				}
+				cc = ccc
+			} else if e.Type == pb.EntryConfChangeV2 {
+				var ccc pb.ConfChangeV2
+				if err := ccc.Unmarshal(e.Data); err != nil {
+					panic(err)
+				}
+				cc = ccc
+			}
+			if cc != nil {
+				alreadyPending := r.pendingConfIndex > r.raftLog.applied
+				alreadyJoint := len(r.prs.Config.Voters[1]) > 0
+				wantsLeaveJoint := len(cc.AsV2().Changes) == 0
+
+				var refused string
+				if alreadyPending {
+					refused = fmt.Sprintf("possible unapplied conf change at index %d (applied to %d)", r.pendingConfIndex, r.raftLog.applied)
+				} else if alreadyJoint && !wantsLeaveJoint {
+					refused = "must transition out of joint config first"
+				} else if !alreadyJoint && wantsLeaveJoint {
+					refused = "not in joint state; refusing empty conf change"
+				}
+
+				if refused != "" {
+					r.logger.Infof("%x ignoring conf change %v at config %s: %s", r.id, cc, r.prs.Config, refused)
 					m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
 					m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
 				} else {
 				} else {
 					r.pendingConfIndex = r.raftLog.lastIndex() + uint64(i) + 1
 					r.pendingConfIndex = r.raftLog.lastIndex() + uint64(i) + 1
@@ -1527,10 +1553,18 @@ func (r *raft) switchToConfig(cfg tracker.Config, prs tracker.ProgressMap) pb.Co
 	if r.state != StateLeader || len(cs.Voters) == 0 {
 	if r.state != StateLeader || len(cs.Voters) == 0 {
 		return cs
 		return cs
 	}
 	}
+
 	if r.maybeCommit() {
 	if r.maybeCommit() {
-		// The quorum size may have been reduced (but not to zero), so see if
-		// any pending entries can be committed.
+		// If the configuration change means that more entries are committed now,
+		// broadcast/append to everyone in the updated config.
 		r.bcastAppend()
 		r.bcastAppend()
+	} else {
+		// Otherwise, still probe the newly added replicas; there's no reason to
+		// let them wait out a heartbeat interval (or the next incoming
+		// proposal).
+		r.prs.Visit(func(id uint64, pr *tracker.Progress) {
+			r.maybeSendAppend(id, false /* sendIfEmpty */)
+		})
 	}
 	}
 	// If the the leadTransferee was removed, abort the leadership transfer.
 	// If the the leadTransferee was removed, abort the leadership transfer.
 	if _, tOK := r.prs.Progress[r.leadTransferee]; !tOK && r.leadTransferee != 0 {
 	if _, tOK := r.prs.Progress[r.leadTransferee]; !tOK && r.leadTransferee != 0 {

+ 17 - 0
raft/rafttest/interaction_env_handler.go

@@ -30,6 +30,16 @@ func (env *InteractionEnv) Handle(t *testing.T, d datadriven.TestData) string {
 	env.Output.Reset()
 	env.Output.Reset()
 	var err error
 	var err error
 	switch d.Cmd {
 	switch d.Cmd {
+	case "_breakpoint":
+		// This is a helper case to attach a debugger to when a problem needs
+		// to be investigated in a longer test file. In such a case, add the
+		// following stanza immediately before the interesting behavior starts:
+		//
+		// _breakpoint:
+		// ----
+		// ok
+		//
+		// and set a breakpoint on the `case` above.
 	case "add-nodes":
 	case "add-nodes":
 		// Example:
 		// Example:
 		//
 		//
@@ -94,6 +104,13 @@ func (env *InteractionEnv) Handle(t *testing.T, d datadriven.TestData) string {
 		//
 		//
 		// tick-heartbeat 3
 		// tick-heartbeat 3
 		err = env.handleTickHeartbeat(t, d)
 		err = env.handleTickHeartbeat(t, d)
+	case "propose":
+		// Propose an entry.
+		//
+		// Example:
+		//
+		// propose 1 foo
+		err = env.handlePropose(t, d)
 	case "propose-conf-change":
 	case "propose-conf-change":
 		// Propose a configuration change.
 		// Propose a configuration change.
 		//
 		//

+ 60 - 24
raft/rafttest/interaction_env_handler_deliver_msgs.go

@@ -15,8 +15,8 @@
 package rafttest
 package rafttest
 
 
 import (
 import (
-	"errors"
 	"fmt"
 	"fmt"
+	"strconv"
 	"testing"
 	"testing"
 
 
 	"github.com/cockroachdb/datadriven"
 	"github.com/cockroachdb/datadriven"
@@ -25,34 +25,70 @@ import (
 )
 )
 
 
 func (env *InteractionEnv) handleDeliverMsgs(t *testing.T, d datadriven.TestData) error {
 func (env *InteractionEnv) handleDeliverMsgs(t *testing.T, d datadriven.TestData) error {
-	if len(env.Messages) == 0 {
-		return errors.New("no messages to deliver")
+	var rs []Recipient
+	for _, arg := range d.CmdArgs {
+		if len(arg.Vals) == 0 {
+			id, err := strconv.ParseUint(arg.Key, 10, 64)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rs = append(rs, Recipient{ID: id})
+		}
+		for i := range arg.Vals {
+			switch arg.Key {
+			case "drop":
+				var id uint64
+				arg.Scan(t, i, &id)
+				var found bool
+				for _, r := range rs {
+					if r.ID == id {
+						found = true
+					}
+				}
+				if found {
+					t.Fatalf("can't both deliver and drop msgs to %d", id)
+				}
+				rs = append(rs, Recipient{ID: id, Drop: true})
+			}
+		}
 	}
 	}
 
 
-	msgs := env.Messages
-	env.Messages = nil
+	if n := env.DeliverMsgs(rs...); n == 0 {
+		env.Output.WriteString("no messages\n")
+	}
+	return nil
+}
 
 
-	return env.DeliverMsgs(msgs)
+type Recipient struct {
+	ID   uint64
+	Drop bool
 }
 }
 
 
-// DeliverMsgs delivers the supplied messages typically taken from env.Messages.
-func (env *InteractionEnv) DeliverMsgs(msgs []raftpb.Message) error {
-	for _, msg := range msgs {
-		toIdx := int(msg.To - 1)
-		var drop bool
-		if toIdx >= len(env.Nodes) {
-			// Drop messages for peers that don't exist yet.
-			drop = true
-			env.Output.WriteString("dropped: ")
-		}
-		fmt.Fprintln(env.Output, raft.DescribeMessage(msg, defaultEntryFormatter))
-		if drop {
-			continue
-		}
-		if err := env.Nodes[toIdx].Step(msg); err != nil {
-			env.Output.WriteString(err.Error())
-			continue
+// DeliverMsgs goes through env.Messages and, depending on the Drop flag,
+// delivers or drops messages to the specified Recipients. Returns the
+// number of messages handled (i.e. delivered or dropped). A handled message
+// is removed from env.Messages.
+func (env *InteractionEnv) DeliverMsgs(rs ...Recipient) int {
+	var n int
+	for _, r := range rs {
+		var msgs []raftpb.Message
+		msgs, env.Messages = splitMsgs(env.Messages, r.ID)
+		n += len(msgs)
+		for _, msg := range msgs {
+			if r.Drop {
+				fmt.Fprint(env.Output, "dropped: ")
+			}
+			fmt.Fprintln(env.Output, raft.DescribeMessage(msg, defaultEntryFormatter))
+			if r.Drop {
+				// NB: it's allowed to drop messages to nodes that haven't been instantiated yet,
+				// we haven't used msg.To yet.
+				continue
+			}
+			toIdx := int(msg.To - 1)
+			if err := env.Nodes[toIdx].Step(msg); err != nil {
+				env.Output.WriteString(err.Error())
+			}
 		}
 		}
 	}
 	}
-	return nil
+	return n
 }
 }

+ 8 - 10
raft/rafttest/interaction_env_handler_process_ready.go

@@ -19,7 +19,6 @@ import (
 
 
 	"github.com/cockroachdb/datadriven"
 	"github.com/cockroachdb/datadriven"
 	"go.etcd.io/etcd/raft"
 	"go.etcd.io/etcd/raft"
-	"go.etcd.io/etcd/raft/quorum"
 	"go.etcd.io/etcd/raft/raftpb"
 	"go.etcd.io/etcd/raft/raftpb"
 )
 )
 
 
@@ -33,6 +32,7 @@ func (env *InteractionEnv) ProcessReady(idx int) error {
 	// TODO(tbg): Allow simulating crashes here.
 	// TODO(tbg): Allow simulating crashes here.
 	rn, s := env.Nodes[idx].RawNode, env.Nodes[idx].Storage
 	rn, s := env.Nodes[idx].RawNode, env.Nodes[idx].Storage
 	rd := rn.Ready()
 	rd := rn.Ready()
+	env.Output.WriteString(raft.DescribeReady(rd, defaultEntryFormatter))
 	// TODO(tbg): the order of operations here is not necessarily safe. See:
 	// TODO(tbg): the order of operations here is not necessarily safe. See:
 	// https://github.com/etcd-io/etcd/pull/10861
 	// https://github.com/etcd-io/etcd/pull/10861
 	if !raft.IsEmptyHardState(rd.HardState) {
 	if !raft.IsEmptyHardState(rd.HardState) {
@@ -50,6 +50,7 @@ func (env *InteractionEnv) ProcessReady(idx int) error {
 	}
 	}
 	for _, ent := range rd.CommittedEntries {
 	for _, ent := range rd.CommittedEntries {
 		var update []byte
 		var update []byte
+		var cs *raftpb.ConfState
 		switch ent.Type {
 		switch ent.Type {
 		case raftpb.EntryConfChange:
 		case raftpb.EntryConfChange:
 			var cc raftpb.ConfChange
 			var cc raftpb.ConfChange
@@ -57,13 +58,13 @@ func (env *InteractionEnv) ProcessReady(idx int) error {
 				return err
 				return err
 			}
 			}
 			update = cc.Context
 			update = cc.Context
-			rn.ApplyConfChange(cc)
+			cs = rn.ApplyConfChange(cc)
 		case raftpb.EntryConfChangeV2:
 		case raftpb.EntryConfChangeV2:
 			var cc raftpb.ConfChangeV2
 			var cc raftpb.ConfChangeV2
 			if err := cc.Unmarshal(ent.Data); err != nil {
 			if err := cc.Unmarshal(ent.Data); err != nil {
 				return err
 				return err
 			}
 			}
-			rn.ApplyConfChange(cc)
+			cs = rn.ApplyConfChange(cc)
 			update = cc.Context
 			update = cc.Context
 		default:
 		default:
 			update = ent.Data
 			update = ent.Data
@@ -78,19 +79,16 @@ func (env *InteractionEnv) ProcessReady(idx int) error {
 		snap.Data = append(snap.Data, update...)
 		snap.Data = append(snap.Data, update...)
 		snap.Metadata.Index = ent.Index
 		snap.Metadata.Index = ent.Index
 		snap.Metadata.Term = ent.Term
 		snap.Metadata.Term = ent.Term
-		cfg := rn.Status().Config
-		snap.Metadata.ConfState = raftpb.ConfState{
-			Voters:         cfg.Voters[0].Slice(),
-			VotersOutgoing: cfg.Voters[1].Slice(),
-			Learners:       quorum.MajorityConfig(cfg.Learners).Slice(),
-			LearnersNext:   quorum.MajorityConfig(cfg.LearnersNext).Slice(),
+		if cs == nil {
+			sl := env.Nodes[idx].History
+			cs = &sl[len(sl)-1].Metadata.ConfState
 		}
 		}
+		snap.Metadata.ConfState = *cs
 		env.Nodes[idx].History = append(env.Nodes[idx].History, snap)
 		env.Nodes[idx].History = append(env.Nodes[idx].History, snap)
 	}
 	}
 	for _, msg := range rd.Messages {
 	for _, msg := range rd.Messages {
 		env.Messages = append(env.Messages, msg)
 		env.Messages = append(env.Messages, msg)
 	}
 	}
 	rn.Advance(rd)
 	rn.Advance(rd)
-	env.Output.WriteString(raft.DescribeReady(rd, defaultEntryFormatter))
 	return nil
 	return nil
 }
 }

+ 34 - 0
raft/rafttest/interaction_env_handler_propose.go

@@ -0,0 +1,34 @@
+// Copyright 2019 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rafttest
+
+import (
+	"testing"
+
+	"github.com/cockroachdb/datadriven"
+)
+
+func (env *InteractionEnv) handlePropose(t *testing.T, d datadriven.TestData) error {
+	idx := firstAsNodeIdx(t, d)
+	if len(d.CmdArgs) != 2 || len(d.CmdArgs[1].Vals) > 0 {
+		t.Fatalf("expected exactly one key with no vals: %+v", d.CmdArgs[1:])
+	}
+	return env.Propose(idx, []byte(d.CmdArgs[1].Key))
+}
+
+// Propose a regular entry.
+func (env *InteractionEnv) Propose(idx int, data []byte) error {
+	return env.Nodes[idx].Propose(data)
+}

+ 10 - 8
raft/rafttest/interaction_env_handler_stabilize.go

@@ -65,22 +65,24 @@ func (env *InteractionEnv) Stabilize(idxs ...int) error {
 				withIndent(func() { env.ProcessReady(idx) })
 				withIndent(func() { env.ProcessReady(idx) })
 			}
 			}
 		}
 		}
-		var msgs []raftpb.Message
 		for _, rn := range nodes {
 		for _, rn := range nodes {
-			msgs, env.Messages = splitMsgs(env.Messages, rn.Status().ID)
-			if len(msgs) > 0 {
-				fmt.Fprintf(env.Output, "> delivering messages\n")
-				withIndent(func() { env.DeliverMsgs(msgs) })
+			id := rn.Status().ID
+			// NB: we grab the messages just to see whether to print the header.
+			// DeliverMsgs will do it again.
+			if msgs, _ := splitMsgs(env.Messages, id); len(msgs) > 0 {
+				fmt.Fprintf(env.Output, "> %d receiving messages\n", id)
+				withIndent(func() { env.DeliverMsgs(Recipient{ID: id}) })
 				done = false
 				done = false
 			}
 			}
-			if done {
-				return nil
-			}
+		}
+		if done {
+			return nil
 		}
 		}
 	}
 	}
 }
 }
 
 
 func splitMsgs(msgs []raftpb.Message, to uint64) (toMsgs []raftpb.Message, rmdr []raftpb.Message) {
 func splitMsgs(msgs []raftpb.Message, to uint64) (toMsgs []raftpb.Message, rmdr []raftpb.Message) {
+	// NB: this method does not reorder messages.
 	for _, msg := range msgs {
 	for _, msg := range msgs {
 		if msg.To == to {
 		if msg.To == to {
 			toMsgs = append(toMsgs, msg)
 			toMsgs = append(toMsgs, msg)

+ 9 - 9
raft/testdata/campaign.txt

@@ -31,12 +31,12 @@ stabilize
   Messages:
   Messages:
   1->2 MsgVote Term:1 Log:1/2
   1->2 MsgVote Term:1 Log:1/2
   1->3 MsgVote Term:1 Log:1/2
   1->3 MsgVote Term:1 Log:1/2
-> delivering messages
+> 2 receiving messages
   1->2 MsgVote Term:1 Log:1/2
   1->2 MsgVote Term:1 Log:1/2
   INFO 2 [term: 0] received a MsgVote message with higher term from 1 [term: 1]
   INFO 2 [term: 0] received a MsgVote message with higher term from 1 [term: 1]
   INFO 2 became follower at term 1
   INFO 2 became follower at term 1
   INFO 2 [logterm: 1, index: 2, vote: 0] cast MsgVote for 1 [logterm: 1, index: 2] at term 1
   INFO 2 [logterm: 1, index: 2, vote: 0] cast MsgVote for 1 [logterm: 1, index: 2] at term 1
-> delivering messages
+> 3 receiving messages
   1->3 MsgVote Term:1 Log:1/2
   1->3 MsgVote Term:1 Log:1/2
   INFO 3 [term: 0] received a MsgVote message with higher term from 1 [term: 1]
   INFO 3 [term: 0] received a MsgVote message with higher term from 1 [term: 1]
   INFO 3 became follower at term 1
   INFO 3 became follower at term 1
@@ -51,7 +51,7 @@ stabilize
   HardState Term:1 Vote:1 Commit:2
   HardState Term:1 Vote:1 Commit:2
   Messages:
   Messages:
   3->1 MsgVoteResp Term:1 Log:0/0
   3->1 MsgVoteResp Term:1 Log:0/0
-> delivering messages
+> 1 receiving messages
   2->1 MsgVoteResp Term:1 Log:0/0
   2->1 MsgVoteResp Term:1 Log:0/0
   INFO 1 received MsgVoteResp from 2 at term 1
   INFO 1 received MsgVoteResp from 2 at term 1
   INFO 1 has received 2 MsgVoteResp votes and 0 vote rejections
   INFO 1 has received 2 MsgVoteResp votes and 0 vote rejections
@@ -65,9 +65,9 @@ stabilize
   Messages:
   Messages:
   1->2 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
   1->2 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
   1->3 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
   1->3 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
-> delivering messages
+> 2 receiving messages
   1->2 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
   1->2 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
-> delivering messages
+> 3 receiving messages
   1->3 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
   1->3 MsgApp Term:1 Log:1/2 Commit:2 Entries:[1/3 EntryNormal ""]
 > 2 handling Ready
 > 2 handling Ready
   Ready MustSync=true:
   Ready MustSync=true:
@@ -83,7 +83,7 @@ stabilize
   1/3 EntryNormal ""
   1/3 EntryNormal ""
   Messages:
   Messages:
   3->1 MsgAppResp Term:1 Log:0/3
   3->1 MsgAppResp Term:1 Log:0/3
-> delivering messages
+> 1 receiving messages
   2->1 MsgAppResp Term:1 Log:0/3
   2->1 MsgAppResp Term:1 Log:0/3
   3->1 MsgAppResp Term:1 Log:0/3
   3->1 MsgAppResp Term:1 Log:0/3
 > 1 handling Ready
 > 1 handling Ready
@@ -94,9 +94,9 @@ stabilize
   Messages:
   Messages:
   1->2 MsgApp Term:1 Log:1/3 Commit:3
   1->2 MsgApp Term:1 Log:1/3 Commit:3
   1->3 MsgApp Term:1 Log:1/3 Commit:3
   1->3 MsgApp Term:1 Log:1/3 Commit:3
-> delivering messages
+> 2 receiving messages
   1->2 MsgApp Term:1 Log:1/3 Commit:3
   1->2 MsgApp Term:1 Log:1/3 Commit:3
-> delivering messages
+> 3 receiving messages
   1->3 MsgApp Term:1 Log:1/3 Commit:3
   1->3 MsgApp Term:1 Log:1/3 Commit:3
 > 2 handling Ready
 > 2 handling Ready
   Ready MustSync=false:
   Ready MustSync=false:
@@ -112,6 +112,6 @@ stabilize
   1/3 EntryNormal ""
   1/3 EntryNormal ""
   Messages:
   Messages:
   3->1 MsgAppResp Term:1 Log:0/3
   3->1 MsgAppResp Term:1 Log:0/3
-> delivering messages
+> 1 receiving messages
   2->1 MsgAppResp Term:1 Log:0/3
   2->1 MsgAppResp Term:1 Log:0/3
   3->1 MsgAppResp Term:1 Log:0/3
   3->1 MsgAppResp Term:1 Log:0/3

+ 152 - 0
raft/testdata/campaign_learner_must_vote.txt

@@ -0,0 +1,152 @@
+# Regression test that verifies that learners can vote. This holds only in the
+# sense that if a learner is asked to vote, a candidate believes that they are a
+# voter based on its current config, which may be more recent than that of the
+# learner. If learners which are actually voters but don't know it yet don't
+# vote in that situation, the raft group may end up unavailable despite a quorum
+# of voters (as of the latest config) being available.
+#
+# See:
+# https://github.com/etcd-io/etcd/pull/10998
+
+# Turn output off during boilerplate.
+log-level none
+----
+ok
+
+# Bootstrap three nodes.
+add-nodes 3 voters=(1,2) learners=(3) index=2
+----
+ok
+
+# n1 gets to be leader.
+campaign 1
+----
+ok
+
+stabilize
+----
+ok (quiet)
+
+# Propose a conf change on n1 that promotes n3 to voter.
+propose-conf-change 1
+v3
+----
+ok
+
+# Commit and fully apply said conf change. n1 and n2 now consider n3 a voter.
+stabilize 1 2
+----
+ok (quiet)
+
+# Drop all inflight messages to 3. We don't want it to be caught up when it is
+# asked to vote.
+deliver-msgs drop=(3)
+----
+ok (quiet)
+
+# We now pretend that n1 is dead, and n2 is trying to become leader.
+
+log-level debug
+----
+ok
+
+campaign 2
+----
+INFO 2 is starting a new election at term 1
+INFO 2 became candidate at term 2
+INFO 2 received MsgVoteResp from 2 at term 2
+INFO 2 [logterm: 1, index: 4] sent MsgVote request to 1 at term 2
+INFO 2 [logterm: 1, index: 4] sent MsgVote request to 3 at term 2
+
+# Send out the MsgVote requests.
+process-ready 2
+----
+Ready MustSync=true:
+Lead:0 State:StateCandidate
+HardState Term:2 Vote:2 Commit:4
+Messages:
+2->1 MsgVote Term:2 Log:1/4
+2->3 MsgVote Term:2 Log:1/4
+
+# n2 is now campaigning while n1 is down (does not respond). The latest config
+# has n3 as a voter, but n3 doesn't even have the corresponding conf change in
+# its log. Still, it casts a vote for n2 which can in turn become leader and
+# catches up n3.
+stabilize 3
+----
+> 3 receiving messages
+  2->3 MsgVote Term:2 Log:1/4
+  INFO 3 [term: 1] received a MsgVote message with higher term from 2 [term: 2]
+  INFO 3 became follower at term 2
+  INFO 3 [logterm: 1, index: 3, vote: 0] cast MsgVote for 2 [logterm: 1, index: 4] at term 2
+> 3 handling Ready
+  Ready MustSync=true:
+  Lead:0 State:StateFollower
+  HardState Term:2 Vote:2 Commit:3
+  Messages:
+  3->2 MsgVoteResp Term:2 Log:0/0
+
+stabilize 2 3
+----
+> 2 receiving messages
+  3->2 MsgVoteResp Term:2 Log:0/0
+  INFO 2 received MsgVoteResp from 3 at term 2
+  INFO 2 has received 2 MsgVoteResp votes and 0 vote rejections
+  INFO 2 became leader at term 2
+> 2 handling Ready
+  Ready MustSync=true:
+  Lead:2 State:StateLeader
+  Entries:
+  2/5 EntryNormal ""
+  Messages:
+  2->1 MsgApp Term:2 Log:1/4 Commit:4 Entries:[2/5 EntryNormal ""]
+  2->3 MsgApp Term:2 Log:1/4 Commit:4 Entries:[2/5 EntryNormal ""]
+> 3 receiving messages
+  2->3 MsgApp Term:2 Log:1/4 Commit:4 Entries:[2/5 EntryNormal ""]
+  DEBUG 3 [logterm: 0, index: 4] rejected MsgApp [logterm: 1, index: 4] from 2
+> 3 handling Ready
+  Ready MustSync=false:
+  Lead:2 State:StateFollower
+  Messages:
+  3->2 MsgAppResp Term:2 Log:0/4 Rejected (Hint: 3)
+> 2 receiving messages
+  3->2 MsgAppResp Term:2 Log:0/4 Rejected (Hint: 3)
+  DEBUG 2 received MsgAppResp(MsgApp was rejected, lastindex: 3) from 3 for index 4
+  DEBUG 2 decreased progress of 3 to [StateProbe match=0 next=4]
+> 2 handling Ready
+  Ready MustSync=false:
+  Messages:
+  2->3 MsgApp Term:2 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v3, 2/5 EntryNormal ""]
+> 3 receiving messages
+  2->3 MsgApp Term:2 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v3, 2/5 EntryNormal ""]
+> 3 handling Ready
+  Ready MustSync=true:
+  HardState Term:2 Vote:2 Commit:4
+  Entries:
+  1/4 EntryConfChangeV2 v3
+  2/5 EntryNormal ""
+  CommittedEntries:
+  1/4 EntryConfChangeV2 v3
+  Messages:
+  3->2 MsgAppResp Term:2 Log:0/5
+  INFO 3 switched to configuration voters=(1 2 3)
+> 2 receiving messages
+  3->2 MsgAppResp Term:2 Log:0/5
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:2 Vote:2 Commit:5
+  CommittedEntries:
+  2/5 EntryNormal ""
+  Messages:
+  2->3 MsgApp Term:2 Log:2/5 Commit:5
+> 3 receiving messages
+  2->3 MsgApp Term:2 Log:2/5 Commit:5
+> 3 handling Ready
+  Ready MustSync=false:
+  HardState Term:2 Vote:2 Commit:5
+  CommittedEntries:
+  2/5 EntryNormal ""
+  Messages:
+  3->2 MsgAppResp Term:2 Log:0/5
+> 2 receiving messages
+  3->2 MsgAppResp Term:2 Log:0/5

+ 0 - 78
raft/testdata/confchange_v1.txt

@@ -1,78 +0,0 @@
-add-nodes 1 voters=(1) index=2
-----
-INFO 1 switched to configuration voters=(1)
-INFO 1 became follower at term 0
-INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
-
-campaign 1
-----
-INFO 1 is starting a new election at term 0
-INFO 1 became candidate at term 1
-INFO 1 received MsgVoteResp from 1 at term 1
-INFO 1 became leader at term 1
-
-propose-conf-change 1
-v2 v3
-----
-ok
-
-add-nodes 2
-
-process-ready 1
-----
-INFO 2 switched to configuration voters=()
-INFO 2 became follower at term 0
-INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
-INFO 3 switched to configuration voters=()
-INFO 3 became follower at term 0
-INFO newRaft 3 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
-
-stabilize 1
-----
-> 1 handling Ready
-  INFO 1 switched to configuration voters=(1 2 3)&&(1) autoleave
-  INFO initiating automatic transition out of joint configuration voters=(1 2 3)&&(1) autoleave
-  Ready MustSync=true:
-  Lead:1 State:StateLeader
-  HardState Term:1 Vote:1 Commit:4
-  Entries:
-  1/3 EntryNormal ""
-  1/4 EntryConfChangeV2 v2 v3
-  CommittedEntries:
-  1/3 EntryNormal ""
-  1/4 EntryConfChangeV2 v2 v3
-> 1 handling Ready
-  Ready MustSync=true:
-  Entries:
-  1/5 EntryConfChangeV2
-
-# NB: this test is broken from here on because the leader doesn't propagate the
-# commit index proactively, see the buglet #11002.
-
-stabilize 2
-----
-ok
-
-stabilize 1
-----
-ok
-
-stabilize 2
-----
-ok
-
-stabilize 1
-----
-ok
-
-stabilize 2
-----
-ok
-
-stabilize 1
-----
-ok
-
-stabilize 2
-----
-ok

+ 97 - 0
raft/testdata/confchange_v1_add_single.txt

@@ -0,0 +1,97 @@
+# Run a V1 membership change that adds a single voter.
+
+# Bootstrap n1.
+add-nodes 1 voters=(1) index=2
+----
+INFO 1 switched to configuration voters=(1)
+INFO 1 became follower at term 0
+INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
+
+campaign 1
+----
+INFO 1 is starting a new election at term 0
+INFO 1 became candidate at term 1
+INFO 1 received MsgVoteResp from 1 at term 1
+INFO 1 became leader at term 1
+
+# Add v2 (with an auto transition).
+propose-conf-change 1 v1=true
+v2
+----
+ok
+
+# Pull n2 out of thin air.
+add-nodes 1
+----
+INFO 2 switched to configuration voters=()
+INFO 2 became follower at term 0
+INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
+
+# n1 commits the conf change using itself as commit quorum, immediately transitions into
+# the final config, and catches up n2. Note that it's using an EntryConfChange, not an
+# EntryConfChangeV2, so this is compatible with nodes that don't know about V2 conf changes.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateLeader
+  HardState Term:1 Vote:1 Commit:4
+  Entries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChange v2
+  CommittedEntries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChange v2
+  INFO 1 switched to configuration voters=(1 2)
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChange v2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChange v2]
+  INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
+  INFO 2 became follower at term 1
+  DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
+> 2 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateFollower
+  HardState Term:1 Commit:0
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+  DEBUG 1 received MsgAppResp(MsgApp was rejected, lastindex: 0) from 2 for index 3
+  DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
+  DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
+  DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+> 2 receiving messages
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+  INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
+  INFO 2 switched to configuration voters=(1 2)
+  INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
+  INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:4
+  Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+  DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4
+> 2 handling Ready
+  Ready MustSync=false:
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4

+ 224 - 0
raft/testdata/confchange_v1_remove_leader.txt

@@ -0,0 +1,224 @@
+# We'll turn this back on after the boilerplate.
+log-level none
+----
+ok
+
+# Run a V1 membership change that removes the leader.
+# Bootstrap n1, n2, n3.
+add-nodes 3 voters=(1,2,3) index=2
+----
+ok
+
+campaign 1
+----
+ok
+
+stabilize
+----
+ok (quiet)
+
+log-level debug
+----
+ok
+
+# Start removing n1.
+propose-conf-change 1 v1=true
+r1
+----
+ok
+
+# Propose an extra entry which will be sent out together with the conf change.
+propose 1 foo
+----
+ok
+
+# Send out the corresponding appends.
+process-ready 1
+----
+Ready MustSync=true:
+Entries:
+1/4 EntryConfChange r1
+1/5 EntryNormal "foo"
+Messages:
+1->2 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
+1->3 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
+1->2 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
+1->3 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
+
+# Send response from n2 (which is enough to commit the entries so far next time
+# n1 runs).
+stabilize 2
+----
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
+  1->2 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/4 EntryConfChange r1
+  1/5 EntryNormal "foo"
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+  2->1 MsgAppResp Term:1 Log:0/5
+
+# Put another entry in n1's log.
+propose 1 bar
+----
+ok
+
+# n1 applies the conf change, so it has now removed itself. But it still has
+# an uncommitted entry in the log. If the leader unconditionally counted itself
+# as part of the commit quorum, we'd be in trouble. In the block below, we see
+# it send out appends to the other nodes for the 'bar' entry.
+stabilize 1
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/6 EntryNormal "bar"
+  Messages:
+  1->2 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
+  1->3 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+  2->1 MsgAppResp Term:1 Log:0/5
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:5
+  CommittedEntries:
+  1/4 EntryConfChange r1
+  1/5 EntryNormal "foo"
+  Messages:
+  1->2 MsgApp Term:1 Log:1/6 Commit:4
+  1->3 MsgApp Term:1 Log:1/6 Commit:4
+  1->2 MsgApp Term:1 Log:1/6 Commit:5
+  1->3 MsgApp Term:1 Log:1/6 Commit:5
+  INFO 1 switched to configuration voters=(2 3)
+
+# n2 responds, n3 doesn't yet. Quorum for 'bar' should not be reached...
+stabilize 2
+----
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
+  1->2 MsgApp Term:1 Log:1/6 Commit:4
+  1->2 MsgApp Term:1 Log:1/6 Commit:5
+> 2 handling Ready
+  Ready MustSync=true:
+  HardState Term:1 Vote:1 Commit:5
+  Entries:
+  1/6 EntryNormal "bar"
+  CommittedEntries:
+  1/4 EntryConfChange r1
+  1/5 EntryNormal "foo"
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+  INFO 2 switched to configuration voters=(2 3)
+
+# ... which thankfully is what we see on the leader.
+stabilize 1
+----
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+
+# When n3 responds, quorum is reached and everything falls into place.
+stabilize
+----
+> 3 receiving messages
+  1->3 MsgApp Term:1 Log:1/3 Commit:3 Entries:[1/4 EntryConfChange r1]
+  1->3 MsgApp Term:1 Log:1/4 Commit:3 Entries:[1/5 EntryNormal "foo"]
+  1->3 MsgApp Term:1 Log:1/5 Commit:3 Entries:[1/6 EntryNormal "bar"]
+  1->3 MsgApp Term:1 Log:1/6 Commit:4
+  1->3 MsgApp Term:1 Log:1/6 Commit:5
+> 3 handling Ready
+  Ready MustSync=true:
+  HardState Term:1 Vote:1 Commit:5
+  Entries:
+  1/4 EntryConfChange r1
+  1/5 EntryNormal "foo"
+  1/6 EntryNormal "bar"
+  CommittedEntries:
+  1/4 EntryConfChange r1
+  1/5 EntryNormal "foo"
+  Messages:
+  3->1 MsgAppResp Term:1 Log:0/4
+  3->1 MsgAppResp Term:1 Log:0/5
+  3->1 MsgAppResp Term:1 Log:0/6
+  3->1 MsgAppResp Term:1 Log:0/6
+  3->1 MsgAppResp Term:1 Log:0/6
+  INFO 3 switched to configuration voters=(2 3)
+> 1 receiving messages
+  3->1 MsgAppResp Term:1 Log:0/4
+  3->1 MsgAppResp Term:1 Log:0/5
+  3->1 MsgAppResp Term:1 Log:0/6
+  3->1 MsgAppResp Term:1 Log:0/6
+  3->1 MsgAppResp Term:1 Log:0/6
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:6
+  CommittedEntries:
+  1/6 EntryNormal "bar"
+  Messages:
+  1->2 MsgApp Term:1 Log:1/6 Commit:6
+  1->3 MsgApp Term:1 Log:1/6 Commit:6
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/6 Commit:6
+> 3 receiving messages
+  1->3 MsgApp Term:1 Log:1/6 Commit:6
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:6
+  CommittedEntries:
+  1/6 EntryNormal "bar"
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/6
+> 3 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:6
+  CommittedEntries:
+  1/6 EntryNormal "bar"
+  Messages:
+  3->1 MsgAppResp Term:1 Log:0/6
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/6
+  3->1 MsgAppResp Term:1 Log:0/6
+
+# However not all is well. n1 is still leader but unconditionally drops all
+# proposals on the floor, so we're effectively stuck if it still heartbeats
+# its followers...
+propose 1 baz
+----
+raft proposal dropped
+
+tick-heartbeat 1
+----
+ok
+
+# ... which, uh oh, it does.
+# TODO(tbg): change behavior so that a leader that is removed immediately steps
+# down, and initiates an optimistic handover.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgHeartbeat Term:1 Log:0/0 Commit:6
+  1->3 MsgHeartbeat Term:1 Log:0/0 Commit:6
+> 2 receiving messages
+  1->2 MsgHeartbeat Term:1 Log:0/0 Commit:6
+> 3 receiving messages
+  1->3 MsgHeartbeat Term:1 Log:0/0 Commit:6
+> 2 handling Ready
+  Ready MustSync=false:
+  Messages:
+  2->1 MsgHeartbeatResp Term:1 Log:0/0
+> 3 handling Ready
+  Ready MustSync=false:
+  Messages:
+  3->1 MsgHeartbeatResp Term:1 Log:0/0
+> 1 receiving messages
+  2->1 MsgHeartbeatResp Term:1 Log:0/0
+  3->1 MsgHeartbeatResp Term:1 Log:0/0

+ 197 - 0
raft/testdata/confchange_v2_add_double_auto.txt

@@ -0,0 +1,197 @@
+# Run a V2 membership change that adds two voters at once and auto-leaves the
+# joint configuration. (This is the same as specifying an explicit transition
+# since more than one change is being made atomically).
+
+# Bootstrap n1.
+add-nodes 1 voters=(1) index=2
+----
+INFO 1 switched to configuration voters=(1)
+INFO 1 became follower at term 0
+INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
+
+campaign 1
+----
+INFO 1 is starting a new election at term 0
+INFO 1 became candidate at term 1
+INFO 1 received MsgVoteResp from 1 at term 1
+INFO 1 became leader at term 1
+
+propose-conf-change 1 transition=auto
+v2 v3
+----
+ok
+
+# Add two "empty" nodes to the cluster, n2 and n3.
+add-nodes 2
+----
+INFO 2 switched to configuration voters=()
+INFO 2 became follower at term 0
+INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
+INFO 3 switched to configuration voters=()
+INFO 3 became follower at term 0
+INFO newRaft 3 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
+
+# n1 immediately gets to commit & apply the conf change using only itself. We see that
+# it starts transitioning out of that joint configuration (though we will only see that
+# proposal in the next ready handling loop, when it is emitted). We also see that this
+# is using joint consensus, which it has to since we're carrying out two additions at
+# once.
+process-ready 1
+----
+Ready MustSync=true:
+Lead:1 State:StateLeader
+HardState Term:1 Vote:1 Commit:4
+Entries:
+1/3 EntryNormal ""
+1/4 EntryConfChangeV2 v2 v3
+CommittedEntries:
+1/3 EntryNormal ""
+1/4 EntryConfChangeV2 v2 v3
+INFO 1 switched to configuration voters=(1 2 3)&&(1) autoleave
+INFO initiating automatic transition out of joint configuration voters=(1 2 3)&&(1) autoleave
+
+# n1 immediately probes n2 and n3.
+stabilize 1
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
+  1->3 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
+
+# First, play out the whole interaction between n1 and n2. We see n1's probe to
+# n2 get rejected (since n2 needs a snapshot); the snapshot is delivered at which
+# point n2 switches to the correct config, and n1 catches it up. This notably
+# includes the empty conf change which gets committed and applied by both and
+# which transitions them out of their joint configuration into the final one (1 2 3).
+stabilize 1 2
+----
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
+  INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
+  INFO 2 became follower at term 1
+  DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
+> 2 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateFollower
+  HardState Term:1 Commit:0
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+  DEBUG 1 received MsgAppResp(MsgApp was rejected, lastindex: 0) from 2 for index 3
+  DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
+  DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
+  DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
+> 2 receiving messages
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
+  INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
+  INFO 2 switched to configuration voters=(1 2 3)&&(1) autoleave
+  INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
+  INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:4
+  Snapshot Index:4 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+  DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/5
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/5
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:5
+  CommittedEntries:
+  1/5 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/5 Commit:5
+  INFO 1 switched to configuration voters=(1 2 3)
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/5 Commit:5
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:5
+  CommittedEntries:
+  1/5 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/5
+  INFO 2 switched to configuration voters=(1 2 3)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/5
+
+# n3 immediately receives a snapshot in the final configuration.
+stabilize 1 3
+----
+> 3 receiving messages
+  1->3 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2 v3]
+  INFO 3 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
+  INFO 3 became follower at term 1
+  DEBUG 3 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
+> 3 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateFollower
+  HardState Term:1 Commit:0
+  Messages:
+  3->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+> 1 receiving messages
+  3->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+  DEBUG 1 received MsgAppResp(MsgApp was rejected, lastindex: 0) from 3 for index 3
+  DEBUG 1 decreased progress of 3 to [StateProbe match=0 next=1]
+  DEBUG 1 [firstindex: 3, commit: 5] sent snapshot[index: 5, term: 1] to 3 [StateProbe match=0 next=1]
+  DEBUG 1 paused sending replication messages to 3 [StateSnapshot match=0 next=1 paused pendingSnap=5]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:5 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+> 3 receiving messages
+  1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:5 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+  INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 5, term: 1]
+  INFO 3 switched to configuration voters=(1 2 3)
+  INFO 3 [commit: 5, lastindex: 5, lastterm: 1] restored snapshot [index: 5, term: 1]
+  INFO 3 [commit: 5] restored snapshot [index: 5, term: 1]
+> 3 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:5
+  Snapshot Index:5 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+  Messages:
+  3->1 MsgAppResp Term:1 Log:0/5
+> 1 receiving messages
+  3->1 MsgAppResp Term:1 Log:0/5
+  DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 3 [StateSnapshot match=5 next=6 paused pendingSnap=5]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->3 MsgApp Term:1 Log:1/5 Commit:5
+> 3 receiving messages
+  1->3 MsgApp Term:1 Log:1/5 Commit:5
+> 3 handling Ready
+  Ready MustSync=false:
+  Messages:
+  3->1 MsgAppResp Term:1 Log:0/5
+> 1 receiving messages
+  3->1 MsgAppResp Term:1 Log:0/5
+
+# Nothing else happens.
+stabilize
+----
+ok

+ 125 - 0
raft/testdata/confchange_v2_add_double_implicit.txt

@@ -0,0 +1,125 @@
+# Run a V2 membership change that adds a single voter but explicitly asks for the
+# use of joint consensus (with auto-leaving).
+
+# TODO(tbg): also verify that if the leader changes while in the joint state, the
+# new leader will auto-transition out of the joint state just the same.
+
+# Bootstrap n1.
+add-nodes 1 voters=(1) index=2
+----
+INFO 1 switched to configuration voters=(1)
+INFO 1 became follower at term 0
+INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
+
+campaign 1
+----
+INFO 1 is starting a new election at term 0
+INFO 1 became candidate at term 1
+INFO 1 received MsgVoteResp from 1 at term 1
+INFO 1 became leader at term 1
+
+propose-conf-change 1 transition=implicit
+v2
+----
+ok
+
+# Add n2.
+add-nodes 1
+----
+INFO 2 switched to configuration voters=()
+INFO 2 became follower at term 0
+INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
+
+# n1 commits the conf change using itself as commit quorum, then starts catching up n2.
+# When that's done, it starts auto-transitioning out. Note that the snapshots propagating
+# the joint config have the AutoLeave flag set in their config.
+stabilize 1 2
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateLeader
+  HardState Term:1 Vote:1 Commit:4
+  Entries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChangeV2 v2
+  CommittedEntries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChangeV2 v2
+  INFO 1 switched to configuration voters=(1 2)&&(1) autoleave
+  INFO initiating automatic transition out of joint configuration voters=(1 2)&&(1) autoleave
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
+  INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
+  INFO 2 became follower at term 1
+  DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
+> 2 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateFollower
+  HardState Term:1 Commit:0
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+  DEBUG 1 received MsgAppResp(MsgApp was rejected, lastindex: 0) from 2 for index 3
+  DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
+  DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
+  DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
+> 2 receiving messages
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
+  INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
+  INFO 2 switched to configuration voters=(1 2)&&(1) autoleave
+  INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
+  INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:4
+  Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:true
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+  DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/5
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/5
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:5
+  CommittedEntries:
+  1/5 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/5 Commit:5
+  INFO 1 switched to configuration voters=(1 2)
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/5 Commit:5
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:5
+  CommittedEntries:
+  1/5 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/5
+  INFO 2 switched to configuration voters=(1 2)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/5

+ 198 - 0
raft/testdata/confchange_v2_add_single_auto.txt

@@ -0,0 +1,198 @@
+# Run a V2 membership change that adds a single voter in auto mode, which means
+# that joint consensus is not used but a direct transition into the new config
+# takes place.
+
+# Bootstrap n1.
+add-nodes 1 voters=(1) index=2
+----
+INFO 1 switched to configuration voters=(1)
+INFO 1 became follower at term 0
+INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
+
+campaign 1
+----
+INFO 1 is starting a new election at term 0
+INFO 1 became candidate at term 1
+INFO 1 received MsgVoteResp from 1 at term 1
+INFO 1 became leader at term 1
+
+# Add v2 (with an auto transition).
+propose-conf-change 1
+v2
+----
+ok
+
+# Pull n2 out of thin air.
+add-nodes 1
+----
+INFO 2 switched to configuration voters=()
+INFO 2 became follower at term 0
+INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
+
+# n1 commits the conf change using itself as commit quorum, immediately transitions into
+# the final config, and catches up n2.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateLeader
+  HardState Term:1 Vote:1 Commit:4
+  Entries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChangeV2 v2
+  CommittedEntries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChangeV2 v2
+  INFO 1 switched to configuration voters=(1 2)
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
+  INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
+  INFO 2 became follower at term 1
+  DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
+> 2 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateFollower
+  HardState Term:1 Commit:0
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+  DEBUG 1 received MsgAppResp(MsgApp was rejected, lastindex: 0) from 2 for index 3
+  DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
+  DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
+  DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+> 2 receiving messages
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+  INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
+  INFO 2 switched to configuration voters=(1 2)
+  INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
+  INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:4
+  Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+  DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4
+> 2 handling Ready
+  Ready MustSync=false:
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+
+# Check that we're not allowed to change membership again while in the joint state.
+# This leads to an empty entry being proposed instead (index 5 in the stabilize block
+# below).
+propose-conf-change 1
+v3 v4 v5
+----
+ok
+
+# Propose a transition out of the joint config. We'll see this at index 6 below.
+propose-conf-change 1
+----
+INFO 1 ignoring conf change {ConfChangeTransitionAuto [] [] []} at config voters=(1 2): possible unapplied conf change at index 5 (applied to 4)
+
+# The group commits the command and everyone switches to the final config.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryConfChangeV2 v3 v4 v5
+  1/6 EntryNormal ""
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2 v3 v4 v5]
+  1->2 MsgApp Term:1 Log:1/5 Commit:4 Entries:[1/6 EntryNormal ""]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryConfChangeV2 v3 v4 v5]
+  1->2 MsgApp Term:1 Log:1/5 Commit:4 Entries:[1/6 EntryNormal ""]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryConfChangeV2 v3 v4 v5
+  1/6 EntryNormal ""
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/5
+  2->1 MsgAppResp Term:1 Log:0/6
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/5
+  2->1 MsgAppResp Term:1 Log:0/6
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:6
+  CommittedEntries:
+  1/5 EntryConfChangeV2 v3 v4 v5
+  1/6 EntryNormal ""
+  Messages:
+  1->2 MsgApp Term:1 Log:1/6 Commit:5
+  1->2 MsgApp Term:1 Log:1/6 Commit:6
+  INFO 1 switched to configuration voters=(1 2 3 4 5)&&(1 2) autoleave
+  INFO initiating automatic transition out of joint configuration voters=(1 2 3 4 5)&&(1 2) autoleave
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/6 Commit:5
+  1->2 MsgApp Term:1 Log:1/6 Commit:6
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/7 EntryConfChangeV2
+  Messages:
+  1->3 MsgApp Term:1 Log:1/5 Commit:6 Entries:[1/6 EntryNormal ""]
+  1->4 MsgApp Term:1 Log:1/5 Commit:6 Entries:[1/6 EntryNormal ""]
+  1->5 MsgApp Term:1 Log:1/5 Commit:6 Entries:[1/6 EntryNormal ""]
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:6
+  CommittedEntries:
+  1/5 EntryConfChangeV2 v3 v4 v5
+  1/6 EntryNormal ""
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+  INFO 2 switched to configuration voters=(1 2 3 4 5)&&(1 2) autoleave
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+
+# Check that trying to transition out again won't do anything.
+propose-conf-change 1
+----
+ok
+
+# Finishes work for the empty entry we just proposed.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/8 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/6 Commit:6 Entries:[1/7 EntryConfChangeV2, 1/8 EntryConfChangeV2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/6 Commit:6 Entries:[1/7 EntryConfChangeV2, 1/8 EntryConfChangeV2]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/7 EntryConfChangeV2
+  1/8 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/8
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/8

+ 206 - 0
raft/testdata/confchange_v2_add_single_explicit.txt

@@ -0,0 +1,206 @@
+# Run a V2 membership change that adds a single voter but explicitly asks for the
+# use of joint consensus, including wanting to transition out of the joint config
+# manually.
+
+# Bootstrap n1.
+add-nodes 1 voters=(1) index=2
+----
+INFO 1 switched to configuration voters=(1)
+INFO 1 became follower at term 0
+INFO newRaft 1 [peers: [1], term: 0, commit: 2, applied: 2, lastindex: 2, lastterm: 1]
+
+campaign 1
+----
+INFO 1 is starting a new election at term 0
+INFO 1 became candidate at term 1
+INFO 1 received MsgVoteResp from 1 at term 1
+INFO 1 became leader at term 1
+
+# Add v2 with an explicit transition.
+propose-conf-change 1 transition=explicit
+v2
+----
+ok
+
+# Pull n2 out of thin air.
+add-nodes 1
+----
+INFO 2 switched to configuration voters=()
+INFO 2 became follower at term 0
+INFO newRaft 2 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
+
+# n1 commits the conf change using itself as commit quorum, then starts catching up n2.
+# Everyone remains in the joint config. Note that the snapshot below has AutoLeave unset.
+stabilize 1 2
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateLeader
+  HardState Term:1 Vote:1 Commit:4
+  Entries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChangeV2 v2
+  CommittedEntries:
+  1/3 EntryNormal ""
+  1/4 EntryConfChangeV2 v2
+  INFO 1 switched to configuration voters=(1 2)&&(1)
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/3 Commit:4 Entries:[1/4 EntryConfChangeV2 v2]
+  INFO 2 [term: 0] received a MsgApp message with higher term from 1 [term: 1]
+  INFO 2 became follower at term 1
+  DEBUG 2 [logterm: 0, index: 3] rejected MsgApp [logterm: 1, index: 3] from 1
+> 2 handling Ready
+  Ready MustSync=true:
+  Lead:1 State:StateFollower
+  HardState Term:1 Commit:0
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/3 Rejected (Hint: 0)
+  DEBUG 1 received MsgAppResp(MsgApp was rejected, lastindex: 0) from 2 for index 3
+  DEBUG 1 decreased progress of 2 to [StateProbe match=0 next=1]
+  DEBUG 1 [firstindex: 3, commit: 4] sent snapshot[index: 4, term: 1] to 2 [StateProbe match=0 next=1]
+  DEBUG 1 paused sending replication messages to 2 [StateSnapshot match=0 next=1 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:false
+> 2 receiving messages
+  1->2 MsgSnap Term:1 Log:0/0 Snapshot: Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:false
+  INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 4, term: 1]
+  INFO 2 switched to configuration voters=(1 2)&&(1)
+  INFO 2 [commit: 4, lastindex: 4, lastterm: 1] restored snapshot [index: 4, term: 1]
+  INFO 2 [commit: 4] restored snapshot [index: 4, term: 1]
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:4
+  Snapshot Index:4 Term:1 ConfState:Voters:[1 2] VotersOutgoing:[1] Learners:[] LearnersNext:[] AutoLeave:false
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+  DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 2 [StateSnapshot match=4 next=5 paused pendingSnap=4]
+> 1 handling Ready
+  Ready MustSync=false:
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4
+> 2 handling Ready
+  Ready MustSync=false:
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/4
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/4
+
+# Check that we're not allowed to change membership again while in the joint state.
+# This leads to an empty entry being proposed instead (index 5 in the stabilize block
+# below).
+propose-conf-change 1
+v3 v4 v5
+----
+INFO 1 ignoring conf change {ConfChangeTransitionAuto [{ConfChangeAddNode 3 []} {ConfChangeAddNode 4 []} {ConfChangeAddNode 5 []}] [] []} at config voters=(1 2)&&(1): must transition out of joint config first
+
+# Propose a transition out of the joint config. We'll see this at index 6 below.
+propose-conf-change 1
+----
+ok
+
+# The group commits the command and everyone switches to the final config.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryNormal ""
+  1/6 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryNormal ""]
+  1->2 MsgApp Term:1 Log:1/5 Commit:4 Entries:[1/6 EntryConfChangeV2]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/4 Commit:4 Entries:[1/5 EntryNormal ""]
+  1->2 MsgApp Term:1 Log:1/5 Commit:4 Entries:[1/6 EntryConfChangeV2]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/5 EntryNormal ""
+  1/6 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/5
+  2->1 MsgAppResp Term:1 Log:0/6
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/5
+  2->1 MsgAppResp Term:1 Log:0/6
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:6
+  CommittedEntries:
+  1/5 EntryNormal ""
+  1/6 EntryConfChangeV2
+  Messages:
+  1->2 MsgApp Term:1 Log:1/6 Commit:5
+  1->2 MsgApp Term:1 Log:1/6 Commit:6
+  INFO 1 switched to configuration voters=(1 2)
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/6 Commit:5
+  1->2 MsgApp Term:1 Log:1/6 Commit:6
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:6
+  CommittedEntries:
+  1/5 EntryNormal ""
+  1/6 EntryConfChangeV2
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+  INFO 2 switched to configuration voters=(1 2)
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/6
+  2->1 MsgAppResp Term:1 Log:0/6
+
+# Check that trying to transition out again won't do anything.
+propose-conf-change 1
+----
+INFO 1 ignoring conf change {ConfChangeTransitionAuto [] [] []} at config voters=(1 2): not in joint state; refusing empty conf change
+
+# Finishes work for the empty entry we just proposed.
+stabilize
+----
+> 1 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/7 EntryNormal ""
+  Messages:
+  1->2 MsgApp Term:1 Log:1/6 Commit:6 Entries:[1/7 EntryNormal ""]
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/6 Commit:6 Entries:[1/7 EntryNormal ""]
+> 2 handling Ready
+  Ready MustSync=true:
+  Entries:
+  1/7 EntryNormal ""
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/7
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/7
+> 1 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Vote:1 Commit:7
+  CommittedEntries:
+  1/7 EntryNormal ""
+  Messages:
+  1->2 MsgApp Term:1 Log:1/7 Commit:7
+> 2 receiving messages
+  1->2 MsgApp Term:1 Log:1/7 Commit:7
+> 2 handling Ready
+  Ready MustSync=false:
+  HardState Term:1 Commit:7
+  CommittedEntries:
+  1/7 EntryNormal ""
+  Messages:
+  2->1 MsgAppResp Term:1 Log:0/7
+> 1 receiving messages
+  2->1 MsgAppResp Term:1 Log:0/7

+ 24 - 9
raft/testdata/snapshot_succeed_via_app_resp.txt

@@ -30,7 +30,7 @@ compact 1 11
 ok (quiet)
 ok (quiet)
 
 
 # Drop inflight messages to n3.
 # Drop inflight messages to n3.
-deliver-msgs 3
+deliver-msgs drop=(3)
 ----
 ----
 ok (quiet)
 ok (quiet)
 
 
@@ -70,7 +70,7 @@ Messages:
 # and responds.
 # and responds.
 stabilize 3
 stabilize 3
 ----
 ----
-> delivering messages
+> 3 receiving messages
   1->3 MsgHeartbeat Term:1 Log:0/0
   1->3 MsgHeartbeat Term:1 Log:0/0
   INFO 3 [term: 0] received a MsgHeartbeat message with higher term from 1 [term: 1]
   INFO 3 [term: 0] received a MsgHeartbeat message with higher term from 1 [term: 1]
   INFO 3 became follower at term 1
   INFO 3 became follower at term 1
@@ -84,14 +84,14 @@ stabilize 3
 # The leader in turn will realize that n3 needs a snapshot, which it initiates.
 # The leader in turn will realize that n3 needs a snapshot, which it initiates.
 stabilize 1
 stabilize 1
 ----
 ----
-> delivering messages
+> 1 receiving messages
   3->1 MsgHeartbeatResp Term:1 Log:0/0
   3->1 MsgHeartbeatResp Term:1 Log:0/0
   DEBUG 1 [firstindex: 12, commit: 11] sent snapshot[index: 11, term: 1] to 3 [StateProbe match=0 next=11]
   DEBUG 1 [firstindex: 12, commit: 11] sent snapshot[index: 11, term: 1] to 3 [StateProbe match=0 next=11]
   DEBUG 1 paused sending replication messages to 3 [StateSnapshot match=0 next=11 paused pendingSnap=11]
   DEBUG 1 paused sending replication messages to 3 [StateSnapshot match=0 next=11 paused pendingSnap=11]
 > 1 handling Ready
 > 1 handling Ready
   Ready MustSync=false:
   Ready MustSync=false:
   Messages:
   Messages:
-  1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[]
+  1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
 
 
 status 1
 status 1
 ----
 ----
@@ -105,8 +105,8 @@ status 1
 # was now fully caught up.
 # was now fully caught up.
 stabilize 3
 stabilize 3
 ----
 ----
-> delivering messages
-  1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[]
+> 3 receiving messages
+  1->3 MsgSnap Term:1 Log:0/0 Snapshot: Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
   INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 11, term: 1]
   INFO log [committed=0, applied=0, unstable.offset=1, len(unstable.Entries)=0] starts to restore snapshot [index: 11, term: 1]
   INFO 3 switched to configuration voters=(1 2 3)
   INFO 3 switched to configuration voters=(1 2 3)
   INFO 3 [commit: 11, lastindex: 11, lastterm: 1] restored snapshot [index: 11, term: 1]
   INFO 3 [commit: 11, lastindex: 11, lastterm: 1] restored snapshot [index: 11, term: 1]
@@ -114,7 +114,7 @@ stabilize 3
 > 3 handling Ready
 > 3 handling Ready
   Ready MustSync=false:
   Ready MustSync=false:
   HardState Term:1 Commit:11
   HardState Term:1 Commit:11
-  Snapshot Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[]
+  Snapshot Index:11 Term:1 ConfState:Voters:[1 2 3] VotersOutgoing:[] Learners:[] LearnersNext:[] AutoLeave:false
   Messages:
   Messages:
   3->1 MsgAppResp Term:1 Log:0/11
   3->1 MsgAppResp Term:1 Log:0/11
 
 
@@ -122,7 +122,7 @@ stabilize 3
 # Leader sends another MsgAppResp, to communicate the updated commit index.
 # Leader sends another MsgAppResp, to communicate the updated commit index.
 stabilize 1
 stabilize 1
 ----
 ----
-> delivering messages
+> 1 receiving messages
   3->1 MsgAppResp Term:1 Log:0/11
   3->1 MsgAppResp Term:1 Log:0/11
   DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 3 [StateSnapshot match=11 next=12 paused pendingSnap=11]
   DEBUG 1 recovered from needing snapshot, resumed sending replication messages to 3 [StateSnapshot match=11 next=12 paused pendingSnap=11]
 > 1 handling Ready
 > 1 handling Ready
@@ -136,6 +136,21 @@ status 1
 2: StateReplicate match=11 next=12
 2: StateReplicate match=11 next=12
 3: StateReplicate match=11 next=12
 3: StateReplicate match=11 next=12
 
 
+# Let things settle.
 stabilize
 stabilize
 ----
 ----
-ok
+> 2 receiving messages
+  1->2 MsgHeartbeat Term:1 Log:0/0 Commit:11
+> 3 receiving messages
+  1->3 MsgApp Term:1 Log:1/11 Commit:11
+> 2 handling Ready
+  Ready MustSync=false:
+  Messages:
+  2->1 MsgHeartbeatResp Term:1 Log:0/0
+> 3 handling Ready
+  Ready MustSync=false:
+  Messages:
+  3->1 MsgAppResp Term:1 Log:0/11
+> 1 receiving messages
+  2->1 MsgHeartbeatResp Term:1 Log:0/0
+  3->1 MsgAppResp Term:1 Log:0/11

+ 2 - 2
raft/util.go

@@ -77,8 +77,8 @@ func DescribeSoftState(ss SoftState) string {
 
 
 func DescribeConfState(state pb.ConfState) string {
 func DescribeConfState(state pb.ConfState) string {
 	return fmt.Sprintf(
 	return fmt.Sprintf(
-		"Voters:%v VotersOutgoing:%v Learners:%v LearnersNext:%v",
-		state.Voters, state.VotersOutgoing, state.Learners, state.LearnersNext,
+		"Voters:%v VotersOutgoing:%v Learners:%v LearnersNext:%v AutoLeave:%v",
+		state.Voters, state.VotersOutgoing, state.Learners, state.LearnersNext, state.AutoLeave,
 	)
 	)
 }
 }